Skip to content

Add default verb support #556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions demo/ReadText.Demo/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface IOptions
string FileName { get; set; }
}

[Verb("head", HelpText = "Displays first lines of a file.")]
[Verb("head", true, HelpText = "Displays first lines of a file.")]
class HeadOptions : IOptions
{
public uint? Lines { get; set; }
Expand Down Expand Up @@ -62,4 +62,4 @@ class TailOptions : IOptions

public string FileName { get; set; }
}
}
}
46 changes: 41 additions & 5 deletions src/CommandLine/Core/InstanceChooser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ public static ParserResult<object> Choose(
bool autoVersion,
IEnumerable<ErrorType> nonFatalErrors)
{
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<ParserResult<object>> choose = () =>
{
var firstArg = arguments.First();
Expand All @@ -31,25 +40,52 @@ public static ParserResult<object> Choose(
nameComparer.Equals(command, firstArg) ||
nameComparer.Equals(string.Concat("--", command), firstArg);

var verbs = Verb.SelectFromTypes(types);

return (autoHelp && preprocCompare("help"))
? MakeNotParsed(types,
MakeHelpVerbRequestedError(verbs,
arguments.Skip(1).FirstOrDefault() ?? string.Empty, nameComparer))
: (autoVersion && preprocCompare("version"))
? MakeNotParsed(types, new VersionRequestedError())
: MatchVerb(tokenizer, verbs, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors);
: MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors);
};

return arguments.Any()
? choose()
: MakeNotParsed(types, new NoVerbSelectedError());
: (defaultVerbCount == 1
? MatchDefaultVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors)
: MakeNotParsed(types, new NoVerbSelectedError()));
}

private static ParserResult<object> MatchDefaultVerb(
Func<IEnumerable<string>, IEnumerable<OptionSpecification>, Result<IEnumerable<Token>, Error>> tokenizer,
IEnumerable<Tuple<Verb, Type>> verbs,
Tuple<Verb, Type> defaultVerb,
IEnumerable<string> arguments,
StringComparer nameComparer,
bool ignoreValueCase,
CultureInfo parsingCulture,
bool autoHelp,
bool autoVersion,
IEnumerable<ErrorType> nonFatalErrors)
{
return !(defaultVerb is null)
? InstanceBuilder.Build(
Maybe.Just<Func<object>>(() => defaultVerb.Item2.AutoDefault()),
tokenizer,
arguments,
nameComparer,
ignoreValueCase,
parsingCulture,
autoHelp,
autoVersion,
nonFatalErrors)
: MakeNotParsed(verbs.Select(v => v.Item2), new BadVerbSelectedError(arguments.First()));
}

private static ParserResult<object> MatchVerb(
Func<IEnumerable<string>, IEnumerable<OptionSpecification>, Result<IEnumerable<Token>, Error>> tokenizer,
IEnumerable<Tuple<Verb, Type>> verbs,
Tuple<Verb, Type> defaultVerb,
IEnumerable<string> arguments,
StringComparer nameComparer,
bool ignoreValueCase,
Expand All @@ -71,7 +107,7 @@ private static ParserResult<object> MatchVerb(
autoHelp,
autoVersion,
nonFatalErrors)
: MakeNotParsed(verbs.Select(v => v.Item2), new BadVerbSelectedError(arguments.First()));
: MatchDefaultVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors);
}

private static HelpVerbRequestedError MakeHelpVerbRequestedError(
Expand Down
19 changes: 15 additions & 4 deletions src/CommandLine/Core/Verb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ 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)
public Verb(string name, string helpText, bool hidden = false, bool isDefault = false)
{
this.name = name ?? throw new ArgumentNullException(nameof(name));
if (!isDefault && string.IsNullOrWhiteSpace(name))
throw new ArgumentNullException(nameof(name));
this.name = name ?? string.Empty;

this.helpText = helpText ?? throw new ArgumentNullException(nameof(helpText));
this.hidden = hidden;
this.isDefault = isDefault;
}

public string Name
Expand All @@ -35,12 +40,18 @@ public bool Hidden
get { return hidden; }
}

public bool IsDefault
{
get => isDefault;
}

public static Verb FromAttribute(VerbAttribute attribute)
{
return new Verb(
attribute.Name,
attribute.HelpText,
attribute.Hidden
attribute.Hidden,
attribute.IsDefault
);
}

Expand All @@ -54,4 +65,4 @@ select Tuple.Create(
type);
}
}
}
}
19 changes: 17 additions & 2 deletions src/CommandLine/Error.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@ public enum ErrorType
/// <summary>
/// Value of <see cref="CommandLine.MissingGroupOptionError"/> type.
/// </summary>
MissingGroupOptionError

