Skip to content

Commit 570d7b7

Browse files
committed
Add GetoptMode parser setting and implementation
Turning on Getopt mode automatically turns on the EnableDashDash and AllowMultiInstance settings as well, but they can be disabled by explicitly setting them to false in the parser settings.
1 parent 3354ffb commit 570d7b7

File tree

9 files changed

+720
-11
lines changed

9 files changed

+720
-11
lines changed

Diff for: src/CommandLine/Core/GetoptTokenizer.cs

+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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 replaces = tokens.Select((t, i) =>
98+
optionSequenceWithSeparatorLookup(t.Text)
99+
.MapValueOrDefault(sep => Tuple.Create(i + 1, sep),
100+
Tuple.Create(-1, '\0'))).SkipWhile(x => x.Item1 < 0).Memoize();
101+
102+
var exploded = tokens.Select((t, i) =>
103+
replaces.FirstOrDefault(x => x.Item1 == i).ToMaybe()
104+
.MapValueOrDefault(r => t.Text.Split(r.Item2).Select(Token.Value),
105+
Enumerable.Empty<Token>().Concat(new[] { t })));
106+
107+
var flattened = exploded.SelectMany(x => x);
108+
109+
return Result.Succeed(flattened, tokenizerResult.SuccessMessages());
110+
}
111+
112+
public static Func<
113+
IEnumerable<string>,
114+
IEnumerable<OptionSpecification>,
115+
Result<IEnumerable<Token>, Error>>
116+
ConfigureTokenizer(
117+
StringComparer nameComparer,
118+
bool ignoreUnknownArguments,
119+
bool enableDashDash,
120+
bool posixlyCorrect)
121+
{
122+
return (arguments, optionSpecs) =>
123+
{
124+
var tokens = GetoptTokenizer.Tokenize(arguments, name => NameLookup.Contains(name, optionSpecs, nameComparer), ignoreUnknownArguments, enableDashDash, posixlyCorrect);
125+
var explodedTokens = GetoptTokenizer.ExplodeOptionList(tokens, name => NameLookup.HavingSeparator(name, optionSpecs, nameComparer));
126+
return explodedTokens;
127+
};
128+
}
129+
130+
private static IEnumerable<Token> TokenizeShortName(
131+
string arg,
132+
Func<string, NameLookupResult> nameLookup,
133+
Action<string> onUnknownOption,
134+
Action<int> onConsumeNext)
135+
{
136+
137+
// First option char that requires a value means we swallow the rest of the string as the value
138+
// But if there is no rest of the string, then instead we swallow the next argument
139+
string chars = arg.Substring(1);
140+
int len = chars.Length;
141+
if (len > 0 && Char.IsDigit(chars[0]))
142+
{
143+
// Assume it's a negative number
144+
yield return Token.Value(arg);
145+
yield break;
146+
}
147+
for (int i = 0; i < len; i++)
148+
{
149+
var s = new String(chars[i], 1);
150+
switch(nameLookup(s))
151+
{
152+
case NameLookupResult.OtherOptionFound:
153+
yield return Token.Name(s);
154+
155+
if (i+1 < len)
156+
{
157+
// Rest of this is the value (e.g. "-sfoo" where "-s" is a string-consuming arg)
158+
yield return Token.Value(chars.Substring(i+1));
159+
yield break;
160+
}
161+
else
162+
{
163+
// Value is in next param (e.g., "-s foo")
164+
onConsumeNext(1);
165+
}
166+
break;
167+
168+
case NameLookupResult.NoOptionFound:
169+
onUnknownOption(s);
170+
break;
171+
172+
default:
173+
yield return Token.Name(s);
174+
break;
175+
}
176+
}
177+
}
178+
179+
private static IEnumerable<Token> TokenizeLongName(
180+
string arg,
181+
Func<string, NameLookupResult> nameLookup,
182+
Action<string> onBadFormatToken,
183+
Action<string> onUnknownOption,
184+
Action<int> onConsumeNext)
185+
{
186+
string[] parts = arg.Substring(2).Split(new char[] { '=' }, 2);
187+
string name = parts[0];
188+
string value = (parts.Length > 1) ? parts[1] : null;
189+
// A parameter like "--stringvalue=" is acceptable, and makes stringvalue be the empty string
190+
if (String.IsNullOrWhiteSpace(name) || name.Contains(" "))
191+
{
192+
onBadFormatToken(arg);
193+
yield break;
194+
}
195+
switch(nameLookup(name))
196+
{
197+
case NameLookupResult.NoOptionFound:
198+
onUnknownOption(name);
199+
yield break;
200+
201+
case NameLookupResult.OtherOptionFound:
202+
yield return Token.Name(name);
203+
if (value == null) // NOT String.IsNullOrEmpty
204+
{
205+
onConsumeNext(1);
206+
}
207+
else
208+
{
209+
yield return Token.Value(value);
210+
}
211+
break;
212+
213+
default:
214+
yield return Token.Name(name);
215+
break;
216+
}
217+
}
218+
}
219+
}

