Skip to content

Commit

Permalink
Merge branch 'rmunn-feature/getopt-mode' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
moh-hassan committed Aug 25, 2020
2 parents 889ac3b + 34ab560 commit 9a54d78
Show file tree
Hide file tree
Showing 27 changed files with 1,209 additions and 51 deletions.
228 changes: 228 additions & 0 deletions src/CommandLine/Core/GetoptTokenizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// 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 Result<IEnumerable<Token>, Error> ExplodeOptionList(
Result<IEnumerable<Token>, Error> tokenizerResult,
Func<string, Maybe<char>> optionSequenceWithSeparatorLookup)
{
var tokens = tokenizerResult.SucceededWith().Memoize();

var exploded = new List<Token>(tokens is ICollection<Token> coll ? coll.Count : tokens.Count());
var nothing = Maybe.Nothing<char>(); // Re-use same Nothing instance for efficiency
var separator = nothing;
foreach (var token in tokens) {
if (token.IsName()) {
separator = optionSequenceWithSeparatorLookup(token.Text);
exploded.Add(token);
} else {
// Forced values are never considered option values, so they should not be split
if (separator.MatchJust(out char sep) && sep != '\0' && !token.IsValueForced()) {
if (token.Text.Contains(sep)) {
exploded.AddRange(token.Text.Split(sep).Select(Token.ValueFromSeparator));
} else {
exploded.Add(token);
}
} else {
exploded.Add(token);
}
separator = nothing; // Only first value after a separator can possibly be split
}
}
return Result.Succeed(exploded as IEnumerable<Token>, tokenizerResult.SuccessMessages());
}

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 = GetoptTokenizer.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
Loading

0 comments on commit 9a54d78

Please sign in to comment.