Skip to content

Commit ee77b84

Browse files
committed
Simplify tokenizer's switch statement
The short and long option code can move into separate functions, which makes the big switch statement much easier to read. Also remove one TODO comment that's not relevant to the current feature.
1 parent 3bed6b4 commit ee77b84

File tree

1 file changed

+97
-84
lines changed

1 file changed

+97
-84
lines changed

src/CommandLine/Core/Tokenizer.cs

Lines changed: 97 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ public static Result<IEnumerable<Token>, Error> Tokenize(
3232
Action<string> onUnknownOption = ignoreUnknownArguments ? doNothing : unknownOptionError;
3333

3434
int consumeNext = 0;
35+
Action<int> onConsumeNext = (n => consumeNext = consumeNext + n);
36+
3537
var tokens = new List<Token>();
36-
Action<string> addValue = (s => tokens.Add(new Value(s)));
37-
Action<string> addName = (s => tokens.Add(new Name(s)));
3838

3939
var enumerator = arguments.GetEnumerator();
4040
while (enumerator.MoveNext())
@@ -44,7 +44,7 @@ public static Result<IEnumerable<Token>, Error> Tokenize(
4444
break;
4545

4646
case string arg when consumeNext > 0:
47-
addValue(arg);
47+
tokens.Add(new Value(arg));
4848
consumeNext = consumeNext - 1;
4949
break;
5050

@@ -53,100 +53,25 @@ public static Result<IEnumerable<Token>, Error> Tokenize(
5353
break;
5454

5555
case "--":
56-
addValue("--");
56+
tokens.Add(new Value("--"));
5757
break;
5858

5959
case "-":
6060
// A single hyphen is always a value (it usually means "read from stdin" or "write to stdout")
61-
addValue("-");
62-
break;
63-
64-
case string arg when arg.StartsWith("--") && arg.Contains("="):
65-
string[] parts = arg.Substring(2).Split(new char[] { '=' }, 2);
66-
if (String.IsNullOrWhiteSpace(parts[0]) || parts[0].Contains(" "))
67-
{
68-
onBadFormatToken(arg);
69-
}
70-
else
71-
{
72-
switch(nameLookup(parts[0]))
73-
{
74-
case NameLookupResult.NoOptionFound:
75-
onUnknownOption(parts[0]);
76-
break;
77-
78-
default:
79-
addName(parts[0]);
80-
addValue(parts[1]);
81-
break;
82-
}
83-
}
61+
tokens.Add(new Value("-"));
8462
break;
8563

8664
case string arg when arg.StartsWith("--"):
87-
var name = arg.Substring(2);
88-
switch (nameLookup(name))
89-
{
90-
case NameLookupResult.OtherOptionFound:
91-
addName(name);
92-
consumeNext = 1;
93-
break;
94-
95-
case NameLookupResult.NoOptionFound:
96-
// When ignoreUnknownArguments is true and AutoHelp is true, calling code is responsible for
97-
// setting up nameLookup so that it will return a known name for --help, so that we don't skip it here
98-
onUnknownOption(name);
99-
break;
100-
101-
default:
102-
addName(name);
103-
break;
104-
}
65+
tokens.AddRange(TokenizeLongName(arg, nameLookup, onBadFormatToken, onUnknownOption, onConsumeNext));
10566
break;
10667

10768
case string arg when arg.StartsWith("-"):
108-
// First option char that requires a value means we swallow the rest of the string as the value
109-
// But if there is no rest of the string, then instead we swallow the next argument
110-
string chars = arg.Substring(1);
111-
int len = chars.Length;
112-
if (len > 0 && Char.IsDigit(chars[0]))
113-
{
114-
// Assume it's a negative number
115-
addValue(arg);
116-
continue;
117-
}
118-
for (int i = 0; i < len; i++)
119-
{
120-
var s = new String(chars[i], 1);
121-
switch(nameLookup(s))
122-
{
123-
case NameLookupResult.OtherOptionFound:
124-
addName(s);
125-
if (i+1 < len)
126-
{
127-
addValue(chars.Substring(i+1));
128-
i = len; // Can't use "break" inside a switch, so this breaks out of the loop
129-
}
130-
else
131-
{
132-
consumeNext = 1;
133-
}
134-
break;
135-
136-
case NameLookupResult.NoOptionFound:
137-
onUnknownOption(s);
138-
break;
139-
140-
default:
141-
addName(s);
142-
break;
143-
}
144-
}
69+
tokens.AddRange(TokenizeShortName(arg, nameLookup, onUnknownOption, onConsumeNext));
14570
break;
14671

14772
case string arg:
14873
// If we get this far, it's a plain value
149-
addValue(arg);
74+
tokens.Add(new Value(arg));
15075
break;
15176
}
15277
}
@@ -158,7 +83,6 @@ public static Result<IEnumerable<Token>, Error> ExplodeOptionList(
15883
Result<IEnumerable<Token>, Error> tokenizerResult,
15984
Func<string, Maybe<char>> optionSequenceWithSeparatorLookup)
16085
{
161-
// TODO: I don't like how this works. I don't want "-s foo;bar baz" to put three values into -s. Let's instead have a third token type, List, besides Name and Value.
16286
var tokens = tokenizerResult.SucceededWith().Memoize();
16387

16488
var replaces = tokens.Select((t, i) =>
@@ -192,5 +116,94 @@ public static Func<
192116
return explodedTokens;
193117
};
194118
}
119+
120+
private static IEnumerable<Token> TokenizeShortName(
121+
string arg,
122+
Func<string, NameLookupResult> nameLookup,
123+
Action<string> onUnknownOption,
124+
Action<int> onConsumeNext)
125+
{
126+
127+
// First option char that requires a value means we swallow the rest of the string as the value
128+
// But if there is no rest of the string, then instead we swallow the next argument
129+
string chars = arg.Substring(1);
130+
int len = chars.Length;
131+
if (len > 0 && Char.IsDigit(chars[0]))
132+
{
133+
// Assume it's a negative number
134+
yield return Token.Value(arg);
135+
yield break;
136+
}
137+
for (int i = 0; i < len; i++)
138+
{
139+
var s = new String(chars[i], 1);
140+
switch(nameLookup(s))
141+
{
142+
case NameLookupResult.OtherOptionFound:
143+
yield return Token.Name(s);
144+
145+
if (i+1 < len)
146+
{
147+
// Rest of this is the value (e.g. "-sfoo" where "-s" is a string-consuming arg)
148+
yield return Token.Value(chars.Substring(i+1));
149+
yield break;
150+
}
151+
else
152+
{
153+
// Value is in next param (e.g., "-s foo")
154+
onConsumeNext(1);
155+
}
156+
break;
157+
158+
case NameLookupResult.NoOptionFound:
159+
onUnknownOption(s);
160+
break;
161+
162+
default:
163+
yield return Token.Name(s);
164+
break;
165+
}
166+
}
167+
}
168+
169+
private static IEnumerable<Token> TokenizeLongName(
170+
string arg,
171+
Func<string, NameLookupResult> nameLookup,
172+
Action<string> onBadFormatToken,
173+
Action<string> onUnknownOption,
174+
Action<int> onConsumeNext)
175+
{
176+
string[] parts = arg.Substring(2).Split(new char[] { '=' }, 2);
177+
string name = parts[0];
178+
string value = (parts.Length > 1) ? parts[1] : null;
179+
// A parameter like "--stringvalue=" is acceptable, and makes stringvalue be the empty string
180+
if (String.IsNullOrWhiteSpace(name) || name.Contains(" "))
181+
{
182+
onBadFormatToken(arg);
183+
yield break;
184+
}
185+
switch(nameLookup(name))
186+
{
187+
case NameLookupResult.NoOptionFound:
188+
onUnknownOption(name);
189+
yield break;
190+
191+
case NameLookupResult.OtherOptionFound:
192+
yield return Token.Name(name);
193+
if (value == null) // NOT String.IsNullOrEmpty
194+
{
195+
onConsumeNext(1);
196+
}
197+
else
198+
{
199+
yield return Token.Value(value);
200+
}
201+
break;
202+
203+
default:
204+
yield return Token.Name(name);
205+
break;
206+
}
207+
}
195208
}
196209
}

0 commit comments

Comments
 (0)