Diff for: src/CommandLine/Infrastructure/StringExtensions.cs

+19-1
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,23 @@ public static bool ToBoolean(this string value)
7373
{
7474
return value.Equals("true", StringComparison.OrdinalIgnoreCase);
7575
}
76+
77+
public static bool ToBooleanLoose(this string value)
78+
{
79+
if ((string.IsNullOrEmpty(value)) ||
80+
(value == "0") ||
81+
(value.Equals("f", StringComparison.OrdinalIgnoreCase)) ||
82+
(value.Equals("n", StringComparison.OrdinalIgnoreCase)) ||
83+
(value.Equals("no", StringComparison.OrdinalIgnoreCase)) ||
84+
(value.Equals("off", StringComparison.OrdinalIgnoreCase)) ||
85+
(value.Equals("false", StringComparison.OrdinalIgnoreCase)))
86+
{
87+
return false;
88+
}
89+
else
90+
{
91+
return true;
92+
}
93+
}
7694
}
77-
}
95+
}

Diff for: src/CommandLine/Parser.cs

+7-2
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,13 @@ private static Result<IEnumerable<Token>, Error> Tokenize(
185185
IEnumerable<OptionSpecification> optionSpecs,
186186
ParserSettings settings)
187187
{
188-
return
189-
Tokenizer.ConfigureTokenizer(
188+
return settings.GetoptMode
189+
? GetoptTokenizer.ConfigureTokenizer(
190+
settings.NameComparer,
191+
settings.IgnoreUnknownArguments,
192+
settings.EnableDashDash,
193+
settings.PosixlyCorrect)(arguments, optionSpecs)
194+
: Tokenizer.ConfigureTokenizer(
190195
settings.NameComparer,
191196
settings.IgnoreUnknownArguments,
192197
settings.EnableDashDash)(arguments, optionSpecs);

Diff for: src/CommandLine/ParserSettings.cs

+34-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.IO;
66

77
using CommandLine.Infrastructure;
8+
using CSharpx;
89

910
namespace CommandLine
1011
{
@@ -23,9 +24,11 @@ public class ParserSettings : IDisposable
2324
private bool autoHelp;
2425
private bool autoVersion;
2526
private CultureInfo parsingCulture;
26-
private bool enableDashDash;
27+
private Maybe<bool> enableDashDash;
2728
private int maximumDisplayWidth;
28-
private bool allowMultiInstance;
29+
private Maybe<bool> allowMultiInstance;
30+
private bool getoptMode;
31+
private Maybe<bool> posixlyCorrect;
2932

3033
/// <summary>
3134
/// Initializes a new instance of the <see cref="ParserSettings"/> class.
@@ -38,6 +41,10 @@ public ParserSettings()
3841
autoVersion = true;
3942
parsingCulture = CultureInfo.InvariantCulture;
4043
maximumDisplayWidth = GetWindowWidth();
44+
getoptMode = false;
45+
enableDashDash = Maybe.Nothing<bool>();
46+
allowMultiInstance = Maybe.Nothing<bool>();
47+
posixlyCorrect = Maybe.Nothing<bool>();
4148
}
4249

4350
private int GetWindowWidth()
@@ -159,11 +166,12 @@ public bool AutoVersion
159166
/// <summary>
160167
/// Gets or sets a value indicating whether enable double dash '--' syntax,
161168
/// that forces parsing of all subsequent tokens as values.
169+
/// If GetoptMode is true, this defaults to true, but can be turned off by explicitly specifying EnableDashDash = false.
162170
/// </summary>
163171
public bool EnableDashDash
164172
{
165-
get { return enableDashDash; }
166-
set { PopsicleSetter.Set(Consumed, ref enableDashDash, value); }
173+
get => enableDashDash.MatchJust(out bool value) ? value : getoptMode;
174+
set => PopsicleSetter.Set(Consumed, ref enableDashDash, Maybe.Just(value));
167175
}
168176

169177
/// <summary>
@@ -177,11 +185,31 @@ public int MaximumDisplayWidth
177185

178186
/// <summary>
179187
/// Gets or sets a value indicating whether options are allowed to be specified multiple times.
188+
/// If GetoptMode is true, this defaults to true, but can be turned off by explicitly specifying AllowMultiInstance = false.
180189
/// </summary>
181190
public bool AllowMultiInstance
182191
{
183-
get => allowMultiInstance;
184-
set => PopsicleSetter.Set(Consumed, ref allowMultiInstance, value);
192+
get => allowMultiInstance.MatchJust(out bool value) ? value : getoptMode;
193+
set => PopsicleSetter.Set(Consumed, ref allowMultiInstance, Maybe.Just(value));
194+
}
195+
196+
/// <summary>
197+
/// Whether strict getopt-like processing is applied to option values; if true, AllowMultiInstance and EnableDashDash will default to true as well.
198+
/// </summary>
199+
public bool GetoptMode
200+
{
201+
get => getoptMode;
202+
set => PopsicleSetter.Set(Consumed, ref getoptMode, value);
203+
}
204+
205+
/// <summary>
206+
/// Whether getopt-like processing should follow the POSIX rules (the equivalent of using the "+" prefix in the C getopt() call).
207+
/// If not explicitly set, will default to false unless the POSIXLY_CORRECT environment variable is set, in which case it will default to true.
208+
/// </summary>
209+
public bool PosixlyCorrect
210+
{
211+
get => posixlyCorrect.MapValueOrDefault(val => val, () => Environment.GetEnvironmentVariable("POSIXLY_CORRECT").ToBooleanLoose());
212+
set => PopsicleSetter.Set(Consumed, ref posixlyCorrect, Maybe.Just(value));
185213
}
186214

187215
internal StringComparer NameComparer
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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.Collections.Generic;
4+
5+
namespace CommandLine.Tests.Fakes
6+
{
7+
public class Simple_Options_WithExtraArgs
8+
{
9+
[Option(HelpText = "Define a string value here.")]
10+
public string StringValue { get; set; }
11+
12+
[Option('s', "shortandlong", HelpText = "Example with both short and long name.")]
13+
public string ShortAndLong { get; set; }
14+
15+
[Option('i', Min = 3, Max = 4, Separator = ',', HelpText = "Define a int sequence here.")]
16+
public IEnumerable<int> IntSequence { get; set; }
17+
18+
[Option('x', HelpText = "Define a boolean or switch value here.")]
19+
public bool BoolValue { get; set; }
20+
21+
[Value(0, HelpText = "Define a long value here.")]
22+
public long LongValue { get; set; }
23+
24+
[Value(1, HelpText = "Extra args get collected here.")]
25+
public IEnumerable<string> ExtraArgs { get; set; }
26+
}
27+
}

0 commit comments

Comments
 (0)