Skip to content

Commit ea5922d

Browse files
authored
feature: add default value API for option/argument (and show it in the help text) (#389)
1 parent 4b13f2e commit ea5922d

14 files changed

+254
-66
lines changed

src/CommandLineUtils/Abstractions/ValueParserProvider.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public IValueParser GetParser(Type type)
101101
return parser;
102102
}
103103

104-
if (ReflectionHelper.IsNullableType(type, out var wrappedType) && wrappedType != null)
104+
if (ReflectionHelper.IsNullableType(type, out var wrappedType))
105105
{
106106
if (wrappedType.IsEnum)
107107
{
@@ -114,15 +114,9 @@ public IValueParser GetParser(Type type)
114114
}
115115
}
116116

117-
if (!type.IsGenericType)
117+
if (ReflectionHelper.IsSpecialValueTupleType(type, out var wrappedType2))
118118
{
119-
return null;
120-
}
121-
122-
var typeDef = type.GetGenericTypeDefinition();
123-
if (typeDef == typeof(ValueTuple<,>) && type.GenericTypeArguments[0] == typeof(bool))
124-
{
125-
var innerParser = GetParser(type.GenericTypeArguments[1]);
119+
var innerParser = GetParser(wrappedType2);
126120
if (innerParser == null)
127121
{
128122
return null;

src/CommandLineUtils/CommandArgument.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ public CommandArgument()
5454
/// </summary>
5555
public string? Value => Values.FirstOrDefault();
5656

57+
/// <summary>
58+
/// The default value of the argument.
59+
/// </summary>
60+
public string? DefaultValue { get; set; }
61+
5762
/// <summary>
5863
/// A collection of validators that execute before invoking <see cref="CommandLineApplication.OnExecute(Func{int})"/>.
5964
/// When validation fails, <see cref="CommandLineApplication.ValidationErrorHandler"/> is invoked.

src/CommandLineUtils/CommandArgument{T}.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class CommandArgument<T> : CommandArgument, IInternalCommandParamOfT
2020
{
2121
private readonly List<T> _parsedValues = new List<T>();
2222
private readonly IValueParser<T> _valueParser;
23+
private T _defaultValue;
2324

2425
/// <summary>
2526
/// Initializes a new instance of <see cref="CommandArgument{T}" />
@@ -29,6 +30,7 @@ public CommandArgument(IValueParser<T> valueParser)
2930
{
3031
_valueParser = valueParser ?? throw new ArgumentNullException(nameof(valueParser));
3132
UnderlyingType = typeof(T);
33+
SetBaseDefaultValue(default);
3234
}
3335

3436
/// <summary>
@@ -41,6 +43,19 @@ public CommandArgument(IValueParser<T> valueParser)
4143
/// </summary>
4244
public IReadOnlyList<T> ParsedValues => _parsedValues;
4345

46+
/// <summary>
47+
/// The default value of the argument.
48+
/// </summary>
49+
public new T DefaultValue
50+
{
51+
get => _defaultValue;
52+
set
53+
{
54+
_defaultValue = value;
55+
SetBaseDefaultValue(value);
56+
}
57+
}
58+
4459
void IInternalCommandParamOfT.Parse(CultureInfo culture)
4560
{
4661
_parsedValues.Clear();
@@ -49,5 +64,20 @@ void IInternalCommandParamOfT.Parse(CultureInfo culture)
4964
_parsedValues.Add(_valueParser.Parse(Name, t, culture));
5065
}
5166
}
67+
68+
void SetBaseDefaultValue(T value)
69+
{
70+
if (!ReflectionHelper.IsSpecialValueTupleType(typeof(T), out _))
71+
{
72+
if (MultipleValues && value is IEnumerable<object> enumerable)
73+
{
74+
base.DefaultValue = string.Join(", ", enumerable.Select(x => x?.ToString()));
75+
}
76+
else
77+
{
78+
base.DefaultValue = value?.ToString();
79+
}
80+
}
81+
}
5282
}
5383
}

src/CommandLineUtils/CommandOption.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ internal CommandOption(CommandOptionType type)
101101
/// </summary>
102102
public List<string?> Values { get; } = new List<string?>();
103103

104+
/// <summary>
105+
/// The default value of the option.
106+
/// </summary>
107+
public string? DefaultValue { get; set; }
108+
104109
/// <summary>
105110
/// Defines the type of the option.
106111
/// </summary>

src/CommandLineUtils/CommandOption{T}.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Globalization;
77
using System.Linq;
8+
using System.Reflection;
89
using McMaster.Extensions.CommandLineUtils.Abstractions;
910

