Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
ericnewton76 committed May 12, 2022
2 parents dffb751 + af75319 commit bfc2ca8
Show file tree
Hide file tree
Showing 46 changed files with 1,919 additions and 215 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ artifacts/*
*.DotSettings.user
# Visual Studio 2015 cache/options directory
.vs/
# Rider
.idea/

[R|r]elease/**
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;
}
}
}
}
30 changes: 27 additions & 3 deletions src/CommandLine/Core/InstanceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@ public static ParserResult<T> Build<T>(
bool autoVersion,
IEnumerable<ErrorType> nonFatalErrors)
{
return Build(
factory,
tokenizer,
arguments,
nameComparer,
ignoreValueCase,
parsingCulture,
autoHelp,
autoVersion,
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 autoVersion,
bool allowMultiInstance,
IEnumerable<ErrorType> nonFatalErrors) {
var typeInfo = factory.MapValueOrDefault(f => f().GetType(), typeof(T));

var specProps = typeInfo.GetSpecifications(pi => SpecificationProperty.Create(
Expand Down Expand Up @@ -64,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 All @@ -95,7 +119,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 Down
31 changes: 29 additions & 2 deletions src/CommandLine/Core/InstanceChooser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,31 @@ public static ParserResult<object> Choose(
bool autoHelp,
bool autoVersion,
IEnumerable<ErrorType> nonFatalErrors)
{
return Choose(
tokenizer,
types,
arguments,
nameComparer,
ignoreValueCase,
parsingCulture,
autoHelp,
autoVersion,
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 autoVersion,
bool allowMultiInstance,
IEnumerable<ErrorType> nonFatalErrors)
{
var verbs = Verb.SelectFromTypes(types);
var defaultVerbs = verbs.Where(t => t.Item1.IsDefault);
Expand All @@ -46,7 +71,7 @@ bool preprocCompare(string command) =>
arguments.Skip(1).FirstOrDefault() ?? string.Empty, nameComparer))
: (autoVersion && preprocCompare("version"))
? MakeNotParsed(types, new VersionRequestedError())
: MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors);
: MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, allowMultiInstance, nonFatalErrors);
}

return arguments.Any()
Expand Down Expand Up @@ -92,6 +117,7 @@ private static ParserResult<object> MatchVerb(
CultureInfo parsingCulture,
bool autoHelp,
bool autoVersion,
bool allowMultiInstance,
IEnumerable<ErrorType> nonFatalErrors)
{
string firstArg = arguments.First();
Expand All @@ -114,7 +140,8 @@ private static ParserResult<object> MatchVerb(
ignoreValueCase,
parsingCulture,
autoHelp,
autoVersion,
autoVersion,
allowMultiInstance,
nonFatalErrors);
}

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
Loading

0 comments on commit bfc2ca8

Please sign in to comment.