Skip to content

Commit

Permalink
Introduce mapped options as defined in natemcmaster#334
Browse files Browse the repository at this point in the history
  • Loading branch information
TheConstructor committed Apr 3, 2021
1 parent fbc8668 commit 912b155
Show file tree
Hide file tree
Showing 22 changed files with 1,139 additions and 53 deletions.
91 changes: 91 additions & 0 deletions src/CommandLineUtils/Attributes/MappedOptionAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Reflection;

namespace McMaster.Extensions.CommandLineUtils.Attributes
{
/// <summary>
/// Represents one or many command line option that is identified by flag proceeded by '-' or '--'.
/// Options are not positional. Compare to <see cref="ArgumentAttribute"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class MappedOptionAttribute : OptionAttributeBase
{
/// <summary>
/// Initializes a new <see cref="MappedOptionAttribute"/>.
/// </summary>
public MappedOptionAttribute(object constantValue)
{
ConstantValue = constantValue;
}

/// <summary>
/// Initializes a new <see cref="MappedOptionAttribute"/>.
/// </summary>
/// <param name="template">The string template. This is parsed into <see cref="CommandOption.ShortName"/> and <see cref="CommandOption.LongName"/>.</param>
/// <param name="constantValue">The value to assign the the option is given.</param>
public MappedOptionAttribute(string template, object constantValue)
{
Template = template;
ConstantValue = constantValue;
}

/// <summary>
/// Initializes a new <see cref="MappedOptionAttribute"/>.
/// </summary>
/// <param name="template">The template</param>
/// <param name="description">The option description</param>
/// <param name="constantValue">The value to assign the the option is given.</param>
public MappedOptionAttribute(string template, string? description, object constantValue)
{
Template = template;
Description = description;
ConstantValue = constantValue;
}

/// <summary>
/// Defines the type of the option. When not set, this will be inferred from the CLR type of the property.
/// </summary>
/// <seealso cref="CommandOption.OptionType"/>
public CommandOptionType? OptionType { get; set; }

/// <summary>
/// Defines the value assigned to the property when the option is given.
/// </summary>
/// <seealso cref="ConstantValueOption{T}.ConstantValue"/>
public object? ConstantValue { get; set; }

internal CommandOption Configure<T>(MappedOption<T> mappedOption, PropertyInfo prop)
{
T constantValue;
try
{
constantValue = (T)ConstantValue!;
}
catch (InvalidCastException e)
{
throw new InvalidOperationException(Strings.CannotDetermineOptionType(prop), e);
}
ConstantValueOption<T> option;
if (Template != null)
{
option = mappedOption.Add(Template, constantValue);
}
else
{
option = mappedOption.Add(constantValue);
var stringValue = constantValue?.ToString().ToKebabCase();
if (stringValue != null)
{
option.LongName = stringValue;
option.ShortName = stringValue.Substring(0, 1);
}
}

Configure(option);
return option;
}
}
}
5 changes: 4 additions & 1 deletion src/CommandLineUtils/Attributes/OptionAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ internal CommandOption Configure(CommandLineApplication app, PropertyInfo prop)
CommandOption option;
if (Template != null)
{
option = new CommandOption(Template, optionType);
option = new CommandOption(Template, optionType)
{
UnderlyingType = prop.PropertyType,
};
}
else
{
Expand Down
45 changes: 24 additions & 21 deletions src/CommandLineUtils/CommandLineApplication.Validation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,35 +76,38 @@ public Func<ValidationResult, int> ValidationErrorHandler
}
}

