diff --git a/src/CommandLine/Core/InstanceChooser.cs b/src/CommandLine/Core/InstanceChooser.cs index 2b868f7c..7e292bfb 100644 --- a/src/CommandLine/Core/InstanceChooser.cs +++ b/src/CommandLine/Core/InstanceChooser.cs @@ -50,18 +50,18 @@ public static ParserResult Choose( { var verbs = Verb.SelectFromTypes(types); var defaultVerbs = verbs.Where(t => t.Item1.IsDefault); - + int defaultVerbCount = defaultVerbs.Count(); if (defaultVerbCount > 1) return MakeNotParsed(types, new MultipleDefaultVerbsError()); var defaultVerb = defaultVerbCount == 1 ? defaultVerbs.First() : null; - Func> choose = () => + ParserResult choose() { var firstArg = arguments.First(); - Func preprocCompare = command => + bool preprocCompare(string command) => nameComparer.Equals(command, firstArg) || nameComparer.Equals(string.Concat("--", command), firstArg); @@ -72,7 +72,7 @@ public static ParserResult Choose( : (autoVersion && preprocCompare("version")) ? MakeNotParsed(types, new VersionRequestedError()) : MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, allowMultiInstance, nonFatalErrors); - }; + } return arguments.Any() ? choose() @@ -120,21 +120,29 @@ private static ParserResult MatchVerb( bool allowMultiInstance, IEnumerable nonFatalErrors) { - return verbs.Any(a => nameComparer.Equals(a.Item1.Name, arguments.First())) - ? InstanceBuilder.Build( - Maybe.Just>( - () => - verbs.Single(v => nameComparer.Equals(v.Item1.Name, arguments.First())).Item2.AutoDefault()), - tokenizer, - arguments.Skip(1), - nameComparer, - ignoreValueCase, - parsingCulture, - autoHelp, - autoVersion, - allowMultiInstance, - nonFatalErrors) - : MatchDefaultVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors); + string firstArg = arguments.First(); + + var verbUsed = verbs.FirstOrDefault(vt => + nameComparer.Equals(vt.Item1.Name, firstArg) + || vt.Item1.Aliases.Any(alias => nameComparer.Equals(alias, firstArg)) + ); + + if (verbUsed == default) + { + return MatchDefaultVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors); + } + return InstanceBuilder.Build( + Maybe.Just>( + () => verbUsed.Item2.AutoDefault()), + tokenizer, + arguments.Skip(1), + nameComparer, + ignoreValueCase, + parsingCulture, + autoHelp, + autoVersion, + allowMultiInstance, + nonFatalErrors); } private static HelpVerbRequestedError MakeHelpVerbRequestedError( diff --git a/src/CommandLine/Core/Verb.cs b/src/CommandLine/Core/Verb.cs index 3a7f12a3..48e9427c 100644 --- a/src/CommandLine/Core/Verb.cs +++ b/src/CommandLine/Core/Verb.cs @@ -9,41 +9,27 @@ namespace CommandLine.Core { sealed class Verb { - private readonly string name; - private readonly string helpText; - private readonly bool hidden; - private readonly bool isDefault; - - public Verb(string name, string helpText, bool hidden = false, bool isDefault = false) + public Verb(string name, string helpText, bool hidden, bool isDefault, string[] aliases) { - if ( string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); - this.name = name; + Name = name; - this.helpText = helpText ?? throw new ArgumentNullException(nameof(helpText)); - this.hidden = hidden; - this.isDefault = isDefault; + HelpText = helpText ?? throw new ArgumentNullException(nameof(helpText)); + Hidden = hidden; + IsDefault = isDefault; + Aliases = aliases ?? new string[0]; } - public string Name - { - get { return name; } - } + public string Name { get; private set; } - public string HelpText - { - get { return helpText; } - } + public string HelpText { get; private set; } - public bool Hidden - { - get { return hidden; } - } + public bool Hidden { get; private set; } - public bool IsDefault - { - get => isDefault; - } + public bool IsDefault { get; private set; } + + public string[] Aliases { get; private set; } public static Verb FromAttribute(VerbAttribute attribute) { @@ -51,7 +37,8 @@ public static Verb FromAttribute(VerbAttribute attribute) attribute.Name, attribute.HelpText, attribute.Hidden, - attribute.IsDefault + attribute.IsDefault, + attribute.Aliases ); } diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index e9ce218d..3894bc9d 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -851,10 +851,10 @@ private IEnumerable AdaptVerbsToSpecifications(IEnumerable var optionSpecs = from verbTuple in Verb.SelectFromTypes(types) select OptionSpecification.NewSwitch( - string.Empty, verbTuple.Item1.Name, + verbTuple.Item1.Aliases.ToDelimitedString(", "), false, - verbTuple.Item1.IsDefault? "(Default Verb) "+verbTuple.Item1.HelpText: verbTuple.Item1.HelpText, //Default verb + verbTuple.Item1.IsDefault ? "(Default Verb) " + verbTuple.Item1.HelpText : verbTuple.Item1.HelpText, //Default verb string.Empty, verbTuple.Item1.Hidden); if (autoHelp) diff --git a/src/CommandLine/VerbAttribute.cs b/src/CommandLine/VerbAttribute.cs index 57318b3a..6ee6024d 100644 --- a/src/CommandLine/VerbAttribute.cs +++ b/src/CommandLine/VerbAttribute.cs @@ -1,6 +1,7 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. using System; +using System.Collections.Generic; namespace CommandLine { @@ -9,11 +10,9 @@ namespace CommandLine /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = true)] //public sealed class VerbAttribute : Attribute - public class VerbAttribute : Attribute + public class VerbAttribute : Attribute { - private readonly string name; - private readonly bool isDefault; - private Infrastructure.LocalizableAttributeProperty helpText; + private readonly Infrastructure.LocalizableAttributeProperty helpText; private Type resourceType; /// @@ -21,24 +20,23 @@ public class VerbAttribute : Attribute /// /// The long name of the verb command. /// Whether the verb is the default verb. + /// aliases for this verb. i.e. "move" and "mv" /// Thrown if is null, empty or whitespace and is false. - public VerbAttribute(string name, bool isDefault = false) + public VerbAttribute(string name, bool isDefault = false, string[] aliases = null) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name"); - this.name = name ; - this.isDefault = isDefault; + Name = name; + IsDefault = isDefault; helpText = new Infrastructure.LocalizableAttributeProperty(nameof(HelpText)); resourceType = null; + Aliases = aliases ?? new string[0]; } /// /// Gets the verb name. /// - public string Name - { - get { return name; } - } + public string Name { get; private set; } /// /// Gets or sets a value indicating whether a command line verb is visible in the help text. @@ -54,7 +52,7 @@ public bool Hidden /// public string HelpText { - get => helpText.Value??string.Empty; + get => helpText.Value ?? string.Empty; set => helpText.Value = value ?? throw new ArgumentNullException("value"); } /// @@ -63,15 +61,17 @@ public string HelpText public Type ResourceType { get => resourceType; - set => resourceType =helpText.ResourceType = value; + set => resourceType = helpText.ResourceType = value; } /// /// Gets whether this verb is the default verb. /// - public bool IsDefault - { - get => isDefault; - } + public bool IsDefault { get; private set; } + + /// + /// Gets or sets the aliases + /// + public string[] Aliases { get; private set; } } } diff --git a/tests/CommandLine.Tests/Unit/Issue591ests.cs b/tests/CommandLine.Tests/Unit/Issue591Tests.cs similarity index 96% rename from tests/CommandLine.Tests/Unit/Issue591ests.cs rename to tests/CommandLine.Tests/Unit/Issue591Tests.cs index 3888a705..41b66b74 100644 --- a/tests/CommandLine.Tests/Unit/Issue591ests.cs +++ b/tests/CommandLine.Tests/Unit/Issue591Tests.cs @@ -10,7 +10,7 @@ namespace CommandLine.Tests.Unit { - public class Issue591ests + public class Issue591Tests { [Fact] public void Parse_option_with_only_explicit_interface_implementation() diff --git a/tests/CommandLine.Tests/Unit/Issue6Tests.cs b/tests/CommandLine.Tests/Unit/Issue6Tests.cs new file mode 100644 index 00000000..224ba41a --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Issue6Tests.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using CommandLine.Tests.Fakes; +using CommandLine.Text; +using FluentAssertions; +using Microsoft.FSharp.Core; +using Xunit; +using Xunit.Abstractions; + +//Issue #6 +//Support Aliases on verbs (i.e. "move" and "mv" are the same verb). + +namespace CommandLine.Tests.Unit +{ + public class Issue6Tests + { + /// + /// Test Verb aliases when one verb is set as a default + /// + /// + /// + [Theory] + [InlineData("move -a bob", typeof(AliasedVerbOption1))] + [InlineData("mv -a bob", typeof(AliasedVerbOption1))] + [InlineData("copy -a bob", typeof(AliasedVerbOption2))] + [InlineData("cp -a bob", typeof(AliasedVerbOption2))] + [InlineData("-a bob", typeof(AliasedVerbOption2))] + public void Parse_option_with_aliased_verbs(string args, Type expectedArgType) + { + var arguments = args.Split(' '); + object options = null; + IEnumerable errors = null; + var result = Parser.Default.ParseArguments(arguments) + .WithParsed(o => options = o) + .WithNotParsed(o => errors = o) + ; + if (errors != null && errors.Any()) + { + foreach (Error e in errors) + { + System.Console.WriteLine(e.ToString()); + } + } + + Assert.NotNull(options); + Assert.Equal(expectedArgType, options.GetType()); + } + + /// + /// Test verb aliases with no default verb and 1 verb with no aliases + /// + /// + /// + [Theory] + [InlineData("move -a bob", typeof(AliasedVerbOption1))] + [InlineData("mv -a bob", typeof(AliasedVerbOption1))] + [InlineData("delete -b fred", typeof(VerbNoAlias))] + public void Parse_option_with_aliased_verb(string args, Type expectedArgType) + { + var arguments = args.Split(' '); + object options = null; + IEnumerable errors = null; + var result = Parser.Default.ParseArguments(arguments) + .WithParsed(o => options = o) + .WithNotParsed(o => errors = o) + ; + if (errors != null && errors.Any()) + { + foreach (Error e in errors) + { + System.Console.WriteLine(e.ToString()); + } + } + + Assert.NotNull(options); + Assert.Equal(expectedArgType, options.GetType()); + } + + /// + /// Verify auto-help generation. + /// + /// + /// + /// + [Theory] + [InlineData("--help", true, new string[] + { + "copy, cp, cpy (Default Verb) Copy some stuff", + "move, mv", + "delete Delete stuff", + "help Display more information on a specific command.", + "version Display version information.", + })] + [InlineData("help", true, new string[] + { + "copy, cp, cpy (Default Verb) Copy some stuff", + "move, mv", + "delete Delete stuff", + "help Display more information on a specific command.", + "version Display version information.", + })] + [InlineData("move --help", false, new string[] + { + "-a, --alpha Required.", + "--help Display this help screen.", + "--version Display version information.", + })] + [InlineData("mv --help", false, new string[] + { + "-a, --alpha Required.", + "--help Display this help screen.", + "--version Display version information.", + })] + [InlineData("delete --help", false, new string[] + { + "-b, --beta Required.", + "--help Display this help screen.", + "--version Display version information.", + })] + public void Parse_help_option_for_aliased_verbs(string args, bool verbsIndex, string[] expected) + { + var arguments = args.Split(' '); + object options = null; + IEnumerable errors = null; + // the order of the arguments here drives the order of the commands shown + // in the help message + var result = Parser.Default.ParseArguments< + AliasedVerbOption2, + AliasedVerbOption1, + VerbNoAlias + >(arguments) + .WithParsed(o => options = o) + .WithNotParsed(o => errors = o) + ; + + var message = HelpText.AutoBuild(result, + error => error, + ex => ex, + verbsIndex: verbsIndex + ); + + string helpMessage = message.ToString(); + var helps = helpMessage.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(2).ToList(); + + expected.Length.Should().Be(helps.Count); + int i = 0; + foreach (var expect in expected) + { + helps[i].Trim().Should().Be(expect); + i++; + } + } + + /// + /// Verify auto-help generation with no default verb. + /// + /// + /// + /// + [Theory] + [InlineData("--help", true, new string[] + { + "move, mv", + "delete Delete stuff", + "help Display more information on a specific command.", + "version Display version information.", + })] + [InlineData("help", true, new string[] + { + "move, mv", + "delete Delete stuff", + "help Display more information on a specific command.", + "version Display version information.", + })] + [InlineData("move --help", false, new string[] + { + "-a, --alpha Required.", + "--help Display this help screen.", + "--version Display version information.", + })] + [InlineData("mv --help", false, new string[] + { + "-a, --alpha Required.", + "--help Display this help screen.", + "--version Display version information.", + })] + [InlineData("delete --help", false, new string[] + { + "-b, --beta Required.", + "--help Display this help screen.", + "--version Display version information.", + })] + public void Parse_help_option_for_aliased_verbs_no_default(string args, bool verbsIndex, string[] expected) + { + var arguments = args.Split(' '); + object options = null; + IEnumerable errors = null; + // the order of the arguments here drives the order of the commands shown + // in the help message + var result = Parser.Default.ParseArguments< + AliasedVerbOption1, + VerbNoAlias + >(arguments) + .WithParsed(o => options = o) + .WithNotParsed(o => errors = o) + ; + + var message = HelpText.AutoBuild(result, + error => error, + ex => ex, + verbsIndex: verbsIndex + ); + + string helpMessage = message.ToString(); + var helps = helpMessage.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(2).ToList(); + + expected.Length.Should().Be(helps.Count); + int i = 0; + foreach (var expect in expected) + { + helps[i].Trim().Should().Be(expect); + i++; + } + } + + [Verb("move", + aliases: new string[] { "mv" } + )] + public class AliasedVerbOption1 + { + [Option('a', "alpha", Required = true)] + public string Option { get; set; } + } + + [Verb("copy", + isDefault: true, + aliases: new string[] { "cp", "cpy" }, + HelpText = "Copy some stuff" + )] + public class AliasedVerbOption2 + { + [Option('a', "alpha", Required = true)] + public string Option { get; set; } + } + + [Verb("delete", HelpText = "Delete stuff")] + public class VerbNoAlias + { + [Option('b', "beta", Required = true)] + public string Option { get; set; } + } + } +}