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

Add GetoptMode parser setting and implementation #684

Merged
merged 4 commits into from
Aug 25, 2020
Merged
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
198 changes: 198 additions & 0 deletions src/CommandLine/Core/GetoptTokenizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// 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;
using System.Linq;
using CommandLine.Infrastructure;
using CSharpx;
using RailwaySharp.ErrorHandling;
using System.Text.RegularExpressions;

namespace CommandLine.Core
{
static class GetoptTokenizer
{
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);
}

public static Result<IEnumerable<Token>, Error> Tokenize(
IEnumerable<string> arguments,
Func<string, NameLookupResult> nameLookup,
bool ignoreUnknownArguments,
bool allowDashDash,
bool posixlyCorrect)
{
var errors = new List<Error>();
Action<string> onBadFormatToken = arg => errors.Add(new BadFormatTokenError(arg));
Action<string> unknownOptionError = name => errors.Add(new UnknownOptionError(name));
Action<string> doNothing = name => {};
Action<string> onUnknownOption = ignoreUnknownArguments ? doNothing : unknownOptionError;

int consumeNext = 0;
Action<int> onConsumeNext = (n => consumeNext = consumeNext + n);
bool forceValues = false;

var tokens = new List<Token>();

var enumerator = arguments.GetEnumerator();
while (enumerator.MoveNext())
{
switch (enumerator.Current) {
case null:
break;

case string arg when forceValues:
tokens.Add(Token.ValueForced(arg));
break;

case string arg when consumeNext > 0:
tokens.Add(Token.Value(arg));
consumeNext = consumeNext - 1;
break;

case "--" when allowDashDash:
forceValues = true;
break;

case "--":
tokens.Add(Token.Value("--"));
if (posixlyCorrect) forceValues = true;
break;

case "-":
// A single hyphen is always a value (it usually means "read from stdin" or "write to stdout")
tokens.Add(Token.Value("-"));
if (posixlyCorrect) forceValues = true;
break;

case string arg when arg.StartsWith("--"):
tokens.AddRange(TokenizeLongName(arg, nameLookup, onBadFormatToken, onUnknownOption, onConsumeNext));
break;

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

case string arg:
// If we get this far, it's a plain value
tokens.Add(Token.Value(arg));
if (posixlyCorrect) forceValues = true;
break;
}
}

return Result.Succeed<IEnumerable<Token>, Error>(tokens.AsEnumerable(), errors.AsEnumerable());
}

public static Func<
IEnumerable<string>,
IEnumerable<OptionSpecification>,
Result<IEnumerable<Token>, Error>>
ConfigureTokenizer(
StringComparer nameComparer,
bool ignoreUnknownArguments,
bool enableDashDash,
bool posixlyCorrect)
{
return (arguments, optionSpecs) =>
{
var tokens = GetoptTokenizer.Tokenize(arguments, name => NameLookup.Contains(name, optionSpecs, nameComparer), ignoreUnknownArguments, enableDashDash, posixlyCorrect);
var explodedTokens = Tokenizer.ExplodeOptionList(tokens, name => NameLookup.HavingSeparator(name, optionSpecs, nameComparer));
return explodedTokens;
};
}

private static IEnumerable<Token> TokenizeShortName(
string arg,
Func<string, NameLookupResult> nameLookup,
Action<string> onUnknownOption,
Action<int> onConsumeNext)
{

// First option char that requires a value means we swallow the rest of the string as the value
// But if there is no rest of the string, then instead we swallow the next argument
string chars = arg.Substring(1);
int len = chars.Length;
if (len > 0 && Char.IsDigit(chars[0]))
{
// Assume it's a negative number
yield return Token.Value(arg);
yield break;
}
for (int i = 0; i < len; i++)
{
var s = new String(chars[i], 1);
switch(nameLookup(s))
{
case NameLookupResult.OtherOptionFound:
yield return Token.Name(s);

if (i+1 < len)
{
// Rest of this is the value (e.g. "-sfoo" where "-s" is a string-consuming arg)
yield return Token.Value(chars.Substring(i+1));
yield break;
}
else
{
// Value is in next param (e.g., "-s foo")
onConsumeNext(1);
}
break;

case NameLookupResult.NoOptionFound:
onUnknownOption(s);
break;

default:
yield return Token.Name(s);
break;
}
}
}

private static IEnumerable<Token> TokenizeLongName(
string arg,
Func<string, NameLookupResult> nameLookup,
Action<string> onBadFormatToken,
Action<string> onUnknownOption,
Action<int> onConsumeNext)
{
string[] parts = arg.Substring(2).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
if (String.IsNullOrWhiteSpace(name) || name.Contains(" "))
{
onBadFormatToken(arg);
yield break;
}
switch(nameLookup(name))
{
case NameLookupResult.NoOptionFound:
onUnknownOption(name);
yield break;

case NameLookupResult.OtherOptionFound:
yield return Token.Name(name);
if (value == null) // NOT String.IsNullOrEmpty
{
onConsumeNext(1);
}
else
{
yield return Token.Value(value);
}
break;

default:
yield return Token.Name(name);
break;
}
}
}
}
4 changes: 2 additions & 2 deletions src/CommandLine/Core/InstanceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,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));
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, false, parsingCulture, ignoreValueCase));

