Mam pojedynczy ciąg zawierający parametry wiersza polecenia do przekazania do innego pliku wykonywalnego i muszę wyodrębnić ciąg [] zawierający poszczególne parametry w taki sam sposób, jak zrobiłby to C #, gdyby polecenia zostały określone w wierszu polecenia. Ciąg [] będzie używany podczas wykonywania innego punktu wejścia zestawu za pośrednictwem odbicia.
Czy jest do tego standardowa funkcja? A może jest preferowana metoda (regex?) Poprawnego dzielenia parametrów? Musi obsługiwać ciągi rozdzielane '"', które mogą poprawnie zawierać spacje, więc nie mogę po prostu podzielić na ''.
Przykładowy ciąg:
string parameterString = @"/src:""C:\tmp\Some Folder\Sub Folder"" /users:""[email protected]"" tasks:""SomeTask,Some Other Task"" -someParam foo";
Przykładowy wynik:
string[] parameterArray = new string[] {
@"/src:C:\tmp\Some Folder\Sub Folder",
@"/users:[email protected]",
@"tasks:SomeTask,Some Other Task",
@"-someParam",
@"foo"
};
Nie potrzebuję biblioteki parsującej wiersza poleceń, tylko sposób na pobranie ciągu [], który powinien zostać wygenerowany.
Aktualizacja : musiałem zmienić oczekiwany wynik, aby pasował do tego, co jest faktycznie generowane przez C # (usunąłem dodatkowe „w podzielonych ciągach”)
źródło
Odpowiedzi:
Oprócz dobrego i czystego rozwiązania zarządzanego przez Earwicker , warto wspomnieć, dla kompletności, że Windows udostępnia również
CommandLineToArgvW
funkcję dzielenia łańcucha na tablicę ciągów:Przykład wywołania tego interfejsu API z C # i rozpakowania wynikowej tablicy ciągów w kodzie zarządzanym można znaleźć pod adresem „ Konwertowanie ciągu wiersza polecenia na Args [] przy użyciu interfejsu API CommandLineToArgvW () ”. Poniżej znajduje się nieco prostsza wersja tego samego kodu:
[DllImport("shell32.dll", SetLastError = true)] static extern IntPtr CommandLineToArgvW( [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs); public static string[] CommandLineToArgs(string commandLine) { int argc; var argv = CommandLineToArgvW(commandLine, out argc); if (argv == IntPtr.Zero) throw new System.ComponentModel.Win32Exception(); try { var args = new string[argc]; for (var i = 0; i < args.Length; i++) { var p = Marshal.ReadIntPtr(argv, i * IntPtr.Size); args[i] = Marshal.PtrToStringUni(p); } return args; } finally { Marshal.FreeHGlobal(argv); } }
źródło
CommandLineToArgs("foo.exe " + commandLine).Skip(1).ToArray();
Irytuje mnie, że nie ma funkcji do dzielenia łańcucha na podstawie funkcji, która sprawdza każdy znak. Gdyby tak było, można by to napisać tak:
public static IEnumerable<string> SplitCommandLine(string commandLine) { bool inQuotes = false; return commandLine.Split(c => { if (c == '\"') inQuotes = !inQuotes; return !inQuotes && c == ' '; }) .Select(arg => arg.Trim().TrimMatchingQuotes('\"')) .Where(arg => !string.IsNullOrEmpty(arg)); }
Chociaż napisałem to, dlaczego nie napisać niezbędnych metod rozszerzających. Okej, namówiłeś mnie do tego ...
Po pierwsze, moja własna wersja Split, która przyjmuje funkcję, która musi zdecydować, czy określony znak powinien podzielić ciąg:
public static IEnumerable<string> Split(this string str, Func<char, bool> controller) { int nextPiece = 0; for (int c = 0; c < str.Length; c++) { if (controller(str[c])) { yield return str.Substring(nextPiece, c - nextPiece); nextPiece = c + 1; } } yield return str.Substring(nextPiece); }
W zależności od sytuacji może dać kilka pustych ciągów, ale może ta informacja będzie przydatna w innych przypadkach, więc nie usuwam pustych wpisów w tej funkcji.
Po drugie (i bardziej przyziemnie) mały pomocnik, który obetnie pasującą parę cudzysłowów od początku i końca łańcucha. Jest bardziej wybredna niż standardowa metoda Trim - przycina tylko jeden znak z każdego końca i nie przycina tylko jednego końca:
public static string TrimMatchingQuotes(this string input, char quote) { if ((input.Length >= 2) && (input[0] == quote) && (input[input.Length - 1] == quote)) return input.Substring(1, input.Length - 2); return input; }
I przypuszczam, że będziesz chciał też trochę testów. Cóż, w porządku. Ale to musi być absolutnie ostatnia rzecz! Najpierw funkcja pomocnicza, która porównuje wynik podziału z oczekiwaną zawartością tablicy:
public static void Test(string cmdLine, params string[] args) { string[] split = SplitCommandLine(cmdLine).ToArray(); Debug.Assert(split.Length == args.Length); for (int n = 0; n < split.Length; n++) Debug.Assert(split[n] == args[n]); }
Wtedy mogę napisać takie testy:
Test(""); Test("a", "a"); Test(" abc ", "abc"); Test("a b ", "a", "b"); Test("a b \"c d\"", "a", "b", "c d");
Oto test dla twoich wymagań:
Test(@"/src:""C:\tmp\Some Folder\Sub Folder"" /users:""[email protected]"" tasks:""SomeTask,Some Other Task"" -someParam", @"/src:""C:\tmp\Some Folder\Sub Folder""", @"/users:""[email protected]""", @"tasks:""SomeTask,Some Other Task""", @"-someParam");
Zwróć uwagę, że implementacja ma tę dodatkową funkcję, że usuwa cudzysłowy wokół argumentu, jeśli ma to sens (dzięki funkcji TrimMatchingQuotes). Uważam, że to część normalnej interpretacji wiersza poleceń.
źródło
char.IsWhiteSpace
zamiast== ' '
Parser wiersza poleceń systemu Windows zachowuje się tak, jak mówisz, podzielony na spację, chyba że przed nim znajduje się niezamknięty cudzysłów. Poleciłbym sam napisać parser. Może coś takiego:
static string[] ParseArguments(string commandLine) { char[] parmChars = commandLine.ToCharArray(); bool inQuote = false; for (int index = 0; index < parmChars.Length; index++) { if (parmChars[index] == '"') inQuote = !inQuote; if (!inQuote && parmChars[index] == ' ') parmChars[index] = '\n'; } return (new string(parmChars)).Split('\n'); }
źródło
Wziąłem odpowiedź od Jeffreya L Whitledge'a i trochę ją poprawiłem.
Obsługuje teraz zarówno pojedyncze, jak i podwójne cudzysłowy. Możesz używać cudzysłowów w samych parametrach, używając innych cudzysłowów.
Usuwa również cytaty z argumentów, ponieważ nie wpływają one na informacje o argumentach.
public static string[] SplitArguments(string commandLine) { var parmChars = commandLine.ToCharArray(); var inSingleQuote = false; var inDoubleQuote = false; for (var index = 0; index < parmChars.Length; index++) { if (parmChars[index] == '"' && !inSingleQuote) { inDoubleQuote = !inDoubleQuote; parmChars[index] = '\n'; } if (parmChars[index] == '\'' && !inDoubleQuote) { inSingleQuote = !inSingleQuote; parmChars[index] = '\n'; } if (!inSingleQuote && !inDoubleQuote && parmChars[index] == ' ') parmChars[index] = '\n'; } return (new string(parmChars)).Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); }
źródło
Dobry i czysty zarządzane rozwiązanie przez Earwicker nie argumentów uchwytem jak ten:
Test("\"He whispered to her \\\"I love you\\\".\"", "He whispered to her \"I love you\".");
Zwrócił 3 elementy:
"He whispered to her \"I love you\"."
Oto poprawka do obsługi „cytatu \" ucieczki \ "cytatu":
public static IEnumerable<string> SplitCommandLine(string commandLine) { bool inQuotes = false; bool isEscaping = false; return commandLine.Split(c => { if (c == '\\' && !isEscaping) { isEscaping = true; return false; } if (c == '\"' && !isEscaping) inQuotes = !inQuotes; isEscaping = false; return !inQuotes && Char.IsWhiteSpace(c)/*c == ' '*/; }) .Select(arg => arg.Trim().TrimMatchingQuotes('\"').Replace("\\\"", "\"")) .Where(arg => !string.IsNullOrEmpty(arg)); }
Przetestowano w 2 dodatkowych przypadkach:
Test("\"C:\\Program Files\"", "C:\\Program Files"); Test("\"He whispered to her \\\"I love you\\\".\"", "He whispered to her \"I love you\".");
Również zauważyć, że akceptowaną odpowiedź przez Atif Aziz , który wykorzystuje CommandLineToArgvW również nie. Zwrócił 4 elementy:
He whispered to her \ I love you".
Mam nadzieję, że pomoże to komuś, kto szuka takiego rozwiązania w przyszłości.
źródło
bla.exe aAAA"b\"ASDS\"c"dSADSD
co powoduje,aAAAb"ASDS"cdSADSD
że to rozwiązanie mogłoby się pojawićaAAA"b"ASDS"c"dSADSD
. Mogę rozważyć zmianę naTrimMatchingQuotes
aRegex("(?<!\\\\)\\\"")
i używać go w ten sposób .Environment.GetCommandLineArgs ()
źródło
Lubię iteratory, a obecnie LINQ sprawia, że jest
IEnumerable<String>
równie łatwy w użyciu jak tablice ciągów, więc podążam za duchem odpowiedzi Jeffreya L Whitledge'a (jako metoda rozszerzeniastring
):public static IEnumerable<string> ParseArguments(this string commandLine) { if (string.IsNullOrWhiteSpace(commandLine)) yield break; var sb = new StringBuilder(); bool inQuote = false; foreach (char c in commandLine) { if (c == '"' && !inQuote) { inQuote = true; continue; } if (c != '"' && !(char.IsWhiteSpace(c) && !inQuote)) { sb.Append(c); continue; } if (sb.Length > 0) { var result = sb.ToString(); sb.Clear(); inQuote = false; yield return result; } } if (sb.Length > 0) yield return sb.ToString(); }
źródło
W swoim pytaniu poprosiłeś o wyrażenie regularne, a ja jestem ich wielkim fanem i użytkownikiem, więc kiedy musiałem zrobić ten sam podział argumentów co Ty, napisałem własne wyrażenie regularne po przeszukaniu go przez Google i nie znajdując prostego rozwiązania. Lubię krótkie rozwiązania, więc zrobiłem jedno i oto jest:
var re = @"\G(""((""""|[^""])+)""|(\S+)) *"; var ms = Regex.Matches(CmdLine, re); var list = ms.Cast<Match>() .Select(m => Regex.Replace( m.Groups[2].Success ? m.Groups[2].Value : m.Groups[4].Value, @"""""", @"""")).ToArray();
Obsługuje spacje i cudzysłowy w cudzysłowach i konwertuje ujęte "" na ". Zapraszam do używania kodu!
źródło
O cholera. To wszystko ... Eugh. Ale to jest oficjalne. Z Microsoft w C # dla .NET Core, może tylko Windows, może wieloplatformowy, ale na licencji MIT.
Wybierz ciekawostki, deklaracje metod i godne uwagi komentarze;
internal static unsafe string[] InternalCreateCommandLine(bool includeArg0) private static unsafe int SegmentCommandLine(char * pCmdLine, string[] argArray, bool includeArg0) private static unsafe int ScanArgument0(ref char* psrc, char[] arg) private static unsafe int ScanArgument(ref char* psrc, ref bool inquote, char[] arg)
-
// First, parse the program name (argv[0]). Argv[0] is parsed under special rules. Anything up to // the first whitespace outside a quoted subtring is accepted. Backslashes are treated as normal // characters.
-
// Rules: 2N backslashes + " ==> N backslashes and begin/end quote // 2N+1 backslashes + " ==> N backslashes + literal " // N backslashes ==> N backslashes
To jest kod przeniesiony do .NET Core z .NET Framework z tego, co zakładam, albo z biblioteki MSVC C, albo z
CommandLineToArgvW
.Oto moja połowiczna próba radzenia sobie z niektórymi oszustwami za pomocą wyrażeń regularnych i ignorowania argumentu zero bit. To trochę magiczne.
private static readonly Regex RxWinArgs = new Regex("([^\\s\"]+\"|((?<=\\s|^)(?!\"\"(?!\"))\")+)(\"\"|.*?)*\"[^\\s\"]*|[^\\s]+", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant); internal static IEnumerable<string> ParseArgumentsWindows(string args) { var match = RxWinArgs.Match(args); while (match.Success) { yield return match.Value; match = match.NextMatch(); } }
Przetestowałem to dość dobrze na zwariowanych generowanych wynikach. Jego dane wyjściowe pasują do sporego procentu tego, co małpy wpisały i przeszły
CommandLineToArgvW
.źródło
Ten artykuł w The Code Project jest tym, czego używałem w przeszłości. To dobry kawałek kodu, ale może zadziałać.
Ten artykuł MSDN jest jedyną rzeczą, jaką udało mi się znaleźć, która wyjaśnia, jak C # analizuje argumenty wiersza polecenia.
źródło
Czysto udało rozwiązanie może być pomocne. Istnieje zbyt wiele „problematycznych” komentarzy dotyczących funkcji WINAPI i nie jest ona dostępna na innych platformach. Oto mój kod, który ma dobrze zdefiniowane zachowanie (możesz je zmienić, jeśli chcesz).
Powinien robić to samo, co .NET / Windows, dostarczając ten
string[] args
parametr, porównałem go z wieloma „interesującymi” wartościami.Jest to klasyczna implementacja maszyny stanu, która pobiera każdy pojedynczy znak z ciągu wejściowego i interpretuje go dla bieżącego stanu, tworząc dane wyjściowe i nowy stan. Stan jest określony w zmiennych
escape
,inQuote
,hadQuote
aprevCh
, a wyjście jest pobierane zcurrentArg
iargs
.Niektóre ze specjalności, które odkryłem podczas eksperymentów w prawdziwym wierszu poleceń (Windows 7):
\\
produkuje\
,\"
produkuje"
,""
w podanym zakresie produkuje"
.^
Postać wydaje się być magiczne, zbyt: zawsze znika, gdy nie podwajając go. W przeciwnym razie nie ma to wpływu na prawdziwy wiersz poleceń. Moja implementacja tego nie obsługuje, ponieważ nie znalazłem wzorca w tym zachowaniu. Może ktoś wie o tym więcej.Coś, co nie pasuje do tego wzorca, to następujące polecenie:
cmd /c "argdump.exe "a b c""
cmd
Polecenia wydaje się złapać zewnętrzne cytaty i wziąć resztę dosłownie. Musi być w tym jakiś specjalny magiczny sos.Nie zrobiłem żadnych testów porównawczych dla mojej metody, ale uważam, że jest dość szybka. Nie używa
Regex
i nie wykonuje żadnej konkatenacji ciągów, ale zamiast tego używa aStringBuilder
do zbierania znaków dla argumentu i umieszcza je na liście./// <summary> /// Reads command line arguments from a single string. /// </summary> /// <param name="argsString">The string that contains the entire command line.</param> /// <returns>An array of the parsed arguments.</returns> public string[] ReadArgs(string argsString) { // Collects the split argument strings List<string> args = new List<string>(); // Builds the current argument var currentArg = new StringBuilder(); // Indicates whether the last character was a backslash escape character bool escape = false; // Indicates whether we're in a quoted range bool inQuote = false; // Indicates whether there were quotes in the current arguments bool hadQuote = false; // Remembers the previous character char prevCh = '\0'; // Iterate all characters from the input string for (int i = 0; i < argsString.Length; i++) { char ch = argsString[i]; if (ch == '\\' && !escape) { // Beginning of a backslash-escape sequence escape = true; } else if (ch == '\\' && escape) { // Double backslash, keep one currentArg.Append(ch); escape = false; } else if (ch == '"' && !escape) { // Toggle quoted range inQuote = !inQuote; hadQuote = true; if (inQuote && prevCh == '"') { // Doubled quote within a quoted range is like escaping currentArg.Append(ch); } } else if (ch == '"' && escape) { // Backslash-escaped quote, keep it currentArg.Append(ch); escape = false; } else if (char.IsWhiteSpace(ch) && !inQuote) { if (escape) { // Add pending escape char currentArg.Append('\\'); escape = false; } // Accept empty arguments only if they are quoted if (currentArg.Length > 0 || hadQuote) { args.Add(currentArg.ToString()); } // Reset for next argument currentArg.Clear(); hadQuote = false; } else { if (escape) { // Add pending escape char currentArg.Append('\\'); escape = false; } // Copy character from input, no special meaning currentArg.Append(ch); } prevCh = ch; } // Save last argument if (currentArg.Length > 0 || hadQuote) { args.Add(currentArg.ToString()); } return args.ToArray(); }
źródło
Posługiwać się:
public static string[] SplitArguments(string args) { char[] parmChars = args.ToCharArray(); bool inSingleQuote = false; bool inDoubleQuote = false; bool escaped = false; bool lastSplitted = false; bool justSplitted = false; bool lastQuoted = false; bool justQuoted = false; int i, j; for(i=0, j=0; i<parmChars.Length; i++, j++) { parmChars[j] = parmChars[i]; if(!escaped) { if(parmChars[i] == '^') { escaped = true; j--; } else if(parmChars[i] == '"' && !inSingleQuote) { inDoubleQuote = !inDoubleQuote; parmChars[j] = '\n'; justSplitted = true; justQuoted = true; } else if(parmChars[i] == '\'' && !inDoubleQuote) { inSingleQuote = !inSingleQuote; parmChars[j] = '\n'; justSplitted = true; justQuoted = true; } else if(!inSingleQuote && !inDoubleQuote && parmChars[i] == ' ') { parmChars[j] = '\n'; justSplitted = true; } if(justSplitted && lastSplitted && (!lastQuoted || !justQuoted)) j--; lastSplitted = justSplitted; justSplitted = false; lastQuoted = justQuoted; justQuoted = false; } else { escaped = false; } } if(lastQuoted) j--; return (new string(parmChars, 0, j)).Split(new[] { '\n' }); }
Opierając się na odpowiedzi Vapor in the Alley , ta obsługuje również ucieczki ^.
Przykłady:
Obsługuje również wiele spacji (łamie argumenty tylko raz na blok spacji).
źródło
Ponieważ chciałem zachować takie samo zachowanie jak OP (podzielić ciąg dokładnie tak samo, jak zrobiłoby to cmd systemu Windows), napisałem kilka przypadków testowych i przetestowałem tutaj opublikowane odpowiedzi:
Test( 0, m, "One", new[] { "One" }); Test( 1, m, "One ", new[] { "One" }); Test( 2, m, " One", new[] { "One" }); Test( 3, m, " One ", new[] { "One" }); Test( 4, m, "One Two", new[] { "One", "Two" }); Test( 5, m, "One Two", new[] { "One", "Two" }); Test( 6, m, "One Two", new[] { "One", "Two" }); Test( 7, m, "\"One Two\"", new[] { "One Two" }); Test( 8, m, "One \"Two Three\"", new[] { "One", "Two Three" }); Test( 9, m, "One \"Two Three\" Four", new[] { "One", "Two Three", "Four" }); Test(10, m, "One=\"Two Three\" Four", new[] { "One=Two Three", "Four" }); Test(11, m, "One\"Two Three\" Four", new[] { "OneTwo Three", "Four" }); Test(12, m, "One\"Two Three Four", new[] { "OneTwo Three Four" }); Test(13, m, "\"One Two\"", new[] { "One Two" }); Test(14, m, "One\" \"Two", new[] { "One Two" }); Test(15, m, "\"One\" \"Two\"", new[] { "One", "Two" }); Test(16, m, "One\\\" Two", new[] { "One\"", "Two" }); Test(17, m, "\\\"One\\\" Two", new[] { "\"One\"", "Two" }); Test(18, m, "One\"", new[] { "One" }); Test(19, m, "\"One", new[] { "One" }); Test(20, m, "One \"\"", new[] { "One", "" }); Test(21, m, "One \"", new[] { "One", "" }); Test(22, m, "1 A=\"B C\"=D 2", new[] { "1", "A=B C=D", "2" }); Test(23, m, "1 A=\"B \\\" C\"=D 2", new[] { "1", "A=B \" C=D", "2" }); Test(24, m, "1 \\A 2", new[] { "1", "\\A", "2" }); Test(25, m, "1 \\\" 2", new[] { "1", "\"", "2" }); Test(26, m, "1 \\\\\" 2", new[] { "1", "\\\"", "2" }); Test(27, m, "\"", new[] { "" }); Test(28, m, "\\\"", new[] { "\"" }); Test(29, m, "'A B'", new[] { "'A", "B'" }); Test(30, m, "^", new[] { "^" }); Test(31, m, "^A", new[] { "A" }); Test(32, m, "^^", new[] { "^" }); Test(33, m, "\\^^", new[] { "\\^" }); Test(34, m, "^\\\\", new[] { "\\\\" }); Test(35, m, "^\"A B\"", new[] { "A B" }); // Test cases Anton Test(36, m, @"/src:""C:\tmp\Some Folder\Sub Folder"" /users:""[email protected]"" tasks:""SomeTask,Some Other Task"" -someParam foo", new[] { @"/src:C:\tmp\Some Folder\Sub Folder", @"/users:[email protected]", @"tasks:SomeTask,Some Other Task", @"-someParam", @"foo" }); // Test cases Daniel Earwicker Test(37, m, "", new string[] { }); Test(38, m, "a", new[] { "a" }); Test(39, m, " abc ", new[] { "abc" }); Test(40, m, "a b ", new[] { "a", "b" }); Test(41, m, "a b \"c d\"", new[] { "a", "b", "c d" }); // Test cases Fabio Iotti Test(42, m, "this is a test ", new[] { "this", "is", "a", "test" }); Test(43, m, "this \"is a\" test", new[] { "this", "is a", "test" }); // Test cases Kevin Thach Test(44, m, "\"C:\\Program Files\"", new[] { "C:\\Program Files" }); Test(45, m, "\"He whispered to her \\\"I love you\\\".\"", new[] { "He whispered to her \"I love you\"." });
„oczekiwana” wartość pochodzi z bezpośredniego przetestowania go za pomocą cmd.exe na moim komputerze (Win10 x64) i prostego programu do drukowania:
static void Main(string[] args) => Console.Out.WriteLine($"Count := {args.Length}\n{string.Join("\n", args.Select((v,i) => $"[{i}] => '{v}'"))}");
Oto wyniki:
Solution | Failed Tests ------------------------------|------------------------------------- Atif Aziz (749653) | 2, 3, 10, 11, 12, 14, 16, 17, 18, 26, 28, 31, 32, 33, 34, 35, 36, 37, 39, 45 Jeffrey L Whitledge (298968) | 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 31, 32, 33, 34, 35, 36, 37, 39, 40, 41, 42, 43, 44, 45 Daniel Earwicker (298990) | 10, 11, 12, 14, 16, 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 31, 32, 33, 34, 35, 36, 45 Anton (299795) | 12, 16, 17, 18, 19, 21, 23, 25, 26, 27, 28, 31, 32, 33, 34, 35, 45 CS. (467313) | 12, 18, 19, 21, 27, 31, 32, 33, 34, 35 Vapour in the Alley (2132004) | 10, 11, 12, 14, 16, 17, 20, 21, 22, 23, 25, 26, 27, 28, 29, 31, 32, 33, 34, 35, 36, 45 Monoman (7774211) | 14, 16, 17, 20, 21, 22, 23, 25, 26, 27, 28, 31, 32, 33, 34, 35, 45 Thomas Petersson (19091999) | 2, 3, 10, 11, 12, 14, 16, 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 31, 32, 33, 34, 35, 36, 39, 45 Fabio Iotti (19725880) | 1, 2, 3, 7, 10, 11, 12, 13, 14, 15, 16, 17, 19, 21, 22, 23, 25, 26, 28, 29, 30, 35, 36, 37, 39, 40, 42, 44, 45 ygoe (23961658) | 26, 31, 32, 33, 34, 35 Kevin Thach (24829691) | 10, 11, 12, 14, 18, 19, 20, 21, 22, 23, 26, 27, 31, 32, 33, 34, 35, 36 Lucas De Jesus (31621370) | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45 HarryP (48008872) | 24, 26, 31, 32, 33, 34, 35 TylerY86 (53290784) | 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 31, 32, 33, 34, 35, 36, 41, 43, 44, 45 Louis Somers (55903304) | 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 29, 31, 32, 33, 34, 35, 36, 39, 41, 43, 44, 45 user2126375 (58233585) | 5, 6, 15, 16, 17, 31, 32, 33, 34, 35 DilipNannaware (59131568) | 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 31, 32, 33, 34, 35, 36, 37, 39, 40, 41, 42, 43, 44, 45 Mikescher (this) | -
Ponieważ żadna odpowiedź nie wydawała się poprawna (przynajmniej w oparciu o mój przypadek użycia), oto moje rozwiązanie, obecnie przechodzi wszystkie przypadki testowe (ale jeśli ktoś ma dodatkowe (nieudane) przypadki narożne, proszę o komentarz):
public static IEnumerable<string> SplitArgs(string commandLine) { var result = new StringBuilder(); var quoted = false; var escaped = false; var started = false; var allowcaret = false; for (int i = 0; i < commandLine.Length; i++) { var chr = commandLine[i]; if (chr == '^' && !quoted) { if (allowcaret) { result.Append(chr); started = true; escaped = false; allowcaret = false; } else if (i + 1 < commandLine.Length && commandLine[i + 1] == '^') { allowcaret = true; } else if (i + 1 == commandLine.Length) { result.Append(chr); started = true; escaped = false; } } else if (escaped) { result.Append(chr); started = true; escaped = false; } else if (chr == '"') { quoted = !quoted; started = true; } else if (chr == '\\' && i + 1 < commandLine.Length && commandLine[i + 1] == '"') { escaped = true; } else if (chr == ' ' && !quoted) { if (started) yield return result.ToString(); result.Clear(); started = false; } else { result.Append(chr); started = true; } } if (started) yield return result.ToString(); }
Kod, którego użyłem do wygenerowania wyników testu, można znaleźć tutaj
źródło
Obecnie jest to kod, który mam:
private String[] SplitCommandLineArgument(String argumentString) { StringBuilder translatedArguments = new StringBuilder(argumentString); bool escaped = false; for (int i = 0; i < translatedArguments.Length; i++) { if (translatedArguments[i] == '"') { escaped = !escaped; } if (translatedArguments[i] == ' ' && !escaped) { translatedArguments[i] = '\n'; } } string[] toReturn = translatedArguments.ToString().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); for(int i = 0; i < toReturn.Length; i++) { toReturn[i] = RemoveMatchingQuotes(toReturn[i]); } return toReturn; } public static string RemoveMatchingQuotes(string stringToTrim) { int firstQuoteIndex = stringToTrim.IndexOf('"'); int lastQuoteIndex = stringToTrim.LastIndexOf('"'); while (firstQuoteIndex != lastQuoteIndex) { stringToTrim = stringToTrim.Remove(firstQuoteIndex, 1); stringToTrim = stringToTrim.Remove(lastQuoteIndex - 1, 1); //-1 because we've shifted the indicies left by one firstQuoteIndex = stringToTrim.IndexOf('"'); lastQuoteIndex = stringToTrim.LastIndexOf('"'); } return stringToTrim; }
Nie działa z cudzysłowami, ale działa w przypadkach, z którymi się do tej pory spotkałem.
źródło
To jest odpowiedź na kod Antona, który nie działa z cudzysłowami. Zmodyfikowałem 3 miejsca.
public static string[] SplitCommandLineArgument( String argumentString ) { StringBuilder translatedArguments = new StringBuilder( argumentString ).Replace( "\\\"", "\r" ); bool InsideQuote = false; for ( int i = 0; i < translatedArguments.Length; i++ ) { if ( translatedArguments[i] == '"' ) { InsideQuote = !InsideQuote; } if ( translatedArguments[i] == ' ' && !InsideQuote ) { translatedArguments[i] = '\n'; } } string[] toReturn = translatedArguments.ToString().Split( new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries ); for ( int i = 0; i < toReturn.Length; i++ ) { toReturn[i] = RemoveMatchingQuotes( toReturn[i] ); toReturn[i] = toReturn[i].Replace( "\r", "\"" ); } return toReturn; } public static string RemoveMatchingQuotes( string stringToTrim ) { int firstQuoteIndex = stringToTrim.IndexOf( '"' ); int lastQuoteIndex = stringToTrim.LastIndexOf( '"' ); while ( firstQuoteIndex != lastQuoteIndex ) { stringToTrim = stringToTrim.Remove( firstQuoteIndex, 1 ); stringToTrim = stringToTrim.Remove( lastQuoteIndex - 1, 1 ); //-1 because we've shifted the indicies left by one firstQuoteIndex = stringToTrim.IndexOf( '"' ); lastQuoteIndex = stringToTrim.LastIndexOf( '"' ); } return stringToTrim; }
źródło
Myślę, że nie ma pojedynczych cudzysłowów lub ^ cudzysłowów dla aplikacji C #. Następująca funkcja działa dobrze dla mnie:
public static IEnumerable<String> SplitArguments(string commandLine) { Char quoteChar = '"'; Char escapeChar = '\\'; Boolean insideQuote = false; Boolean insideEscape = false; StringBuilder currentArg = new StringBuilder(); // needed to keep "" as argument but drop whitespaces between arguments Int32 currentArgCharCount = 0; for (Int32 i = 0; i < commandLine.Length; i++) { Char c = commandLine[i]; if (c == quoteChar) { currentArgCharCount++; if (insideEscape) { currentArg.Append(c); // found \" -> add " to arg insideEscape = false; } else if (insideQuote) { insideQuote = false; // quote ended } else { insideQuote = true; // quote started } } else if (c == escapeChar) { currentArgCharCount++; if (insideEscape) // found \\ -> add \\ (only \" will be ") currentArg.Append(escapeChar + escapeChar); insideEscape = !insideEscape; } else if (Char.IsWhiteSpace(c)) { if (insideQuote) { currentArgCharCount++; currentArg.Append(c); // append whitespace inside quote } else { if (currentArgCharCount > 0) yield return currentArg.ToString(); currentArgCharCount = 0; currentArg.Clear(); } } else { currentArgCharCount++; if (insideEscape) { // found non-escaping backslash -> add \ (only \" will be ") currentArg.Append(escapeChar); currentArgCharCount = 0; insideEscape = false; } currentArg.Append(c); } } if (currentArgCharCount > 0) yield return currentArg.ToString(); }
źródło
Możesz rzucić okiem na kod, który opublikowałem wczoraj:
[C #] Ścieżka i ciągi argumentów
Dzieli nazwę pliku + argumenty na łańcuch []. Obsługiwane są krótkie ścieżki, zmienne środowiskowe i brakujące rozszerzenia plików.
(Początkowo było to dla UninstallString in Registry.)
źródło
Wypróbuj ten kod:
string[] str_para_linha_comando(string str, out int argumentos) { string[] linhaComando = new string[32]; bool entre_aspas = false; int posicao_ponteiro = 0; int argc = 0; int inicio = 0; int fim = 0; string sub; for(int i = 0; i < str.Length;) { if (entre_aspas) { // Está entre aspas sub = str.Substring(inicio+1, fim - (inicio+1)); linhaComando[argc - 1] = sub; posicao_ponteiro += ((fim - posicao_ponteiro)+1); entre_aspas = false; i = posicao_ponteiro; } else { tratar_aspas: if (str.ElementAt(i) == '\"') { inicio = i; fim = str.IndexOf('\"', inicio + 1); entre_aspas = true; argc++; } else { // Se não for aspas, então ler até achar o primeiro espaço em branco if (str.ElementAt(i) == ' ') { if (str.ElementAt(i + 1) == '\"') { i++; goto tratar_aspas; } // Pular os espaços em branco adiconais while(str.ElementAt(i) == ' ') i++; argc++; inicio = i; fim = str.IndexOf(' ', inicio); if (fim == -1) fim = str.Length; sub = str.Substring(inicio, fim - inicio); linhaComando[argc - 1] = sub; posicao_ponteiro += (fim - posicao_ponteiro); i = posicao_ponteiro; if (posicao_ponteiro == str.Length) break; } else { argc++; inicio = i; fim = str.IndexOf(' ', inicio); if (fim == -1) fim = str.Length; sub = str.Substring(inicio, fim - inicio); linhaComando[argc - 1] = sub; posicao_ponteiro += fim - posicao_ponteiro; i = posicao_ponteiro; if (posicao_ponteiro == str.Length) break; } } } } argumentos = argc; return linhaComando; }
Jest napisany w języku portugalskim.
źródło
posicao_ponteiro += ((fim - posicao_ponteiro)+1);
.Oto jedna linijka, która wykonuje zadanie (zobacz jedną linię, która wykonuje całą pracę wewnątrz metody BurstCmdLineArgs (...)).
Nie to, co nazwałbym najbardziej czytelną linią kodu, ale możesz ją podzielić ze względu na czytelność. Celowo jest prosty i nie działa dobrze dla wszystkich przypadków argumentów (takich jak argumenty nazw plików, które zawierają separator znaków podzielonego łańcucha).
To rozwiązanie sprawdziło się w moich rozwiązaniach, które z niego korzystają. Jak powiedziałem, wykonuje swoją pracę bez szczurzego kodu, aby obsłużyć każdy możliwy format argumentu n-silni.
using System; using System.Collections.Generic; using System.Linq; namespace CmdArgProcessor { class Program { static void Main(string[] args) { // test switches and switches with values // -test1 1 -test2 2 -test3 -test4 -test5 5 string dummyString = string.Empty; var argDict = BurstCmdLineArgs(args); Console.WriteLine("Value for switch = -test1: {0}", argDict["test1"]); Console.WriteLine("Value for switch = -test2: {0}", argDict["test2"]); Console.WriteLine("Switch -test3 is present? {0}", argDict.TryGetValue("test3", out dummyString)); Console.WriteLine("Switch -test4 is present? {0}", argDict.TryGetValue("test4", out dummyString)); Console.WriteLine("Value for switch = -test5: {0}", argDict["test5"]); // Console output: // // Value for switch = -test1: 1 // Value for switch = -test2: 2 // Switch -test3 is present? True // Switch -test4 is present? True // Value for switch = -test5: 5 } public static Dictionary<string, string> BurstCmdLineArgs(string[] args) { var argDict = new Dictionary<string, string>(); // Flatten the args in to a single string separated by a space. // Then split the args on the dash delimiter of a cmd line "switch". // E.g. -mySwitch myValue // or -JustMySwitch (no value) // where: all values must follow a switch. // Then loop through each string returned by the split operation. // If the string can be split again by a space character, // then the second string is a value to be paired with a switch, // otherwise, only the switch is added as a key with an empty string as the value. // Use dictionary indexer to retrieve values for cmd line switches. // Use Dictionary::ContainsKey(...) where only a switch is recorded as the key. string.Join(" ", args).Split('-').ToList().ForEach(s => argDict.Add(s.Split()[0], (s.Split().Count() > 1 ? s.Split()[1] : ""))); return argDict; } } }
źródło
Nie mogę znaleźć niczego, co mi się podobało. Nienawidzę zepsuć stosu magią wydajności dla małej linii poleceń (gdyby był to strumień terabajta, byłaby to inna historia).
Oto moje podejście, obsługuje ucieczki cytatów z podwójnymi cudzysłowami, takimi jak te:
wynik:
public static string[] SplitArguments(string commandLine) { List<string> args = new List<string>(); List<char> currentArg = new List<char>(); char? quoteSection = null; // Keeps track of a quoted section (and the type of quote that was used to open it) char[] quoteChars = new[] {'\'', '\"'}; char previous = ' '; // Used for escaping double quotes for (var index = 0; index < commandLine.Length; index++) { char c = commandLine[index]; if (quoteChars.Contains(c)) { if (previous == c) // Escape sequence detected { previous = ' '; // Prevent re-escaping if (!quoteSection.HasValue) { quoteSection = c; // oops, we ended the quoted section prematurely continue; // don't add the 2nd quote (un-escape) } if (quoteSection.Value == c) quoteSection = null; // appears to be an empty string (not an escape sequence) } else if (quoteSection.HasValue) { if (quoteSection == c) quoteSection = null; // End quoted section } else quoteSection = c; // Start quoted section } else if (char.IsWhiteSpace(c)) { if (!quoteSection.HasValue) { args.Add(new string(currentArg.ToArray())); currentArg.Clear(); previous = c; continue; } } currentArg.Add(c); previous = c; } if (currentArg.Count > 0) args.Add(new string(currentArg.ToArray())); return args.ToArray(); }
źródło
Zaimplementowałem maszynę stanu, aby mieć takie same wyniki parsera, jak gdyby argumenty były przekazywane do aplikacji .NET i przetwarzane w
static void Main(string[] args)
metodzie.public static IList<string> ParseCommandLineArgsString(string commandLineArgsString) { List<string> args = new List<string>(); commandLineArgsString = commandLineArgsString.Trim(); if (commandLineArgsString.Length == 0) return args; int index = 0; while (index != commandLineArgsString.Length) { args.Add(ReadOneArgFromCommandLineArgsString(commandLineArgsString, ref index)); } return args; } private static string ReadOneArgFromCommandLineArgsString(string line, ref int index) { if (index >= line.Length) return string.Empty; var sb = new StringBuilder(512); int state = 0; while (true) { char c = line[index]; index++; switch (state) { case 0: //string outside quotation marks if (c == '\\') //possible escaping character for quotation mark otherwise normal character { state = 1; } else if (c == '"') //opening quotation mark for string between quotation marks { state = 2; } else if (c == ' ') //closing arg { return sb.ToString(); } else { sb.Append(c); } break; case 1: //possible escaping \ for quotation mark or normal character if (c == '"') //If escaping quotation mark only quotation mark is added into result { state = 0; sb.Append(c); } else // \ works as not-special character { state = 0; sb.Append('\\'); index--; } break; case 2: //string between quotation marks if (c == '"') //quotation mark in string between quotation marks can be escape mark for following quotation mark or can be ending quotation mark for string between quotation marks { state = 3; } else if (c == '\\') //escaping \ for possible following quotation mark otherwise normal character { state = 4; } else //text in quotation marks { sb.Append(c); } break; case 3: //quotation mark in string between quotation marks if (c == '"') //Quotation mark after quotation mark - that means that this one is escaped and can added into result and we will stay in string between quotation marks state { state = 2; sb.Append(c); } else //we had two consecutive quotation marks - this means empty string but the following chars (until space) will be part of same arg result as well { state = 0; index--; } break; case 4: //possible escaping \ for quotation mark or normal character in string between quotation marks if (c == '"') //If escaping quotation mark only quotation mark added into result { state = 2; sb.Append(c); } else { state = 2; sb.Append('\\'); index--; } break; } if (index == line.Length) return sb.ToString(); } }
źródło
Oto rozwiązanie, które traktuje spacje (pojedyncze lub wielokrotne spacje) jako separator parametrów wiersza poleceń i zwraca rzeczywiste argumenty wiersza poleceń:
static string[] ParseMultiSpacedArguments(string commandLine) { var isLastCharSpace = false; char[] parmChars = commandLine.ToCharArray(); bool inQuote = false; for (int index = 0; index < parmChars.Length; index++) { if (parmChars[index] == '"') inQuote = !inQuote; if (!inQuote && parmChars[index] == ' ' && !isLastCharSpace) parmChars[index] = '\n'; isLastCharSpace = parmChars[index] == '\n' || parmChars[index] == ' '; } return (new string(parmChars)).Split('\n'); }
źródło
Istnieje pakiet NuGet, który zawiera dokładnie te funkcje, których potrzebujesz:
Microsoft.CodeAnalysis.Common zawiera klasę CommandLineParser z metodą SplitCommandLineIntoArguments .
Używasz tego w ten sposób:
using Microsoft.CodeAnalysis; // [...] var cli = @"/src:""C:\tmp\Some Folder\Sub Folder"" /users:""[email protected]"" tasks:""SomeTask,Some Other Task"" -someParam foo"; var cliArgs = CommandLineParser.SplitCommandLineIntoArguments(cli, true); Console.WriteLine(string.Join('\n', cliArgs)); // prints out: // /src:"C:\tmp\Some Folder\Sub Folder" // /users:"[email protected]" // tasks:"SomeTask,Some Other Task" // -someParam // foo
źródło
Nie jestem pewien, czy cię zrozumiałem, ale czy problem polega na tym, że znak używany jako rozdzielacz występuje również w tekście? (Z wyjątkiem tego, że jest poprzedzony podwójnym „?)
Jeśli tak, utworzyłbym
for
pętlę i zamieniłbym wszystkie wystąpienia, w których występuje <"> na <|> (lub inny" bezpieczny "znak, ale upewnij się, że zastępuje on tylko <">, a nie <"">Po iteracji łańcucha, zrobiłbym tak, jak wcześniej napisałem, podzielić ciąg, ale teraz na znaku <|>.
źródło
Tak, obiekt typu string ma wbudowaną funkcję o nazwie,
Split()
która przyjmuje pojedynczy parametr określający znak do wyszukania jako separator i zwraca tablicę ciągów (ciąg []) z poszczególnymi wartościami w niej zawartymi.źródło