Skip to content
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

Fix #600 - better handling of --help and --version options #608

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 src/CommandLine/Core/ArgumentsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ namespace CommandLine.Core
static class ArgumentsExtensions
{
public static IEnumerable<Error> Preprocess(
this IEnumerable<string> arguments,
this IEnumerable<Token> arguments,
IEnumerable<
Func<IEnumerable<string>, IEnumerable<Error>>
Func<IEnumerable<Token>, IEnumerable<Error>>
> preprocessorLookup)
{
return preprocessorLookup.TryHead().MapValueOrDefault(
Expand Down
78 changes: 66 additions & 12 deletions src/CommandLine/Core/InstanceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,35 @@ public static ParserResult<T> Build<T>(
bool autoHelp,
bool autoVersion,
IEnumerable<ErrorType> nonFatalErrors)
{
return Build(
factory,
tokenizer,
arguments,
nameComparer,
ignoreValueCase,
parsingCulture,
autoHelp,
false,
autoVersion,
false,
false,
nonFatalErrors);
}

public static ParserResult<T> Build<T>(
Maybe<Func<T>> factory,
Func<IEnumerable<string>, IEnumerable<OptionSpecification>, Result<IEnumerable<Token>, Error>> tokenizer,
IEnumerable<string> arguments,
StringComparer nameComparer,
bool ignoreValueCase,
CultureInfo parsingCulture,
bool autoHelp,
bool autoHelpShortName,
bool autoVersion,
bool autoVersionShortName,
bool allowMultiInstance,
IEnumerable<ErrorType> nonFatalErrors)
{
var typeInfo = factory.MapValueOrDefault(f => f().GetType(), typeof(T));

Expand All @@ -32,7 +61,9 @@ public static ParserResult<T> Build<T>(

var specs = from pt in specProps select pt.Specification;

var optionSpecs = specs
var autoSpecs = AddAutoSpecs(specs, nameComparer, autoHelp, autoHelpShortName, autoVersion, autoVersionShortName);

var optionSpecs = autoSpecs
.ThrowingValidate(SpecificationGuards.Lookup)
.OfType<OptionSpecification>()
.Memoize();
Expand All @@ -47,12 +78,13 @@ public static ParserResult<T> Build<T>(
errs => new NotParsed<T>(makeDefault().GetType().ToTypeInfo(), errs);

var argumentsList = arguments.Memoize();
Func<ParserResult<T>> buildUp = () =>
{
var tokenizerResult = tokenizer(argumentsList, optionSpecs);

var tokens = tokenizerResult.SucceededWith().Memoize();
var tokenizerResult = tokenizer(argumentsList, optionSpecs);

var tokens = tokenizerResult.SucceededWith().Memoize();

Func<ParserResult<T>> buildUp = () =>
{
var partitions = TokenPartitioner.Partition(
tokens,
name => TypeLookup.FindTypeDescriptorAndSibling(name, optionSpecs, nameComparer));
Expand All @@ -64,14 +96,14 @@ public static ParserResult<T> Build<T>(
OptionMapper.MapValues(
(from pt in specProps where pt.Specification.IsOption() select pt),
optionsPartition,
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, parsingCulture, ignoreValueCase),
(vals, type, isScalar, isFlag) => TypeConverter.ChangeType(vals, type, isScalar, isFlag, parsingCulture, ignoreValueCase),
nameComparer);

var valueSpecPropsResult =
ValueMapper.MapValues(
(from pt in specProps where pt.Specification.IsValue() orderby ((ValueSpecification)pt.Specification).Index select pt),
valuesPartition,
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, parsingCulture, ignoreValueCase));
valuesPartition,
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, false, parsingCulture, ignoreValueCase));

var missingValueErrors = from token in errorsPartition
select
Expand All @@ -86,7 +118,7 @@ public static ParserResult<T> Build<T>(

//build the instance, determining if the type is mutable or not.
T instance;
if(typeInfo.IsMutable() == true)
if (typeInfo.IsMutable() == true)
{
instance = BuildMutable(factory, specPropsWithValue, setPropertyErrors);
}
Expand All @@ -95,7 +127,7 @@ public static ParserResult<T> Build<T>(
instance = BuildImmutable(typeInfo, factory, specProps, specPropsWithValue, setPropertyErrors);
}

var validationErrors = specPropsWithValue.Validate(SpecificationPropertyRules.Lookup(tokens));
var validationErrors = specPropsWithValue.Validate(SpecificationPropertyRules.Lookup(tokens, allowMultiInstance));

var allErrors =
tokenizerResult.SuccessMessages()
Expand All @@ -112,8 +144,8 @@ public static ParserResult<T> Build<T>(
};

var preprocessorErrors = (
argumentsList.Any()
? arguments.Preprocess(PreprocessorGuards.Lookup(nameComparer, autoHelp, autoVersion))
tokens.Any()
? tokens.Preprocess(PreprocessorGuards.Lookup(nameComparer, autoHelp, autoHelpShortName, autoVersion, autoVersionShortName))
: Enumerable.Empty<Error>()
).Memoize();

Expand All @@ -126,6 +158,28 @@ public static ParserResult<T> Build<T>(
return result;
}

private static IEnumerable<Specification> AddAutoSpecs(IEnumerable<Specification> specs, StringComparer nameComparer, bool autoHelp, bool autoHelpShortName, bool autoVersion, bool autoVersionShortName)
{
var optionSpecs = specs.OfType<OptionSpecification>().Memoize();
bool useHelpShortName = autoHelpShortName && !(optionSpecs.Any(spec => nameComparer.Equals(spec.ShortName, "h")));
bool useVersionShortName = autoVersionShortName && !(optionSpecs.Any(spec => nameComparer.Equals(spec.ShortName, "V"))); // Uppercase V
bool addAutoHelp = autoHelp && !(optionSpecs.Any(spec => nameComparer.Equals(spec.LongName, "help")));
bool addAutoVersion = autoVersion && !(optionSpecs.Any(spec => nameComparer.Equals(spec.LongName, "version")));

var autoSpecs = new List<OptionSpecification>(2);
if (addAutoHelp)
{
// TODO: Get help text for --help option from SentenceBuilder instead
autoSpecs.Add(OptionSpecification.NewSwitch(useHelpShortName ? "h" : String.Empty, "help", false, "Display this help screen.", String.Empty, false, false));
}
if (addAutoVersion)
{
// TODO: Get help text for --version option from SentenceBuilder instead
autoSpecs.Add(OptionSpecification.NewSwitch(useVersionShortName ? "V" : String.Empty, "version", false, "Display version information.", String.Empty, false, false));
}
return specs.Concat(autoSpecs);
}

private static T BuildMutable<T>(Maybe<Func<T>> factory, IEnumerable<SpecificationProperty> specPropsWithValue, List<Error> setPropertyErrors )
{
var mutable = factory.MapValueOrDefault(f => f(), () => Activator.CreateInstance<T>());
Expand Down
51 changes: 46 additions & 5 deletions src/CommandLine/Core/InstanceChooser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,35 @@ public static ParserResult<object> Choose(
bool autoHelp,
bool autoVersion,
IEnumerable<ErrorType> nonFatalErrors)
{
return Choose(
tokenizer,
types,
arguments,
nameComparer,
ignoreValueCase,
parsingCulture,
autoHelp,
false,
autoVersion,
false,
false,
nonFatalErrors);
}

public static ParserResult<object> Choose(
Func<IEnumerable<string>, IEnumerable<OptionSpecification>, Result<IEnumerable<Token>, Error>> tokenizer,
IEnumerable<Type> types,
IEnumerable<string> arguments,
StringComparer nameComparer,
bool ignoreValueCase,
CultureInfo parsingCulture,
bool autoHelp,
bool autoHelpShortName,
bool autoVersion,
bool autoVersionShortName,
bool allowMultiInstance,
IEnumerable<ErrorType> nonFatalErrors)
{
var verbs = Verb.SelectFromTypes(types);
var defaultVerbs = verbs.Where(t => t.Item1.IsDefault);
Expand All @@ -40,19 +69,19 @@ public static ParserResult<object> Choose(
nameComparer.Equals(command, firstArg) ||
nameComparer.Equals(string.Concat("--", command), firstArg);

return (autoHelp && preprocCompare("help"))
return (autoHelp && preprocCompare("help")) || (autoHelp && autoHelpShortName && nameComparer.Equals("-h", firstArg))
? MakeNotParsed(types,
MakeHelpVerbRequestedError(verbs,
arguments.Skip(1).FirstOrDefault() ?? string.Empty, nameComparer))
: (autoVersion && preprocCompare("version"))
: (autoVersion && preprocCompare("version")) || (autoVersion && autoVersionShortName && nameComparer.Equals("-V", firstArg))
? MakeNotParsed(types, new VersionRequestedError())
: MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors);
: MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoHelpShortName, autoVersion, autoVersionShortName, allowMultiInstance, nonFatalErrors);
};

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

Expand All @@ -65,7 +94,10 @@ private static ParserResult<object> MatchDefaultVerb(
bool ignoreValueCase,
CultureInfo parsingCulture,
bool autoHelp,
bool autoHelpShortName,
bool autoVersion,
bool autoVersionShortName,
bool allowMultiInstance,
IEnumerable<ErrorType> nonFatalErrors)
{
return !(defaultVerb is null)
Expand All @@ -77,7 +109,10 @@ private static ParserResult<object> MatchDefaultVerb(
ignoreValueCase,
parsingCulture,
autoHelp,
autoHelpShortName,
autoVersion,
autoVersionShortName,
allowMultiInstance,
nonFatalErrors)
: MakeNotParsed(verbs.Select(v => v.Item2), new BadVerbSelectedError(arguments.First()));
}
Expand All @@ -91,7 +126,10 @@ private static ParserResult<object> MatchVerb(
bool ignoreValueCase,
CultureInfo parsingCulture,
bool autoHelp,
bool autoHelpShortName,
bool autoVersion,
bool autoVersionShortName,
bool allowMultiInstance,
IEnumerable<ErrorType> nonFatalErrors)
{
return verbs.Any(a => nameComparer.Equals(a.Item1.Name, arguments.First()))
Expand All @@ -105,9 +143,12 @@ private static ParserResult<object> MatchVerb(
ignoreValueCase,
parsingCulture,
autoHelp,
autoHelpShortName,
autoVersion,
autoVersionShortName,
allowMultiInstance,
nonFatalErrors)
: MatchDefaultVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors);
: MatchDefaultVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoHelpShortName, autoVersion, autoVersionShortName, allowMultiInstance, nonFatalErrors);
}

private static HelpVerbRequestedError MakeHelpVerbRequestedError(
Expand Down
6 changes: 3 additions & 3 deletions src/CommandLine/Core/NameLookup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace CommandLine.Core
enum NameLookupResult
{
NoOptionFound,
BooleanOptionFound,
FlagOptionFound,
OtherOptionFound
}

Expand All @@ -20,8 +20,8 @@ public static NameLookupResult Contains(string name, IEnumerable<OptionSpecifica
{
var option = specifications.FirstOrDefault(a => name.MatchName(a.ShortName, a.LongName, comparer));
if (option == null) return NameLookupResult.NoOptionFound;
return option.ConversionType == typeof(bool)
? NameLookupResult.BooleanOptionFound
return option.ConversionType == typeof(bool) || option.FlagCounter
? NameLookupResult.FlagOptionFound
: NameLookupResult.OtherOptionFound;
}

Expand Down
38 changes: 23 additions & 15 deletions src/CommandLine/Core/OptionMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,41 @@ public static Result<
MapValues(
IEnumerable<SpecificationProperty> propertyTuples,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> options,
Func<IEnumerable<string>, Type, bool, Maybe<object>> converter,
Func<IEnumerable<string>, Type, bool, bool, Maybe<object>> converter,
StringComparer comparer)
{
var sequencesAndErrors = propertyTuples
.Select(
pt =>
{
var matched = options.FirstOrDefault(s =>
var matched = options.Where(s =>
s.Key.MatchName(((OptionSpecification)pt.Specification).ShortName, ((OptionSpecification)pt.Specification).LongName, comparer)).ToMaybe();
return matched.IsJust()
? (
from sequence in matched
from converted in
converter(
sequence.Value,
pt.Property.PropertyType,
pt.Specification.TargetType != TargetType.Sequence)
select Tuple.Create(
pt.WithValue(Maybe.Just(converted)), Maybe.Nothing<Error>())
)

if (matched.IsJust())
{
var matches = matched.GetValueOrDefault(Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>());
var values = new List<string>();
foreach (var kvp in matches)
{
foreach (var value in kvp.Value)
{
values.Add(value);
}
}

bool isFlag = pt.Specification.Tag == SpecificationType.Option && ((OptionSpecification)pt.Specification).FlagCounter;

return converter(values, isFlag ? typeof(bool) : pt.Property.PropertyType, pt.Specification.TargetType != TargetType.Sequence, isFlag)
.Select(value => Tuple.Create(pt.WithValue(Maybe.Just(value)), Maybe.Nothing<Error>()))
.GetValueOrDefault(
Tuple.Create<SpecificationProperty, Maybe<Error>>(
pt,
Maybe.Just<Error>(
new BadFormatConversionError(
((OptionSpecification)pt.Specification).FromOptionSpecification()))))
: Tuple.Create(pt, Maybe.Nothing<Error>());
((OptionSpecification)pt.Specification).FromOptionSpecification()))));
}

return Tuple.Create(pt, Maybe.Nothing<Error>());
}
).Memoize();
return Result.Succeed(
Expand Down
Loading