Skip to content

Commit bfc2ca8

Browse files
committed
Merge branch 'develop'
2 parents dffb751 + af75319 commit bfc2ca8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1919
-215
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ artifacts/*
3737
*.DotSettings.user
3838
# Visual Studio 2015 cache/options directory
3939
.vs/
40+
# Rider
4041
.idea/
4142

4243
[R|r]elease/**
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information.
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using CommandLine.Infrastructure;
7+
using CSharpx;
8+
using RailwaySharp.ErrorHandling;
9+
using System.Text.RegularExpressions;
10+
11+
namespace CommandLine.Core
12+
{
13+
static class GetoptTokenizer
14+
{
15+
public static Result<IEnumerable<Token>, Error> Tokenize(
16+
IEnumerable<string> arguments,
17+
Func<string, NameLookupResult> nameLookup)
18+
{
19+
return GetoptTokenizer.Tokenize(arguments, nameLookup, ignoreUnknownArguments:false, allowDashDash:true, posixlyCorrect:false);
20+
}
21+
22+
public static Result<IEnumerable<Token>, Error> Tokenize(
23+
IEnumerable<string> arguments,
24+
Func<string, NameLookupResult> nameLookup,
25+
bool ignoreUnknownArguments,
26+
bool allowDashDash,
27+
bool posixlyCorrect)
28+
{
29+
var errors = new List<Error>();
30+
Action<string> onBadFormatToken = arg => errors.Add(new BadFormatTokenError(arg));
31+
Action<string> unknownOptionError = name => errors.Add(new UnknownOptionError(name));
32+
Action<string> doNothing = name => {};
33+
Action<string> onUnknownOption = ignoreUnknownArguments ? doNothing : unknownOptionError;
34+
35+
int consumeNext = 0;
36+
Action<int> onConsumeNext = (n => consumeNext = consumeNext + n);
37+
bool forceValues = false;
38+
39+
var tokens = new List<Token>();
40+
41+
var enumerator = arguments.GetEnumerator();
42+
while (enumerator.MoveNext())
43+
{
44+
switch (enumerator.Current) {
45+
case null:
46+
break;
47+
48+
case string arg when forceValues:
49+
tokens.Add(Token.ValueForced(arg));
50+
break;
51+
52+
case string arg when consumeNext > 0:
53+
tokens.Add(Token.Value(arg));
54+
consumeNext = consumeNext - 1;
55+
break;
56+
57+
case "--" when allowDashDash:
58+
forceValues = true;
59+
break;
60+
61+
case "--":
62+
tokens.Add(Token.Value("--"));
63+
if (posixlyCorrect) forceValues = true;
64+
break;
65+
66+
case "-":
67+
// A single hyphen is always a value (it usually means "read from stdin" or "write to stdout")
68+
tokens.Add(Token.Value("-"));
69+
if (posixlyCorrect) forceValues = true;
70+
break;
71+
72+
case string arg when arg.StartsWith("--"):
73+
tokens.AddRange(TokenizeLongName(arg, nameLookup, onBadFormatToken, onUnknownOption, onConsumeNext));
74+
break;
75+
76+
case string arg when arg.StartsWith("-"):
77+
tokens.AddRange(TokenizeShortName(arg, nameLookup, onUnknownOption, onConsumeNext));
78+
break;
79+
80+
case string arg:
81+
// If we get this far, it's a plain value
82+
tokens.Add(Token.Value(arg));
83+
if (posixlyCorrect) forceValues = true;
84+
break;
85+
}
86+
}
87+
88+
return Result.Succeed<IEnumerable<Token>, Error>(tokens.AsEnumerable(), errors.AsEnumerable());
89+
}
90+
91+
public static Result<IEnumerable<Token>, Error> ExplodeOptionList(
92+
Result<IEnumerable<Token>, Error> tokenizerResult,
93+
Func<string, Maybe<char>> optionSequenceWithSeparatorLookup)
94+
{
95+
var tokens = tokenizerResult.SucceededWith().Memoize();
96+
97+
var exploded = new List<Token>(tokens is ICollection<Token> coll ? coll.Count : tokens.Count());
98+
var nothing = Maybe.Nothing<char>(); // Re-use same Nothing instance for efficiency
99+
var separator = nothing;
100+
foreach (var token in tokens) {
101+
if (token.IsName()) {
102+
separator = optionSequenceWithSeparatorLookup(token.Text);
103+
exploded.Add(token);
104+
} else {
105+
// Forced values are never considered option values, so they should not be split
106+
if (separator.MatchJust(out char sep) && sep != '\0' && !token.IsValueForced()) {
107+
if (token.Text.Contains(sep)) {
108+
exploded.AddRange(token.Text.Split(sep).Select(Token.ValueFromSeparator));
109+
} else {
110+
exploded.Add(token);
111+
}
112+
} else {
113+
exploded.Add(token);
114+
}
115+
separator = nothing; // Only first value after a separator can possibly be split
116+
}
117+
}
118+
return Result.Succeed(exploded as IEnumerable<Token>, tokenizerResult.SuccessMessages());
119+
}
120+
121+
public static Func<
122+
IEnumerable<string>,
123+
IEnumerable<OptionSpecification>,
124+
Result<IEnumerable<Token>, Error>>
125+
ConfigureTokenizer(
126+
StringComparer nameComparer,
127+
bool ignoreUnknownArguments,
128+
bool enableDashDash,
129+
bool posixlyCorrect)
130+
{
131+
return (arguments, optionSpecs) =>
132+
{
133+
var tokens = GetoptTokenizer.Tokenize(arguments, name => NameLookup.Contains(name, optionSpecs, nameComparer), ignoreUnknownArguments, enableDashDash, posixlyCorrect);
134+
var explodedTokens = GetoptTokenizer.ExplodeOptionList(tokens, name => NameLookup.HavingSeparator(name, optionSpecs, nameComparer));
135+
return explodedTokens;
136+
};
137+
}
138+
139+
private static IEnumerable<Token> TokenizeShortName(
140+
string arg,
141+
Func<string, NameLookupResult> nameLookup,
142+
Action<string> onUnknownOption,
143+
Action<int> onConsumeNext)
144+
{
145+
146+
// First option char that requires a value means we swallow the rest of the string as the value
147+
// But if there is no rest of the string, then instead we swallow the next argument
148+
string chars = arg.Substring(1);
149+
int len = chars.Length;
150+
if (len > 0 && Char.IsDigit(chars[0]))
151+
{
152+
// Assume it's a negative number
153+
yield return Token.Value(arg);
154+
yield break;
155+
}
156+
for (int i = 0; i < len; i++)
157+
{
158+
var s = new String(chars[i], 1);
159+
switch(nameLookup(s))
160+
{
161+
case NameLookupResult.OtherOptionFound:
162+
yield return Token.Name(s);
163+
164+
if (i+1 < len)
165+
{
166+
// Rest of this is the value (e.g. "-sfoo" where "-s" is a string-consuming arg)
167+
yield return Token.Value(chars.Substring(i+1));
168+
yield break;
169+
}
170+
else
171+
{
172+
// Value is in next param (e.g., "-s foo")
173+
onConsumeNext(1);
174+
}
175+
break;
176+
177+
case NameLookupResult.NoOptionFound:
178+
onUnknownOption(s);
179+
break;
180+
181+
default:
182+
yield return Token.Name(s);
183+
break;
184+
}
185+
}
186+
}
187+
188+
private static IEnumerable<Token> TokenizeLongName(
189+
string arg,
190+
Func<string, NameLookupResult> nameLookup,
191+
Action<string> onBadFormatToken,
192+
Action<string> onUnknownOption,
193+
Action<int> onConsumeNext)
194+
{
195+
string[] parts = arg.Substring(2).Split(new char[] { '=' }, 2);
196+
string name = parts[0];
197+
string value = (parts.Length > 1) ? parts[1] : null;
198+
// A parameter like "--stringvalue=" is acceptable, and makes stringvalue be the empty string
199+
if (String.IsNullOrWhiteSpace(name) || name.Contains(" "))
200+
{
201+
onBadFormatToken(arg);
202+
yield break;
203+
}
204+
switch(nameLookup(name))
205+
{
206+
case NameLookupResult.NoOptionFound:
207+
onUnknownOption(name);
208+
yield break;
209+
210+
case NameLookupResult.OtherOptionFound:
211+
yield return Token.Name(name);
212+
if (value == null) // NOT String.IsNullOrEmpty
213+
{
214+
onConsumeNext(1);
215+
}
216+
else
217+
{
218+
yield return Token.Value(value);
219+
}
220+
break;
221+
222+
default:
223+
yield return Token.Name(name);
224+
break;
225+
}
226+
}
227+
}
228+
}

src/CommandLine/Core/InstanceBuilder.cs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,30 @@ public static ParserResult<T> Build<T>(
2424
bool autoVersion,
2525
IEnumerable<ErrorType> nonFatalErrors)
2626
{
27+
return Build(
28+
factory,
29+
tokenizer,
30+
arguments,
31+
nameComparer,
32+
ignoreValueCase,
33+
parsingCulture,
34+
autoHelp,
35+
autoVersion,
36+
false,
37+
nonFatalErrors);
38+
}
39+
40+
public static ParserResult<T> Build<T>(
41+
Maybe<Func<T>> factory,
42+
Func<IEnumerable<string>, IEnumerable<OptionSpecification>, Result<IEnumerable<Token>, Error>> tokenizer,
43+
IEnumerable<string> arguments,
44+
StringComparer nameComparer,
45+
bool ignoreValueCase,
46+
CultureInfo parsingCulture,
47+
bool autoHelp,
48+
bool autoVersion,
49+
bool allowMultiInstance,
50+
IEnumerable<ErrorType> nonFatalErrors) {
2751
var typeInfo = factory.MapValueOrDefault(f => f().GetType(), typeof(T));
2852

2953
var specProps = typeInfo.GetSpecifications(pi => SpecificationProperty.Create(
@@ -64,14 +88,14 @@ public static ParserResult<T> Build<T>(
6488
OptionMapper.MapValues(
6589
(from pt in specProps where pt.Specification.IsOption() select pt),
6690
optionsPartition,
67-
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, parsingCulture, ignoreValueCase),
91+
(vals, type, isScalar, isFlag) => TypeConverter.ChangeType(vals, type, isScalar, isFlag, parsingCulture, ignoreValueCase),
6892
nameComparer);
6993

7094
var valueSpecPropsResult =
7195
ValueMapper.MapValues(
7296
(from pt in specProps where pt.Specification.IsValue() orderby ((ValueSpecification)pt.Specification).Index select pt),
7397
valuesPartition,
74-
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, parsingCulture, ignoreValueCase));
98+
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, false, parsingCulture, ignoreValueCase));
7599

76100
var missingValueErrors = from token in errorsPartition
77101
select
@@ -95,7 +119,7 @@ public static ParserResult<T> Build<T>(
95119
instance = BuildImmutable(typeInfo, factory, specProps, specPropsWithValue, setPropertyErrors);
96120
}
97121

98-
var validationErrors = specPropsWithValue.Validate(SpecificationPropertyRules.Lookup(tokens));
122+
var validationErrors = specPropsWithValue.Validate(SpecificationPropertyRules.Lookup(tokens, allowMultiInstance));
99123

100124
var allErrors =
101125
tokenizerResult.SuccessMessages()

src/CommandLine/Core/InstanceChooser.cs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,31 @@ public static ParserResult<object> Choose(
2222
bool autoHelp,
2323
bool autoVersion,
2424
IEnumerable<ErrorType> nonFatalErrors)
25+
{
26+
return Choose(
27+
tokenizer,
28+
types,
29+
arguments,
30+
nameComparer,
31+
ignoreValueCase,
32+
parsingCulture,
33+
autoHelp,
34+
autoVersion,
35+
false,
36+
nonFatalErrors);
37+
}
38+
39+
public static ParserResult<object> Choose(
40+
Func<IEnumerable<string>, IEnumerable<OptionSpecification>, Result<IEnumerable<Token>, Error>> tokenizer,
41+
IEnumerable<Type> types,
42+
IEnumerable<string> arguments,
43+
StringComparer nameComparer,
44+
bool ignoreValueCase,
45+
CultureInfo parsingCulture,
46+
bool autoHelp,
47+
bool autoVersion,
48+
bool allowMultiInstance,
49+
IEnumerable<ErrorType> nonFatalErrors)
2550
{
2651
var verbs = Verb.SelectFromTypes(types);
2752
var defaultVerbs = verbs.Where(t => t.Item1.IsDefault);
@@ -46,7 +71,7 @@ bool preprocCompare(string command) =>
4671
arguments.Skip(1).FirstOrDefault() ?? string.Empty, nameComparer))
4772
: (autoVersion && preprocCompare("version"))
4873
? MakeNotParsed(types, new VersionRequestedError())
49-
: MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors);
74+
: MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, allowMultiInstance, nonFatalErrors);
5075
}
5176

5277
return arguments.Any()
@@ -92,6 +117,7 @@ private static ParserResult<object> MatchVerb(
92117
CultureInfo parsingCulture,
93118
bool autoHelp,
94119
bool autoVersion,
120+
bool allowMultiInstance,
95121
IEnumerable<ErrorType> nonFatalErrors)
96122
{
97123
string firstArg = arguments.First();
@@ -114,7 +140,8 @@ private static ParserResult<object> MatchVerb(
114140
ignoreValueCase,
115141
parsingCulture,
116142
autoHelp,
117-
autoVersion,
143+
autoVersion,
144+
allowMultiInstance,
118145
nonFatalErrors);
119146
}
120147

src/CommandLine/Core/NameLookup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static NameLookupResult Contains(string name, IEnumerable<OptionSpecifica
2020
{
2121
var option = specifications.FirstOrDefault(a => name.MatchName(a.ShortName, a.LongName, comparer));
2222
if (option == null) return NameLookupResult.NoOptionFound;
23-
return option.ConversionType == typeof(bool)
23+
return option.ConversionType == typeof(bool) || (option.ConversionType == typeof(int) && option.FlagCounter)
2424
? NameLookupResult.BooleanOptionFound
2525
: NameLookupResult.OtherOptionFound;
2626
}

0 commit comments

Comments
 (0)