Skip to content

Commit

Permalink
Use pattern matching in tokenizer
Browse files Browse the repository at this point in the history
Pattern matching is a particularly good fit here, as each case is
data-driven and the flow is easy to read top-to-bottom.
  • Loading branch information
rmunn committed Mar 25, 2020
1 parent 1c86eea commit 3bed6b4
Showing 1 changed file with 88 additions and 123 deletions.
211 changes: 88 additions & 123 deletions src/CommandLine/Core/Tokenizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ public static Result<IEnumerable<Token>, Error> Tokenize(
bool allowDashDash)
{
var errors = new List<Error>();
Action<Error> onError = errors.Add;
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;
var tokens = new List<Token>();
Expand All @@ -36,154 +39,116 @@ public static Result<IEnumerable<Token>, Error> Tokenize(
var enumerator = arguments.GetEnumerator();
while (enumerator.MoveNext())
{
string arg = enumerator.Current;
// TODO: Turn this into a switch statement with pattern matching
if (arg == null)
{
continue;
}
switch (enumerator.Current) {
case null:
break;

if (consumeNext > 0)
{
addValue(arg);
consumeNext = consumeNext - 1;
continue;
}
case string arg when consumeNext > 0:
addValue(arg);
consumeNext = consumeNext - 1;
break;

if (arg == "--")
{
if (allowDashDash)
{
case "--" when allowDashDash:
consumeNext = System.Int32.MaxValue;
continue;
}
else
{
addValue(arg);
continue;
}
}
break;

if (arg.StartsWith("--"))
{
if (arg.Contains("="))
{
case "--":
addValue("--");
break;

case "-":
// A single hyphen is always a value (it usually means "read from stdin" or "write to stdout")
addValue("-");
break;

case string arg when arg.StartsWith("--") && arg.Contains("="):
string[] parts = arg.Substring(2).Split(new char[] { '=' }, 2);
if (String.IsNullOrWhiteSpace(parts[0]) || parts[0].Contains(" "))
{
onError(new BadFormatTokenError(arg));
continue;
onBadFormatToken(arg);
}
else
{
var name = parts[0];
var tokenType = nameLookup(name);
if (tokenType == NameLookupResult.NoOptionFound)
switch(nameLookup(parts[0]))
{
if (ignoreUnknownArguments)
{
continue;
}
else
{
onError(new UnknownOptionError(name));
continue;
}
case NameLookupResult.NoOptionFound:
onUnknownOption(parts[0]);
break;

default:
addName(parts[0]);
addValue(parts[1]);
break;
}
addName(parts[0]);
addValue(parts[1]);
continue;
}
}
else
{
break;

case string arg when arg.StartsWith("--"):
var name = arg.Substring(2);
var tokenType = nameLookup(name);
if (tokenType == NameLookupResult.OtherOptionFound)
{
addName(name);
consumeNext = 1;
continue;
}
else if (tokenType == NameLookupResult.NoOptionFound)
switch (nameLookup(name))
{
if (ignoreUnknownArguments)
{
case NameLookupResult.OtherOptionFound:
addName(name);
consumeNext = 1;
break;

case NameLookupResult.NoOptionFound:
// When ignoreUnknownArguments is true and AutoHelp is true, calling code is responsible for
// setting up nameLookup so that it will return a known name for --help, so that we don't skip it here
continue;
}
else
{
onError(new UnknownOptionError(name));
continue;
}
onUnknownOption(name);
break;

default:
addName(name);
break;
}
else
break;

case string arg when arg.StartsWith("-"):
// 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]))
{
addName(name);
// Assume it's a negative number
addValue(arg);
continue;
}
}
}

if (arg == "-")
{
// A single hyphen is always a value (it usually means "read from stdin" or "write to stdout")
addValue(arg);
continue;
}

if (arg.StartsWith("-"))
{
// 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
addValue(arg);
continue;
}
for (int i = 0; i < len; i++)
{
var s = new String(chars[i], 1);
var tokenType = nameLookup(s);
if (tokenType == NameLookupResult.OtherOptionFound)
for (int i = 0; i < len; i++)
{
addName(s);
if (i+1 < len)
{
addValue(chars.Substring(i+1));
break;
}
else
var s = new String(chars[i], 1);
switch(nameLookup(s))
{
consumeNext = 1;
case NameLookupResult.OtherOptionFound:
addName(s);
if (i+1 < len)
{
addValue(chars.Substring(i+1));
i = len; // Can't use "break" inside a switch, so this breaks out of the loop
}
else
{
consumeNext = 1;
}
break;

case NameLookupResult.NoOptionFound:
onUnknownOption(s);
break;

default:
addName(s);
break;
}
}
else if (tokenType == NameLookupResult.NoOptionFound)
{
if (ignoreUnknownArguments)
{
continue;
}
else
{
onError(new UnknownOptionError(s));
}
}
else
{
addName(s);
}
}
continue;
}
break;

// If we get this far, it's a plain value
addValue(arg);
case string arg:
// If we get this far, it's a plain value
addValue(arg);
break;
}
}

return Result.Succeed<IEnumerable<Token>, Error>(tokens.AsEnumerable(), errors.AsEnumerable());
Expand Down

0 comments on commit 3bed6b4

Please sign in to comment.