Skip to content

Commit 9a54d78

Browse files
committed
Merge branch 'rmunn-feature/getopt-mode' into develop
2 parents 889ac3b + 34ab560 commit 9a54d78

27 files changed

+1209
-51
lines changed
+228
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

+2-2
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,14 @@ public static ParserResult<T> Build<T>(
8888
OptionMapper.MapValues(
8989
(from pt in specProps where pt.Specification.IsOption() select pt),
9090
optionsPartition,
91-
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, parsingCulture, ignoreValueCase),
91+
(vals, type, isScalar, isFlag) => TypeConverter.ChangeType(vals, type, isScalar, isFlag, parsingCulture, ignoreValueCase),
9292
nameComparer);
9393

9494
var valueSpecPropsResult =
9595
ValueMapper.MapValues(
9696
(from pt in specProps where pt.Specification.IsValue() orderby ((ValueSpecification)pt.Specification).Index select pt),
9797
valuesPartition,
98-
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, parsingCulture, ignoreValueCase));
98+
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, false, parsingCulture, ignoreValueCase));
9999

100100
var missingValueErrors = from token in errorsPartition
101101
select

src/CommandLine/Core/NameLookup.cs

+1-1
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
}

src/CommandLine/Core/OptionMapper.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public static Result<
1515
MapValues(
1616
IEnumerable<SpecificationProperty> propertyTuples,
1717
IEnumerable<KeyValuePair<string, IEnumerable<string>>> options,
18-
Func<IEnumerable<string>, Type, bool, Maybe<object>> converter,
18+
Func<IEnumerable<string>, Type, bool, bool, Maybe<object>> converter,
1919
StringComparer comparer)
2020
{
2121
var sequencesAndErrors = propertyTuples
@@ -27,7 +27,7 @@ public static Result<
2727
if (matched.IsJust())
2828
{
2929
var matches = matched.GetValueOrDefault(Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>());
30-
var values = new HashSet<string>();
30+
var values = new List<string>();
3131
foreach (var kvp in matches)
3232
{
3333
foreach (var value in kvp.Value)
@@ -36,7 +36,9 @@ public static Result<
3636
}
3737
}
3838

39-
return converter(values, pt.Property.PropertyType, pt.Specification.TargetType != TargetType.Sequence)
39+
bool isFlag = pt.Specification.Tag == SpecificationType.Option && ((OptionSpecification)pt.Specification).FlagCounter;
40+
41+
return converter(values, isFlag ? typeof(bool) : pt.Property.PropertyType, pt.Specification.TargetType != TargetType.Sequence, isFlag)
4042
.Select(value => Tuple.Create(pt.WithValue(Maybe.Just(value)), Maybe.Nothing<Error>()))
4143
.GetValueOrDefault(
4244
Tuple.Create<SpecificationProperty, Maybe<Error>>(

src/CommandLine/Core/OptionSpecification.cs

+14-3
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,20 @@ sealed class OptionSpecification : Specification
1414
private readonly char separator;
1515
private readonly string setName;
1616
private readonly string group;
17+
private readonly bool flagCounter;
1718

1819
public OptionSpecification(string shortName, string longName, bool required, string setName, Maybe<int> min, Maybe<int> max,
1920
char separator, Maybe<object> defaultValue, string helpText, string metaValue, IEnumerable<string> enumValues,
20-
Type conversionType, TargetType targetType, string group, bool hidden = false)
21+
Type conversionType, TargetType targetType, string group, bool flagCounter = false, bool hidden = false)
2122
: base(SpecificationType.Option,
22-
required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, targetType, hidden)
23+
required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, conversionType == typeof(int) && flagCounter ? TargetType.Switch : targetType, hidden)
2324
{
2425
this.shortName = shortName;
2526
this.longName = longName;
2627
this.separator = separator;
2728
this.setName = setName;
2829
this.group = group;
30+
this.flagCounter = flagCounter;
2931
}
3032

3133
public static OptionSpecification FromAttribute(OptionAttribute attribute, Type conversionType, IEnumerable<string> enumValues)
@@ -45,13 +47,14 @@ public static OptionSpecification FromAttribute(OptionAttribute attribute, Type
4547
conversionType,
4648
conversionType.ToTargetType(),
4749
attribute.Group,
50+
attribute.FlagCounter,
4851
attribute.Hidden);
4952
}
5053

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

5760
public string ShortName
@@ -78,5 +81,13 @@ public string Group
7881
{
7982
get { return group; }
8083
}
84+
85+
/// <summary>
86+
/// Whether this is an int option that counts how many times a flag was set rather than taking a value on the command line
87+
/// </summary>
88+
public bool FlagCounter
89+
{
90+
get { return flagCounter; }
91+
}
8192
}
8293
}

src/CommandLine/Core/SpecificationExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public static OptionSpecification WithLongName(this OptionSpecification specific
3535
specification.ConversionType,
3636
specification.TargetType,
3737
specification.Group,
38+
specification.FlagCounter,
3839
specification.Hidden);
3940
}
4041

0 commit comments

Comments
 (0)