foreach (var option in GetOptions())
foreach (var option in GetAnyOptions())
{
var context = factory.Create(option);

string? name = null;
if (option.LongName != null)
if (option is IParseableOption parseableOption)
{
name = "--" + option.LongName;
}
string? name = null;
if (parseableOption.LongName != null)
{
name = "--" + parseableOption.LongName;
}

if (name == null && option.ShortName != null)
{
name = "-" + option.ShortName;
}
if (name == null && parseableOption.ShortName != null)
{
name = "-" + parseableOption.ShortName;
}

if (name == null && option.SymbolName != null)
{
name = "-" + option.SymbolName;
}
if (name == null && parseableOption.SymbolName != null)
{
name = "-" + parseableOption.SymbolName;
}

if (name == null && option.ValueName != null)
{
name = option.ValueName;
}
if (name == null && parseableOption.ValueName != null)
{
name = parseableOption.ValueName;
}

if (!string.IsNullOrEmpty(name))
{
context.DisplayName = name;
context.MemberName = name;
if (!string.IsNullOrEmpty(name))
{
context.DisplayName = name;
context.MemberName = name;
}
}

foreach (var validator in option.Validators)
Expand Down
53 changes: 46 additions & 7 deletions src/CommandLineUtils/CommandLineApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -57,7 +56,7 @@ static CommandLineApplication()
private readonly ConventionContext _conventionContext;
private readonly List<IConvention> _conventions = new();
private readonly List<CommandArgument> _arguments = new();
private readonly List<CommandOption> _options = new();
private readonly List<IOption> _options = new();
private readonly List<CommandLineApplication> _subcommands = new();
internal readonly List<string> _remainingArguments = new();

Expand Down Expand Up @@ -184,9 +183,9 @@ public string? Name
public string? ExtendedHelpText { get; set; }

/// <summary>
/// Available command-line options on this command. Use <see cref="GetOptions"/> to get all available options, which may include inherited options.
/// Available command-line options on this command. Use <see cref="GetAnyOptions"/> to get all available options, which may include inherited options.
/// </summary>
public IReadOnlyCollection<CommandOption> Options => _options;
public IReadOnlyCollection<IOption> Options => _options;

/// <summary>
/// Whether a Pager should be used to display help text.
Expand Down Expand Up @@ -401,10 +400,10 @@ public char[] OptionNameValueSeparators
public TextWriter Error { get; set; }

/// <summary>
/// Gets all command line options available to this command, including any inherited options.
/// Gets all command line options available to this command, including any inherited options and <see cref="MappedOption{T}"/>s.
/// </summary>
/// <returns>Command line options.</returns>
public IEnumerable<CommandOption> GetOptions()
public IEnumerable<IOption> GetAnyOptions()
{
var expr = Options.AsEnumerable();
var rootNode = this;
Expand All @@ -417,6 +416,15 @@ public IEnumerable<CommandOption> GetOptions()
return expr;
}

/// <summary>
/// Gets all command line options available to this command, including any inherited options.
/// </summary>
/// <returns>Command line options.</returns>
public IEnumerable<IParseableOption> GetOptions()
{
return GetAnyOptions().OfType<IParseableOption>();
}

/// <summary>
/// Add another name for the command.
/// <para>
Expand Down Expand Up @@ -572,6 +580,11 @@ public CommandOption Option(string template, string description, CommandOptionTy
/// </summary>
/// <param name="option"></param>
public void AddOption(CommandOption option)
{
AddOption((IOption)option);
}

internal void AddOption(IOption option)
{
_options.Add(option);
}
Expand Down Expand Up @@ -605,6 +618,24 @@ public CommandOption<T> Option<T>(string template, string description, CommandOp
return option;
}

/// <summary>
/// Add a new set of mapped options
/// </summary>
/// <param name="optionType"></param>
/// <param name="configuration"></param>
/// <param name="inherited"></param>
/// <typeparam name="T">The type of the values on the option</typeparam>
/// <returns>The option</returns>
public MappedOption<T> MappedOption<T>(CommandOptionType optionType, Action<MappedOption<T>> configuration, bool inherited)
{
var mappedOption = new MappedOption<T>(this, optionType)
{
Inherited = inherited
};
configuration(mappedOption);
return mappedOption;
}

#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
/// <summary>
/// Adds a command line argument
Expand Down Expand Up @@ -1181,10 +1212,18 @@ public ServiceProvider(CommandLineApplication parent)

// prefer this type before AdditionalServices because it is common for service containers to automatically
// create IEnumerable<T> to allow registration of multiple services
if (serviceType == typeof(IEnumerable<CommandOption>))
if (serviceType == typeof(IEnumerable<IOption>))
{
return _parent.GetAnyOptions();
}
if (serviceType == typeof(IEnumerable<IParseableOption>))
{
return _parent.GetOptions();
}
if (serviceType == typeof(IEnumerable<CommandOption>))
{
return _parent.GetAnyOptions().OfType<CommandOption>();
}

if (serviceType == typeof(IEnumerable<CommandArgument>))
{
Expand Down
4 changes: 2 additions & 2 deletions src/CommandLineUtils/CommandOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace McMaster.Extensions.CommandLineUtils
/// Represents one or many command line option that is identified by flag proceeded by '-' or '--'.
/// Options are not positional. Compare to <see cref="CommandArgument"/>.
/// </summary>
public class CommandOption
public class CommandOption : IParseableOption
{
private protected readonly List<string?> _values = new();

Expand Down Expand Up @@ -244,7 +244,7 @@ internal string ToTemplateString()
}


private bool IsEnglishLetter(char c)
private static bool IsEnglishLetter(char c)
{
return c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
}
Expand Down
85 changes: 85 additions & 0 deletions src/CommandLineUtils/ConstantValueOption{T}.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace McMaster.Extensions.CommandLineUtils
{
/// <summary>
/// An option that returns a constant <see cref="ParsedValue"/> when specified on the command-line
/// </summary>
/// <typeparam name="T">type of <see cref="ConstantValue"/></typeparam>
public class ConstantValueOption<T> : CommandOption, IOption<T>, IInternalCommandParamOfT
{
private readonly List<T> _parsedValues = new List<T>();
private readonly T _constantValue;
private bool _hasBeenParsed;

/// <summary>
/// Initializes a new <see cref="ConstantValueOption{T}"/>
/// </summary>
/// <param name="template">The option template.</param>
/// <param name="constantValue">The value to return is specified on the command line</param>
/// <see cref="MappedOption{T}"/>
public ConstantValueOption(string template, T constantValue) : base(template, CommandOptionType.NoValue)
{
_constantValue = constantValue;
UnderlyingType = typeof(T);
}

internal ConstantValueOption(T constantValue) : base(CommandOptionType.NoValue)
{
_constantValue = constantValue;
UnderlyingType = typeof(T);
}

/// <summary>
/// The value that is returned for any usage of this option
/// </summary>
public T ConstantValue => _constantValue;

/// <summary>
/// The parsed value.
/// </summary>
public T ParsedValue => ParsedValues.FirstOrDefault();

/// <summary>
/// All parsed values;
/// </summary>
public IReadOnlyList<T> ParsedValues
{
get
{
if (!_hasBeenParsed)
{
((IInternalCommandParamOfT)this).Parse(CultureInfo.CurrentCulture);
}

return _parsedValues;
}
}

/// <summary>
/// "Parse" the user-given values into the constant value
/// </summary>
void IInternalCommandParamOfT.Parse(CultureInfo culture)
{
_hasBeenParsed = true;
_parsedValues.Clear();
foreach (var t in base._values)
{
_parsedValues.Add(_constantValue);
}
}

/// <inheritdoc/>
public override void Reset()
{
_hasBeenParsed = false;
_parsedValues.Clear();
base.Reset();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ public static IConventionBuilder UseHelpOptionAttribute(this IConventionBuilder
/// <param name="builder">The builder.</param>
/// <returns>The builder.</returns>
public static IConventionBuilder UseOptionAttributes(this IConventionBuilder builder)
=> builder.AddConvention(new OptionAttributeConvention());
=> builder.AddConvention(new OptionAttributeConvention())
.AddConvention(new MappedOptionAttributeConvention());

/// <summary>
/// Applies settings from <see cref="ArgumentAttribute" /> on the model type.
Expand Down
Loading

0 comments on commit 912b155

Please sign in to comment.