I need to support nesting so none of these worked for me. I gave up trying to do it via Regex and just coded:
public static Argument[] ParseCmdLine(string args) {
List<string> ls = new List<string>();
StringBuilder sb = new StringBuilder(128);
// support quoted text nesting up to 8 levels deep
Span<char> quoteChar = stackalloc char[8];
int quoteLevel = 0;
for (int i = 0; i < args.Length; ++i) {
char ch = args[i];
switch (ch) {
case ' ':
if (quoteLevel == 0) {
ls.Add(sb.ToString());
sb.Clear();
break;
}
goto default;
case '"':
case '\'':
if (quoteChar[quoteLevel] == ch) {
--quoteLevel;
} else {
quoteChar[++quoteLevel] = ch;
}
goto default;
default:
sb.Append(ch);
break;
}
}
if (sb.Length > 0) { ls.Add(sb.ToString()); sb.Clear(); }
return Arguments.ParseCmdLine(ls.ToArray());
}
And here's some additional code to parse the command line arguments to objects:
public struct Argument {
public string Prefix;
public string Name;
public string Eq;
public string QuoteType;
public string Value;
public string[] ToArray() => this.Eq == " " ? new string[] { $"{Prefix}{Name}", $"{QuoteType}{Value}{QuoteType}" } : new string[] { this.ToString() };
public override string ToString() => $"{Prefix}{Name}{Eq}{QuoteType}{Value}{QuoteType}";
}
private static readonly Regex RGX_MatchArg = new Regex(@"^(?<prefix>-{1,2}|\/)(?<name>[a-zA-Z][a-zA-Z_-]*)(?<assignment>(?<eq>[:= ]|$)(?<quote>[""'])?(?<value>.+?)(?:\k<quote>|\s*$))?");
private static readonly Regex RGX_MatchQuoted = new Regex(@"(?<quote>[""'])?(?<value>.+?)(?:\k<quote>|\s*$)");
public static Argument[] ParseCmdLine(string[] rawArgs) {
int count = 0;
Argument[] pairs = new Argument[rawArgs.Length];
int i = 0;
while(i < rawArgs.Length) {
string current = rawArgs[i];
i+=1;
Match matches = RGX_MatchArg.Match(current);
Argument arg = new Argument();
arg.Prefix = matches.Groups["prefix"].Value;
arg.Name = matches.Groups["name"].Value;
arg.Value = matches.Groups["value"].Value;
if(!string.IsNullOrEmpty(arg.Value)) {
arg.Eq = matches.Groups["eq"].Value;
arg.QuoteType = matches.Groups["quote"].Value;
} else if ((i < rawArgs.Length) && !rawArgs[i].StartsWith('-') && !rawArgs[i].StartsWith('/')) {
arg.Eq = " ";
Match quoted = RGX_MatchQuoted.Match(rawArgs[i]);
arg.QuoteType = quoted.Groups["quote"].Value;
arg.Value = quoted.Groups["value"].Value;
i+=1;
}
if(string.IsNullOrEmpty(arg.QuoteType) && arg.Value.IndexOfAny(new char[] { ' ', '/', '\\', '-', '=', ':' }) >= 0) {
arg.QuoteType = "\"";
}
pairs[count++] = arg;
}
return pairs.Slice(0..count);
}
public static ILookup<string, Argument> ToLookup(this Argument[] args) => args.ToLookup((arg) => arg.Name, StringComparer.OrdinalIgnoreCase);
}
It's able to parse all different kinds of argument variants:
-test -environment staging /DEqTest=avalue /Dcolontest:anothervalue /DwithSpaces="heys: guys" /slashargflag -action="Do: 'The Thing'" -action2 "do: 'Do: \"The Thing\"'" -init
Nested quotes just need to be alternated between different quote types.