From e686de8ef9286210fa839205e95811b9876ed04c Mon Sep 17 00:00:00 2001 From: Fuwei Chin Date: Thu, 21 Jul 2022 13:34:15 +0800 Subject: [PATCH 1/2] refactor: Use CommandLineToArgvW equivalents to parse args line The goal of this change is to parse nginx configure arguments which can be get by `nginx -V`. TODO Tests need to be updated. BREAKING CHANGE: quote with single quotes are not supported, double quote escaping and backslash escaping. --- Commander.NET/Utils.cs | 232 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 227 insertions(+), 5 deletions(-) diff --git a/Commander.NET/Utils.cs b/Commander.NET/Utils.cs index 507a5cc..e7b65c5 100644 --- a/Commander.NET/Utils.cs +++ b/Commander.NET/Utils.cs @@ -9,7 +9,7 @@ namespace Commander.NET { - internal static class Utils + internal static class Utils { internal static IEnumerable GetParameterMembers(BindingFlags flags) where Q : Attribute { @@ -101,8 +101,8 @@ internal static MemberInfo GetCommandWithName(string name, BindingFlags bindi return member; } return null; - } - + } + /* internal static string[] SplitArgumentsLine(string line) { List args = new List(); @@ -155,8 +155,230 @@ internal static string[] SplitArgumentsLine(string line) reset(); - return args.Where(a => !string.IsNullOrWhiteSpace(a)) - .ToArray(); + return args.Where(a => !string.IsNullOrWhiteSpace(a)).ToArray(); + } + */ + internal static string[] SplitArgumentsLine(string line) + { + return CommandLineToArgvW("echo " + line).Skip(1).ToArray(); + } + + /** + * C# equivalent of CommandLineToArgvW + * Translated from https://source.winehq.org/git/wine.git/blob/HEAD:/dlls/shcore/main.c#l264 + */ + public static string[] CommandLineToArgvW(string cmdline) + { + if ((cmdline = cmdline.Trim()).Length == 0) + { + return new string[0]; + } + int len = cmdline.Length; + int argc = 0; + int i = 0; + char s = cmdline[i]; + char END = '\0'; + /* The first argument, the executable path, follows special rules */ + argc = 1; + if (s == '"') + { + do + { + s = ++i < len ? cmdline[i] : END; + if (s == '"') + break; + } while (s != END); + } + else + { + while (s != END && s != ' ' && s != '\t') + { + s = ++i < len ? cmdline[i] : END; + } + } + /* skip to the first argument, if any */ + while (s == ' ' || s == '\t') + s = ++i < len ? cmdline[i] : END; + if (s != END) + argc++; + + /* Analyze the remaining arguments */ + int qcount = 0; // quote count + int bcount = 0; // backslash count + while (i < len) + { + s = cmdline[i]; + if ((s == ' ' || s == '\t') && qcount == 0) + { + /* skip to the next argument and count it if any */ + do + { + s = ++i < len ? cmdline[i] : END; + } while (s == ' ' || s == '\t'); + if (s != END) + argc++; + bcount = 0; + } + else if (s == '\\') + { + /* '\', count them */ + bcount++; + s = ++i < len ? cmdline[i] : END; + } + else if (s == '"') + { + /* '"' */ + if ((bcount & 1) == 0) + qcount++; /* unescaped '"' */ + s = ++i < len ? cmdline[i] : END; + bcount = 0; + /* consecutive quotes, see comment in copying code below */ + while (s == '"') + { + qcount++; + s = ++i < len ? cmdline[i] : END; + } + qcount = qcount % 3; + if (qcount == 2) + qcount = 0; + } + else + { + /* a regular character */ + bcount = 0; + s = ++i < len ? cmdline[i] : END; + } + } + string[] argv = new string[argc]; + StringBuilder sb = new StringBuilder(); + i = 0; + int j = 0; + s = cmdline[i]; + if (s == '"') + { + do + { + s = ++i < len ? cmdline[i] : END; + if (s == '"') + break; + else + sb.Append(s); + } while (s != END); + argv[j++] = sb.ToString(); + sb.Clear(); + } + else + { + while (s != END && s != ' ' && s != '\t') + { + sb.Append(s); + s = ++i < len ? cmdline[i] : END; + } + argv[j++] = sb.ToString(); + sb.Clear(); + } + while (s == ' ' || s == '\t') + s = ++i < len ? cmdline[i] : END; + if (i >= len) + return argv; + qcount = 0; + bcount = 0; + while (i < len) + { + if ((s == ' ' || s == '\t') && qcount == 0) + { + /* close the argument */ + argv[j++] = sb.ToString(); + sb.Clear(); + bcount = 0; + /* skip to the next one and initialize it if any */ + do + { + s = ++i < len ? cmdline[i] : END; + } while (s == ' ' || s == '\t'); + } + else if (s == '\\') + { + sb.Append(s); + s = ++i < len ? cmdline[i] : END; + bcount++; + } + else if (s == '"') + { + if ((bcount & 1) == 0) + { + /* Preceded by an even number of '\', this is half that number of '\', plus a quote which we erase. */ + sb.Length -= bcount / 2; + qcount++; + } + else + { + /* Preceded by an odd number of '\', this is half that number of '\' followed by a '"' */ + sb.Length = (sb.Length - 1) - bcount / 2 - 1; + sb.Append('"'); + } + s = ++i < len ? cmdline[i] : END; + bcount = 0; + /* Now count the number of consecutive quotes. Note that qcount + * already takes into account the opening quote if any, as well as + * the quote that lead us here. + */ + while (s == '"') + { + if (++qcount == 3) + { + sb.Append('"'); + qcount = 0; + } + s = ++i < len ? cmdline[i] : END; + } + if (qcount == 2) + qcount = 0; + } + else + { + /* a regular character */ + sb.Append(s); + s = ++i < len ? cmdline[i] : END; + bcount = 0; + } + } + if (sb.Length > 0) + { + argv[j++] = sb.ToString(); + sb.Clear(); + } + return argv; } + + /** + * Windows native CommandLineToArgvW + * Copied from https://stackoverflow.com/questions/298830/split-string-containing-command-line-parameters-into-string-in-c-sharp#answer-749653 + */ + /* + [DllImport("shell32.dll", SetLastError = true)] + static extern IntPtr CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs); + public static string[] CommandLineToArgvW(string commandLine) + { + int argc; + IntPtr argv = CommandLineToArgvW(commandLine, out argc); + if (argv == IntPtr.Zero) + throw new System.ComponentModel.Win32Exception(); + try + { + string[] args = new string[argc]; + for (int i = 0; i < argc; i++) + { + IntPtr p = Marshal.ReadIntPtr(argv, i * IntPtr.Size); + args[i] = Marshal.PtrToStringUni(p); + } + return args; + } + finally + { + Marshal.FreeHGlobal(argv); + } + } + */ } } From 9a396c89a52f0b24fb0e00fc700bdec13bc0e17f Mon Sep 17 00:00:00 2001 From: Fuwei Chin Date: Thu, 21 Jul 2022 14:02:24 +0800 Subject: [PATCH 2/2] refactor: Allow --somekey="double-quoted spaced-separated" Allow double-quotes and spaces when separators are set to equal (=) and/or colon (:), base on previous commit "refactor: Use CommandLineToArgvW equivalents to parse args line". This maybe a BREAKING CHANGE, empty arg values are also allowed now. --- Commander.NET/Models/RawArguments.cs | 49 +++++++++++++++++----------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/Commander.NET/Models/RawArguments.cs b/Commander.NET/Models/RawArguments.cs index 520d607..fd9422e 100644 --- a/Commander.NET/Models/RawArguments.cs +++ b/Commander.NET/Models/RawArguments.cs @@ -8,7 +8,7 @@ namespace Commander.NET.Models { - internal class RawArguments + internal class RawArguments { BindingFlags bindingFlags; HashSet booleanKeys = new HashSet(); @@ -55,32 +55,43 @@ internal string GetMatchingKey(IEnumerable keys) internal List GetPositionalArguments() { return new List(positionalArguments); - } - + } + internal RawArguments Parse(string[] args, Separators separators) { List commands = Utils.GetCommandNames(bindingFlags); for (int i = 0; i < args.Length; i++) { - if (string.IsNullOrWhiteSpace(args[i])) + string arg = args[i]; + if (string.IsNullOrWhiteSpace(arg)) continue; + if ((arg.Matches(@"^-[a-zA-Z0-9_]=") || arg.Matches(@"^--[a-zA-Z0-9_-]{2,}=")) && separators.HasFlag(Separators.Equals)) + { + string pair = arg.TrimStart('-'); + int pos = pair.IndexOf('='); + string key = pair.Substring(0, pos); + string value = pair.Substring(pos + 1); + Console.WriteLine(key + "=" + value); - if ((args[i].Matches(@"^-[a-zA-Z0-9_]=\w+$") || args[i].Matches(@"^--[a-zA-Z0-9_-]{2,}=\w+$")) && separators.HasFlag(Separators.Equals) - || (args[i].Matches(@"^-[a-zA-Z0-9_]:\w+$") || args[i].Matches(@"^--[a-zA-Z0-9_-]{2,}:\w+$")) && separators.HasFlag(Separators.Colon)) + TryAddKeyValuePair(key, value); + } + else if ((arg.Matches(@"^-[a-zA-Z0-9_]:") || arg.Matches(@"^--[a-zA-Z0-9_-]{2,}:")) && separators.HasFlag(Separators.Colon)) { - string key = args[i].TrimStart('-').Split(':')[0].Split('=')[0]; - string value = args[i].Split(':').Last().Split('=').Last(); + string pair = arg.TrimStart('-'); + int pos = pair.IndexOf(':'); + string key = pair.Substring(0, pos); + string value = pair.Substring(pos + 1); TryAddKeyValuePair(key, value); } - else if (args[i].Matches(@"^-[a-zA-Z0-9_]$") || args[i].Matches(@"^--[a-zA-Z0-9_-]{2,}$")) + else if (arg.Matches(@"^-[a-zA-Z0-9_]$") || arg.Matches(@"^--[a-zA-Z0-9_-]{2,}$")) { - string key = args[i].TrimStart('-'); + string key = arg.TrimStart('-'); int intTest; - if (!booleanKeys.Contains(key) && i < args.Length - 1 - && (!args[i + 1].StartsWith("-") || int.TryParse(args[i+1], out intTest))) + if (!booleanKeys.Contains(key) && i < args.Length - 1 + && (!args[i + 1].StartsWith("-") || int.TryParse(args[i + 1], out intTest))) { TryAddKeyValuePair(key, args[i + 1]); i++; @@ -90,19 +101,19 @@ internal RawArguments Parse(string[] args, Separators separators) flags.Add(key); } } - else if (args[i].Matches(@"^-[a-zA-Z0-9_]{2,}$")) + else if (arg.Matches(@"^-[a-zA-Z0-9_]{2,}$")) { // Multiple flags flags.AddRange( - args[i].ToCharArray().Select(c => c.ToString()) + arg.ToCharArray().Select(c => c.ToString()) ); } else { - if (commands.Contains(args[i])) + if (commands.Contains(arg)) { // We caught a command name, stop parsing - Command = args[i]; + Command = arg; CommandIndex = i; return this; @@ -110,13 +121,13 @@ internal RawArguments Parse(string[] args, Separators separators) else { // No commands with this name, add it to the positional arguments - positionalArguments.Add(args[i]); + positionalArguments.Add(arg); } } } return this; - } - + } + internal bool GetBoolean(string key) { return flags.Contains(key) || keyValuePairs.ContainsKey(key);