MissingGroupOptionError,
/// <summary>
/// Value of <see cref="CommandLine.MultipleDefaultVerbsError"/> type.
/// </summary>
MultipleDefaultVerbsError
}

/// <summary>
Expand Down Expand Up @@ -556,4 +559,16 @@ public IEnumerable<NameInfo> Names
get { return names; }
}
}

/// <summary>
/// Models an error generated when multiple default verbs are defined.
/// </summary>
public sealed class MultipleDefaultVerbsError : Error
{
public const string ErrorMessage = "More than one default verb is not allowed.";

internal MultipleDefaultVerbsError()
: base(ErrorType.MultipleDefaultVerbsError)
{ }
}
}
2 changes: 2 additions & 0 deletions src/CommandLine/Text/SentenceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ public override Func<Error, string> FormatError
"' (",
string.Join(", ", missingGroupOptionError.Names.Select(n => n.NameText)),
") is required.");
case ErrorType.MultipleDefaultVerbsError:
return MultipleDefaultVerbsError.ErrorMessage;
}
throw new InvalidOperationException();
};
Expand Down
19 changes: 15 additions & 4 deletions src/CommandLine/VerbAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@ namespace CommandLine
public class VerbAttribute : Attribute
{
private readonly string name;
private readonly bool isDefault;
private Infrastructure.LocalizableAttributeProperty helpText;
private Type resourceType;

/// <summary>
/// Initializes a new instance of the <see cref="CommandLine.VerbAttribute"/> class.
/// </summary>
/// <param name="name">The long name of the verb command.</param>
/// <exception cref="System.ArgumentException">Thrown if <paramref name="name"/> is null, empty or whitespace.</exception>
public VerbAttribute(string name)
/// <param name="isDefault">Whether the verb is the default verb.</param>
/// <exception cref="System.ArgumentException">Thrown if <paramref name="name"/> is null, empty or whitespace and <paramref name="isDefault"/> is false.</exception>
public VerbAttribute(string name, bool isDefault = false)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name");
if (!isDefault && string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name");

this.name = name;
this.name = name ?? string.Empty;
this.isDefault = isDefault;
helpText = new Infrastructure.LocalizableAttributeProperty(nameof(HelpText));
resourceType = null;
}
Expand Down Expand Up @@ -62,5 +65,13 @@ public Type ResourceType
get => resourceType;
set => resourceType =helpText.ResourceType = value;
}

/// <summary>
/// Gets whether this verb is the default verb.
/// </summary>
public bool IsDefault
{
get => isDefault;
}
}
}
23 changes: 22 additions & 1 deletion tests/CommandLine.Tests/Fakes/Verb_Fakes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,25 @@ class Verb_With_Option_And_Value_Of_String_Type
[Value(0)]
public string PosValue { get; set; }
}
}

[Verb("default1", true)]
class Default_Verb_One
{
[Option('t', "test-one")]
public bool TestValueOne { get; set; }
}

[Verb("default2", true)]
class Default_Verb_Two
{
[Option('t', "test-two")]
public bool TestValueTwo { get; set; }
}

[Verb(null, true)]
class Default_Verb_With_Empty_Name
{
[Option('t', "test")]
public bool TestValue { get; set; }
}
}
45 changes: 45 additions & 0 deletions tests/CommandLine.Tests/Unit/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -825,5 +825,50 @@ public void Blank_lines_are_inserted_between_verbs()
// Teardown
}


[Fact]
public void Parse_default_verb_implicit()
{
var parser = Parser.Default;
parser.ParseArguments<Default_Verb_One>(new[] { "-t" })
.WithNotParsed(errors => throw new InvalidOperationException("Must be parsed."))
.WithParsed(args =>
{
Assert.True(args.TestValueOne);
});
}

[Fact]
public void Parse_default_verb_explicit()
{
var parser = Parser.Default;
parser.ParseArguments<Default_Verb_One>(new[] { "default1", "-t" })
.WithNotParsed(errors => throw new InvalidOperationException("Must be parsed."))
.WithParsed(args =>
{
Assert.True(args.TestValueOne);
});
}

[Fact]
public void Parse_multiple_default_verbs()
{
var parser = Parser.Default;
parser.ParseArguments<Default_Verb_One, Default_Verb_Two>(new string[] { })
.WithNotParsed(errors => Assert.IsType<MultipleDefaultVerbsError>(errors.First()))
.WithParsed(args => throw new InvalidOperationException("Should not be parsed."));
}

[Fact]
public void Parse_default_verb_with_empty_name()
{
var parser = Parser.Default;
parser.ParseArguments<Default_Verb_With_Empty_Name>(new[] { "-t" })
.WithNotParsed(errors => throw new InvalidOperationException("Must be parsed."))
.WithParsed(args =>
{
Assert.True(args.TestValue);
});
}
}
}