1011
namespace McMaster.Extensions.CommandLineUtils
@@ -19,6 +20,7 @@ public class CommandOption<T> : CommandOption, IInternalCommandParamOfT
1920
{
2021
private readonly List<T> _parsedValues = new List<T>();
2122
private readonly IValueParser<T> _valueParser;
23+
private T _defaultValue;
2224

2325
/// <summary>
2426
/// Initializes a new instance of <see cref="CommandOption{T}" />
@@ -31,6 +33,7 @@ public CommandOption(IValueParser<T> valueParser, string template, CommandOption
3133
{
3234
_valueParser = valueParser ?? throw new ArgumentNullException(nameof(valueParser));
3335
UnderlyingType = typeof(T);
36+
SetBaseDefaultValue(default);
3437
}
3538

3639
/// <summary>
@@ -43,6 +46,19 @@ public CommandOption(IValueParser<T> valueParser, string template, CommandOption
4346
/// </summary>
4447
public IReadOnlyList<T> ParsedValues => _parsedValues;
4548

49+
/// <summary>
50+
/// The default value of the option.
51+
/// </summary>
52+
public new T DefaultValue
53+
{
54+
get => _defaultValue;
55+
set
56+
{
57+
_defaultValue = value;
58+
SetBaseDefaultValue(value);
59+
}
60+
}
61+
4662
void IInternalCommandParamOfT.Parse(CultureInfo culture)
4763
{
4864
_parsedValues.Clear();
@@ -51,5 +67,20 @@ void IInternalCommandParamOfT.Parse(CultureInfo culture)
5167
_parsedValues.Add(_valueParser.Parse(LongName ?? ShortName ?? SymbolName, t, culture));
5268
}
5369
}
70+
71+
void SetBaseDefaultValue(T value)
72+
{
73+
if (!ReflectionHelper.IsSpecialValueTupleType(typeof(T), out _))
74+
{
75+
if (OptionType == CommandOptionType.MultipleValue && value is IEnumerable<object> enumerable)
76+
{
77+
base.DefaultValue = string.Join(", ", enumerable.Select(x => x?.ToString()));
78+
}
79+
else
80+
{
81+
base.DefaultValue = value?.ToString();
82+
}
83+
}
84+
}
5485
}
5586
}

src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ private void AddArgument(PropertyInfo prop,
9595
argPropOrder.Add(argumentAttr.Order, prop);
9696
argOrder.Add(argumentAttr.Order, argument);
9797

98+
var getter = ReflectionHelper.GetPropertyGetter(prop);
9899
var setter = ReflectionHelper.GetPropertySetter(prop);
99100

100101
if (argument.MultipleValues)
@@ -109,14 +110,23 @@ private void AddArgument(PropertyInfo prop,
109110

110111
convention.Application.OnParsingComplete(r =>
111112
{
112-
if (argument.Values.Count == 0)
113-
{
114-
return;
115-
}
116-
117113
if (r.SelectedCommand is IModelAccessor cmd)
118114
{
119-
setter.Invoke(cmd.GetModel(), collectionParser.Parse(argument.Name, argument.Values));
115+
if (argument.Values.Count == 0)
116+
{
117+
if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out _))
118+
{
119+
if (getter.Invoke(cmd.GetModel()) is IEnumerable<object> value)
120+
{
121+
argument.Values.AddRange(value.Select(x => x?.ToString()));
122+
argument.DefaultValue = string.Join(", ", value.Select(x => x?.ToString()));
123+
}
124+
}
125+
}
126+
else
127+
{
128+
setter.Invoke(cmd.GetModel(), collectionParser.Parse(argument.Name, argument.Values));
129+
}
120130
}
121131
});
122132
}
@@ -130,19 +140,29 @@ private void AddArgument(PropertyInfo prop,
130140

131141
convention.Application.OnParsingComplete(r =>
132142
{
133-
if (argument.Values.Count == 0)
134-
{
135-
return;
136-
}
137-
138143
if (r.SelectedCommand is IModelAccessor cmd)
139144
{
140-
setter.Invoke(
141-
cmd.GetModel(),
142-
parser.Parse(
143-
argument.Name,
144-
argument.Value,
145-
convention.Application.ValueParsers.ParseCulture));
145+
if (argument.Values.Count == 0)
146+
{
147+
if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out _))
148+
{
149+
var value = getter.Invoke(cmd.GetModel());
150+
if (value != null)
151+
{
152+
argument.Values.Add(value.ToString());
153+
argument.DefaultValue = value.ToString();
154+
}
155+
}
156+
}
157+
else
158+
{
159+
setter.Invoke(
160+
cmd.GetModel(),
161+
parser.Parse(
162+
argument.Name,
163+
argument.Value,
164+
convention.Application.ValueParsers.ParseCulture));
165+
}
146166
}
147167
});
148168
}