var missingValueErrors = from token in errorsPartition
select
Expand Down
2 changes: 1 addition & 1 deletion src/CommandLine/Core/NameLookup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ 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)
return option.ConversionType == typeof(bool) || (option.ConversionType == typeof(int) && option.FlagCounter)
? NameLookupResult.BooleanOptionFound
: NameLookupResult.OtherOptionFound;
}
Expand Down
8 changes: 5 additions & 3 deletions src/CommandLine/Core/OptionMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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
Expand All @@ -27,7 +27,7 @@ public static Result<
if (matched.IsJust())
{
var matches = matched.GetValueOrDefault(Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>());
var values = new HashSet<string>();
var values = new List<string>();
foreach (var kvp in matches)
{
foreach (var value in kvp.Value)
Expand All @@ -36,7 +36,9 @@ public static Result<
}
}

return converter(values, pt.Property.PropertyType, pt.Specification.TargetType != TargetType.Sequence)
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>>(
Expand Down
17 changes: 14 additions & 3 deletions src/CommandLine/Core/OptionSpecification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,20 @@ sealed class OptionSpecification : Specification
private readonly char separator;
private readonly string setName;
private readonly string group;
private readonly bool flagCounter;

public OptionSpecification(string shortName, string longName, bool required, string setName, Maybe<int> min, Maybe<int> max,
char separator, Maybe<object> defaultValue, string helpText, string metaValue, IEnumerable<string> enumValues,
Type conversionType, TargetType targetType, string group, bool hidden = false)
Type conversionType, TargetType targetType, string group, bool flagCounter = false, bool hidden = false)
: base(SpecificationType.Option,
required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, targetType, hidden)
required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, conversionType == typeof(int) && flagCounter ? TargetType.Switch : targetType, hidden)
{
this.shortName = shortName;
this.longName = longName;
this.separator = separator;
this.setName = setName;
this.group = group;
this.flagCounter = flagCounter;
}

public static OptionSpecification FromAttribute(OptionAttribute attribute, Type conversionType, IEnumerable<string> enumValues)
Expand All @@ -45,13 +47,14 @@ public static OptionSpecification FromAttribute(OptionAttribute attribute, Type
conversionType,
conversionType.ToTargetType(),
attribute.Group,
attribute.FlagCounter,
attribute.Hidden);
}

public static OptionSpecification NewSwitch(string shortName, string longName, bool required, string helpText, string metaValue, bool hidden = false)
{
return new OptionSpecification(shortName, longName, required, string.Empty, Maybe.Nothing<int>(), Maybe.Nothing<int>(),
'\0', Maybe.Nothing<object>(), helpText, metaValue, Enumerable.Empty<string>(), typeof(bool), TargetType.Switch, string.Empty, hidden);
'\0', Maybe.Nothing<object>(), helpText, metaValue, Enumerable.Empty<string>(), typeof(bool), TargetType.Switch, string.Empty, false, hidden);
}

public string ShortName
Expand All @@ -78,5 +81,13 @@ public string Group
{
get { return group; }
}

/// <summary>
/// Whether this is an int option that counts how many times a flag was set rather than taking a value on the command line
/// </summary>
public bool FlagCounter
{
get { return flagCounter; }
}
}
}
1 change: 1 addition & 0 deletions src/CommandLine/Core/SpecificationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static OptionSpecification WithLongName(this OptionSpecification specific
specification.ConversionType,
specification.TargetType,
specification.Group,
specification.FlagCounter,
specification.Hidden);
}

Expand Down
34 changes: 30 additions & 4 deletions src/CommandLine/Core/Token.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ public static Token Value(string text, bool explicitlyAssigned)

public static Token ValueForced(string text)
{
return new Value(text, false, true);
return new Value(text, false, true, false);
}

public static Token ValueFromSeparator(string text)
{
return new Value(text, false, false, true);
}

public TokenType Tag
Expand Down Expand Up @@ -86,29 +91,45 @@ class Value : Token, IEquatable<Value>
{
private readonly bool explicitlyAssigned;
private readonly bool forced;
private readonly bool fromSeparator;

public Value(string text)
: this(text, false, false)
: this(text, false, false, false)
{
}

public Value(string text, bool explicitlyAssigned)
: this(text, explicitlyAssigned, false)
: this(text, explicitlyAssigned, false, false)
{
}

public Value(string text, bool explicitlyAssigned, bool forced)
public Value(string text, bool explicitlyAssigned, bool forced, bool fromSeparator)
: base(TokenType.Value, text)
{
this.explicitlyAssigned = explicitlyAssigned;
this.forced = forced;
this.fromSeparator = fromSeparator;
}

/// <summary>
/// Whether this value came from a long option with "=" separating the name from the value
/// </summary>
public bool ExplicitlyAssigned
{
get { return explicitlyAssigned; }
}

/// <summary>
/// Whether this value came from a sequence specified with a separator (e.g., "--files a.txt,b.txt,c.txt")
/// </summary>
public bool FromSeparator
{
get { return fromSeparator; }
}

/// <summary>
/// Whether this value came from args after the -- separator (when EnableDashDash = true)
/// </summary>
public bool Forced
{
get { return forced; }
Expand Down Expand Up @@ -153,6 +174,11 @@ public static bool IsValue(this Token token)
return token.Tag == TokenType.Value;
}

public static bool IsValueFromSeparator(this Token token)
{
return token.IsValue() && ((Value)token).FromSeparator;
}

public static bool IsValueForced(this Token token)
{
return token.IsValue() && ((Value)token).Forced;
Expand Down
Loading