Skip to content

Add setting to interpret options starting with a single dash as long named options #767

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

Open
wants to merge 3 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
55 changes: 47 additions & 8 deletions src/CommandLine/Core/GetoptTokenizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,22 @@ public static Result<IEnumerable<Token>, Error> Tokenize(
IEnumerable<string> arguments,
Func<string, NameLookupResult> nameLookup)
{
return GetoptTokenizer.Tokenize(arguments, nameLookup, ignoreUnknownArguments:false, allowDashDash:true, posixlyCorrect:false);
return GetoptTokenizer.Tokenize(
arguments,
nameLookup,
ignoreUnknownArguments: false,
allowDashDash: true,
posixlyCorrect: false,
optionsParseMode: OptionsParseMode.Default);
}

public static Result<IEnumerable<Token>, Error> Tokenize(
IEnumerable<string> arguments,
Func<string, NameLookupResult> nameLookup,
bool ignoreUnknownArguments,
bool allowDashDash,
bool posixlyCorrect)
bool posixlyCorrect,
OptionsParseMode optionsParseMode)
{
var errors = new List<Error>();
Action<string> onBadFormatToken = arg => errors.Add(new BadFormatTokenError(arg));
Expand Down Expand Up @@ -70,11 +77,31 @@ public static Result<IEnumerable<Token>, Error> Tokenize(
break;

case string arg when arg.StartsWith("--"):

if (optionsParseMode == OptionsParseMode.SingleDashOnly)
{
onBadFormatToken(arg);
continue;
}

tokens.AddRange(TokenizeLongName(arg, nameLookup, onBadFormatToken, onUnknownOption, onConsumeNext));
break;

case string arg when arg.StartsWith("-"):
tokens.AddRange(TokenizeShortName(arg, nameLookup, onUnknownOption, onConsumeNext));
switch(optionsParseMode)
{
case OptionsParseMode.Default:
tokens.AddRange(TokenizeShortName(arg, nameLookup, onUnknownOption, onConsumeNext));
break;

case OptionsParseMode.SingleOrDoubleDash:
case OptionsParseMode.SingleDashOnly:
tokens.AddRange(TokenizeLongName(arg, nameLookup, onBadFormatToken, onUnknownOption, onConsumeNext, dashCount: 1));
break;

default:
throw new ArgumentOutOfRangeException(nameof(optionsParseMode), optionsParseMode, null);
}
break;

case string arg:
Expand Down Expand Up @@ -126,12 +153,23 @@ public static Func<
StringComparer nameComparer,
bool ignoreUnknownArguments,
bool enableDashDash,
bool posixlyCorrect)
bool posixlyCorrect,
OptionsParseMode optionsParseMode)
{
return (arguments, optionSpecs) =>
{
var tokens = GetoptTokenizer.Tokenize(arguments, name => NameLookup.Contains(name, optionSpecs, nameComparer), ignoreUnknownArguments, enableDashDash, posixlyCorrect);
var explodedTokens = GetoptTokenizer.ExplodeOptionList(tokens, name => NameLookup.HavingSeparator(name, optionSpecs, nameComparer));
var tokens = GetoptTokenizer.Tokenize(
arguments,
name => NameLookup.Contains(name, optionSpecs, nameComparer),
ignoreUnknownArguments,
enableDashDash,
posixlyCorrect,
optionsParseMode);

var explodedTokens = GetoptTokenizer.ExplodeOptionList(
tokens,
name => NameLookup.HavingSeparator(name, optionSpecs, nameComparer));

return explodedTokens;
};
}
Expand Down Expand Up @@ -190,9 +228,10 @@ private static IEnumerable<Token> TokenizeLongName(
Func<string, NameLookupResult> nameLookup,
Action<string> onBadFormatToken,
Action<string> onUnknownOption,
Action<int> onConsumeNext)
Action<int> onConsumeNext,
int dashCount = 2)
{
string[] parts = arg.Substring(2).Split(new char[] { '=' }, 2);
string[] parts = arg.Substring(dashCount).Split(new char[] { '=' }, 2);
string name = parts[0];
string value = (parts.Length > 1) ? parts[1] : null;
// A parameter like "--stringvalue=" is acceptable, and makes stringvalue be the empty string
Expand Down
5 changes: 4 additions & 1 deletion src/CommandLine/Core/InstanceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static ParserResult<T> Build<T>(
CultureInfo parsingCulture,
bool autoHelp,
bool autoVersion,
OptionsParseMode optionsParseMode,
IEnumerable<ErrorType> nonFatalErrors)
{
return Build(
Expand All @@ -34,6 +35,7 @@ public static ParserResult<T> Build<T>(
autoHelp,
autoVersion,
false,
optionsParseMode,
nonFatalErrors);
}

Expand All @@ -47,6 +49,7 @@ public static ParserResult<T> Build<T>(
bool autoHelp,
bool autoVersion,
bool allowMultiInstance,
OptionsParseMode optionsParseMode,
IEnumerable<ErrorType> nonFatalErrors) {
var typeInfo = factory.MapValueOrDefault(f => f().GetType(), typeof(T));

Expand Down Expand Up @@ -137,7 +140,7 @@ public static ParserResult<T> Build<T>(

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

Expand Down
20 changes: 14 additions & 6 deletions src/CommandLine/Core/InstanceChooser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public static ParserResult<object> Choose(
CultureInfo parsingCulture,
bool autoHelp,
bool autoVersion,
OptionsParseMode optionsParseMode,
IEnumerable<ErrorType> nonFatalErrors)
{
return Choose(
Expand All @@ -33,6 +34,7 @@ public static ParserResult<object> Choose(
autoHelp,
autoVersion,
false,
optionsParseMode,
nonFatalErrors);
}

Expand All @@ -46,6 +48,7 @@ public static ParserResult<object> Choose(
bool autoHelp,
bool autoVersion,
bool allowMultiInstance,
OptionsParseMode optionsParseMode,
IEnumerable<ErrorType> nonFatalErrors)
{
var verbs = Verb.SelectFromTypes(types);
Expand All @@ -62,22 +65,23 @@ ParserResult<object> choose()
var firstArg = arguments.First();

bool preprocCompare(string command) =>
nameComparer.Equals(command, firstArg) ||
nameComparer.Equals(string.Concat("--", command), firstArg);
nameComparer.Equals(command, firstArg)
|| optionsParseMode != OptionsParseMode.SingleDashOnly && nameComparer.Equals(string.Concat("--", command), firstArg)
|| optionsParseMode != OptionsParseMode.Default && nameComparer.Equals(string.Concat("-", command), firstArg);

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, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, allowMultiInstance, nonFatalErrors);
: MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, allowMultiInstance, optionsParseMode, 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, autoVersion, optionsParseMode, nonFatalErrors)
: MakeNotParsed(types, new NoVerbSelectedError()));
}

Expand All @@ -91,6 +95,7 @@ private static ParserResult<object> MatchDefaultVerb(
CultureInfo parsingCulture,
bool autoHelp,
bool autoVersion,
OptionsParseMode optionsParseMode,
IEnumerable<ErrorType> nonFatalErrors)
{
return !(defaultVerb is null)
Expand All @@ -103,6 +108,7 @@ private static ParserResult<object> MatchDefaultVerb(
parsingCulture,
autoHelp,
autoVersion,
optionsParseMode,
nonFatalErrors)
: MakeNotParsed(verbs.Select(v => v.Item2), new BadVerbSelectedError(arguments.First()));
}
Expand All @@ -118,6 +124,7 @@ private static ParserResult<object> MatchVerb(
bool autoHelp,
bool autoVersion,
bool allowMultiInstance,
OptionsParseMode optionsParseMode,
IEnumerable<ErrorType> nonFatalErrors)
{
string firstArg = arguments.First();
Expand All @@ -129,7 +136,7 @@ private static ParserResult<object> MatchVerb(

if (verbUsed == default)
{
return MatchDefaultVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors);
return MatchDefaultVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, optionsParseMode, nonFatalErrors);
}
return InstanceBuilder.Build(
Maybe.Just<Func<object>>(
Expand All @@ -141,7 +148,8 @@ private static ParserResult<object> MatchVerb(
parsingCulture,
autoHelp,
autoVersion,
allowMultiInstance,
allowMultiInstance,
optionsParseMode,
nonFatalErrors);
}

Expand Down
17 changes: 9 additions & 8 deletions src/CommandLine/Core/PreprocessorGuards.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,31 @@ namespace CommandLine.Core
static class PreprocessorGuards
{
public static IEnumerable<Func<IEnumerable<string>, IEnumerable<Error>>>
Lookup(StringComparer nameComparer, bool autoHelp, bool autoVersion)
Lookup(StringComparer nameComparer, bool autoHelp, bool autoVersion, OptionsParseMode optionsParseMode)
{
var list = new List<Func<IEnumerable<string>, IEnumerable<Error>>>();
if (autoHelp)
list.Add(HelpCommand(nameComparer));
list.Add(HelpCommand(nameComparer, optionsParseMode));
if (autoVersion)
list.Add(VersionCommand(nameComparer));
list.Add(VersionCommand(nameComparer, optionsParseMode));
return list;
}

public static Func<IEnumerable<string>, IEnumerable<Error>> HelpCommand(StringComparer nameComparer)
public static Func<IEnumerable<string>, IEnumerable<Error>> HelpCommand(StringComparer nameComparer, OptionsParseMode optionsParseMode)
{
return
arguments =>
nameComparer.Equals("--help", arguments.First())
optionsParseMode != OptionsParseMode.SingleDashOnly && nameComparer.Equals("--help", arguments.First())
|| optionsParseMode != OptionsParseMode.Default && nameComparer.Equals("-help", arguments.First())
? new Error[] { new HelpRequestedError() }
: Enumerable.Empty<Error>();
}

public static Func<IEnumerable<string>, IEnumerable<Error>> VersionCommand(StringComparer nameComparer)
public static Func<IEnumerable<string>, IEnumerable<Error>> VersionCommand(StringComparer nameComparer, OptionsParseMode optionsParseMode)
{
return
arguments =>
nameComparer.Equals("--version", arguments.First())
optionsParseMode != OptionsParseMode.SingleDashOnly && nameComparer.Equals("--version", arguments.First())
|| optionsParseMode != OptionsParseMode.Default && nameComparer.Equals("-version", arguments.First())
? new Error[] { new VersionRequestedError() }
: Enumerable.Empty<Error>();
}
Expand Down
54 changes: 38 additions & 16 deletions src/CommandLine/Core/Tokenizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,41 @@ public static Result<IEnumerable<Token>, Error> Tokenize(
public static Result<IEnumerable<Token>, Error> Tokenize(
IEnumerable<string> arguments,
Func<string, NameLookupResult> nameLookup,
Func<IEnumerable<Token>, IEnumerable<Token>> normalize)
Func<IEnumerable<Token>, IEnumerable<Token>> normalize,
OptionsParseMode optionsParseMode = OptionsParseMode.Default)
{
var tokens = new List<Token>();
var errors = new List<Error>();

Action<Error> onError = errors.Add;

var tokens = (from arg in arguments
from token in !arg.StartsWith("-", StringComparison.Ordinal)
? new[] { Token.Value(arg) }
: arg.StartsWith("--", StringComparison.Ordinal)
? TokenizeLongName(arg, onError)
: TokenizeShortName(arg, nameLookup)
select token)
.Memoize();
foreach (var argument in arguments)
{
if (!argument.StartsWith("-", StringComparison.Ordinal))
{
tokens.Add(Token.Value(argument));
}
else if (argument.StartsWith("--", StringComparison.Ordinal))
{
if (optionsParseMode == OptionsParseMode.SingleDashOnly)
{
onError(new BadFormatTokenError(argument));
continue;
}

tokens.AddRange(TokenizeLongName(argument, onError));
}
else if (optionsParseMode == OptionsParseMode.SingleOrDoubleDash || optionsParseMode == OptionsParseMode.SingleDashOnly)
{
tokens.AddRange(TokenizeLongName(argument, onError, dashCount: 1));
}
else
{
tokens.AddRange(TokenizeShortName(argument, nameLookup));
}
}

var normalized = normalize(tokens).Memoize();
var normalized = normalize(tokens.Memoize()).Memoize();

var unkTokens = (from t in normalized where t.IsName() && nameLookup(t.Text) == NameLookupResult.NoOptionFound select t).Memoize();

Expand Down Expand Up @@ -123,7 +143,8 @@ public static Func<
ConfigureTokenizer(
StringComparer nameComparer,
bool ignoreUnknownArguments,
bool enableDashDash)
bool enableDashDash,
OptionsParseMode optionsParseMode = OptionsParseMode.Default)
{
return (arguments, optionSpecs) =>
{
Expand All @@ -136,8 +157,8 @@ public static Func<
? Tokenizer.PreprocessDashDash(
arguments,
args =>
Tokenizer.Tokenize(args, name => NameLookup.Contains(name, optionSpecs, nameComparer), normalize))
: Tokenizer.Tokenize(arguments, name => NameLookup.Contains(name, optionSpecs, nameComparer), normalize);
Tokenizer.Tokenize(args, name => NameLookup.Contains(name, optionSpecs, nameComparer), normalize, optionsParseMode))
: Tokenizer.Tokenize(arguments, name => NameLookup.Contains(name, optionSpecs, nameComparer), normalize, optionsParseMode);
var explodedTokens = Tokenizer.ExplodeOptionList(tokens, name => NameLookup.HavingSeparator(name, optionSpecs, nameComparer));
return explodedTokens;
};
Expand Down Expand Up @@ -191,11 +212,12 @@ private static IEnumerable<Token> TokenizeShortName(

private static IEnumerable<Token> TokenizeLongName(
string value,
Action<Error> onError)
Action<Error> onError,
int dashCount = 2)
{
if (value.Length > 2 && value.StartsWith("--", StringComparison.Ordinal))
if (value.Length > dashCount && value.StartsWith(new string('-', dashCount), StringComparison.Ordinal))
{
var text = value.Substring(2);
var text = value.Substring(dashCount);
var equalIndex = text.IndexOf('=');
if (equalIndex <= 0)
{
Expand Down
27 changes: 27 additions & 0 deletions src/CommandLine/OptionsParseMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information.

namespace CommandLine
{
/// <summary>
/// Defines how commandline options are being parsed.
/// </summary>
public enum OptionsParseMode
{
/// <summary>
/// Options that start with a double dash must be defined using its full name. E.g. git rebase --interactive
/// Options that start with a single dash are interpreted as list of short named options. E.g. git clean -xdf
/// </summary>
Default,

/// <summary>
/// Options that start with a single or double dash are interpreted as short or full named option.
/// </summary>
SingleOrDoubleDash,

/// <summary>
/// Options that start with a single dash are interpreted as short or full named option.
/// Options that start with a double dash are considered an invalid input.
/// </summary>
SingleDashOnly
}
}
Loading