src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections;
6+
using System.Collections.Generic;
57
using System.ComponentModel.DataAnnotations;
68
using System.Linq;
79
using System.Reflection;
@@ -57,6 +59,7 @@ private protected void AddOption(ConventionContext context, CommandOption option
5759
context.Application._longOptions.Add(option.LongName, prop);
5860
}
5961

62+
var getter = ReflectionHelper.GetPropertyGetter(prop);
6063
var setter = ReflectionHelper.GetPropertySetter(prop);
6164

6265
switch (option.OptionType)
@@ -71,9 +74,19 @@ private protected void AddOption(ConventionContext context, CommandOption option
7174
{
7275
if (!option.HasValue())
7376
{
74-
return;
77+
if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out var type))
78+
{
79+
if (getter.Invoke(modelAccessor.GetModel()) is IEnumerable<object> value)
80+
{
81+
option.Values.AddRange(value.Select(x => x?.ToString()));
82+
option.DefaultValue = string.Join(", ", value.Select(x => x?.ToString()));
83+
}
84+
}
85+
}
86+
else
87+
{
88+
setter.Invoke(modelAccessor.GetModel(), collectionParser.Parse(option.LongName, option.Values));
7589
}
76-
setter.Invoke(modelAccessor.GetModel(), collectionParser.Parse(option.LongName, option.Values));
7790
});
7891
break;
7992
case CommandOptionType.SingleOrNoValue:
@@ -87,9 +100,21 @@ private protected void AddOption(ConventionContext context, CommandOption option
87100
{
88101
if (!option.HasValue())
89102
{
90-
return;
103+
if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out var type))
104+
{
105+
var value = getter.Invoke(modelAccessor.GetModel());
106+
107+
if (value != null)
108+
{
109+
option.Values.Add(value.ToString());
110+
option.DefaultValue = value.ToString();
111+
}
112+
}
113+
}
114+
else
115+
{
116+
setter.Invoke(modelAccessor.GetModel(), parser.Parse(option.LongName, option.Value(), context.Application.ValueParsers.ParseCulture));
91117
}
92-
setter.Invoke(modelAccessor.GetModel(), parser.Parse(option.LongName, option.Value(), context.Application.ValueParsers.ParseCulture));
93118
});
94119
break;
95120
case CommandOptionType.NoValue:

src/CommandLineUtils/HelpText/DefaultHelpTextGenerator.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@ protected virtual void GenerateArguments(
214214
}
215215
}
216216

217+
if (!string.IsNullOrEmpty(arg.DefaultValue))
218+
{
219+
description += $"\nDefault value is: {arg.DefaultValue}.";
220+
}
221+
217222
var wrappedDescription = IndentWriter?.Write(description);
218223
var message = string.Format(outputFormat, arg.Name, wrappedDescription);
219224

@@ -266,6 +271,11 @@ protected virtual void GenerateOptions(
266271
}
267272
}
268273

274+
if (!string.IsNullOrEmpty(opt.DefaultValue))
275+
{
276+
description += $"\nDefault value is: {opt.DefaultValue}.";
277+
}
278+
269279
var wrappedDescription = IndentWriter?.Write(description);
270280
var message = string.Format(outputFormat, Format(opt), wrappedDescription);
271281

@@ -412,9 +422,9 @@ private string[] ExtractNamesFromEnum(Type type)
412422
return ExtractNamesFromEnum(wrappedType);
413423
}
414424

415-
if (ReflectionHelper.IsSpecialValueTupleType(type, out var fieldInfo))
425+
if (ReflectionHelper.IsSpecialValueTupleType(type, out var wrappedType2))
416426
{
417-
return ExtractNamesFromEnum(fieldInfo.FieldType);
427+
return ExtractNamesFromEnum(wrappedType2);
418428
}
419429

420430
if (type.IsEnum)

src/CommandLineUtils/Internal/CommandLineProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ private bool ProcessOption(OptionArgument arg)
216216
// If we find a help/version option, show information and stop parsing
217217
if (_currentCommand.OptionHelp == option)
218218
{
219-
_currentCommand.ShowHelp();
219+
_currentCommand.OnParsingComplete(_ => _currentCommand.ShowHelp());
220220
option.TryParse(null);
221221
return false;
222222
}

0 commit comments

Comments
 (0)