Podziel ciąg zawierający parametry wiersza polecenia na ciąg [] w C #

91

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”)

Anton
źródło
5
Za każdym razem, gdy ktoś odpowiada, wydaje się, że masz sprzeciw oparty na materiale, którego nie ma w Twoim poście. Proponuję zaktualizować swój post tym materiałem. Możesz uzyskać lepsze odpowiedzi.
tvanfosson
1
Dobre pytanie, szukam tego samego. Miałem nadzieję, że ktoś powie "hej .net ujawnia to tutaj ..." :) Jeśli kiedyś to napotkam, opublikuję to tutaj, mimo że ma około 6 lat. Wciąż aktualne pytanie!
MikeJansen
Utworzyłem czysto zarządzaną wersję w odpowiedzi poniżej, ponieważ potrzebowałem również tej funkcji.
ygoe

Odpowiedzi:

75

Oprócz dobrego i czystego rozwiązania zarządzanego przez Earwicker , warto wspomnieć, dla kompletności, że Windows udostępnia również CommandLineToArgvWfunkcję dzielenia łańcucha na tablicę ciągów:

LPWSTR *CommandLineToArgvW(
    LPCWSTR lpCmdLine, int *pNumArgs);

Analizuje ciąg znaków wiersza poleceń Unicode i zwraca tablicę wskaźników do argumentów wiersza poleceń, wraz z liczbą takich argumentów, w sposób podobny do standardowych wartości argv i argc w czasie wykonywania języka C.

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);
    }
}
Atif Aziz
źródło
1
Ta funkcja wymaga, abyś uniknął końcowego ukośnika odwrotnego ścieżki wewnątrz cudzysłowów. „C: \ Program Files \” musi mieć wartość „C: \ Program Files \\”, aby ta funkcja działała i poprawnie przeanalizowała ciąg.
Magnus Lindhe,
8
Warto również zauważyć, że CommandLineArgvW oczekuje, że pierwszym argumentem będzie nazwa programu, a zastosowana magia parsowania nie jest taka sama, jeśli nie zostanie przekazana. Możesz to sfałszować za pomocą czegoś takiego:CommandLineToArgs("foo.exe " + commandLine).Skip(1).ToArray();
Scott Wegner
4
Ze względu na kompletność MSVCRT nie używa CommandLineToArgvW () do konwersji wiersza poleceń na argc / argv. Używa własnego kodu, który jest inny. Na przykład spróbuj wywołać CreateProcess z tym ciągiem znaków: a "b c" def. W main () dostaniesz 3 argumenty (jak udokumentowano w MSDN), ale kombi CommandLineToArgvW () / GetCommandLineW () da ci 2.
LRN
7
OMG, to jest taki bałagan. typowa zupa MS. nic nie jest kanonizowane i nigdy KISS nie jest szanowany w świecie MS.
v.oddou,
1
Opublikowałem wieloplatformową wersję implementacji MSVCRT przetłumaczonej przez Microsoft i przybliżenie o wysokiej dokładności przy użyciu Regex. Wiem, że to stare, ale hej - żadnych zwojów ciała.
TylerY86
101

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ń.

Daniel Earwicker
źródło
Musiałem odznaczyć to jako odpowiedź, ponieważ nie miałem odpowiednich oczekiwanych wyników. Rzeczywiste dane wyjściowe nie powinny mieć znaku „w ostatecznej tablicy
Anton
16
Przychodzę do Stack Overflow, aby uciec od wymagań, które ciągle się zmieniają! :) Możesz użyć Replace ("\" "," ") zamiast TrimMatchingQuotes (), aby pozbyć się wszystkich cudzysłowów, ale system Windows obsługuje \", aby umożliwić przepuszczanie znaku cudzysłowu. Moja funkcja Split nie może tego zrobić.
Daniel Earwicker,
1
Fajny Earwicker :) Anton: To jest rozwiązanie, które starałem się wam opisać we wcześniejszym poście, ale Earwicker spisał się dużo lepiej;) I też bardzo go rozszerzyłem;)
Israr Khan
biały znak nie jest jedynym znakiem oddzielającym argumenty wiersza poleceń, prawda?
Louis Rhys,
@Louis Rhys - nie jestem pewien. Jeśli jest to problem, można go łatwo rozwiązać: użyj char.IsWhiteSpacezamiast== ' '
Daniel Earwicker
25

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');
    }
Jeffrey L Whitledge
źródło
2
Skończyło się na tym samym, excepy użyłem .Split (new char [] {'\ n'}, StringSplitOptions.RemoveEmptyEntries) w ostatnim wierszu na wypadek, gdyby między parametrami były dodatkowe ''. Wydaje się, że działa.
Anton
3
Zakładam, że Windows musi mieć sposób na uniknięcie cudzysłowów w parametrach ... ten algorytm nie bierze tego pod uwagę.
rmeador
Usuwanie pustych wierszy, usuwanie zewnętrznych cudzysłowów i obsługa cudzysłowów pozostawiamy jako excersize dla czytelnika.
Jeffrey L Whitledge
Char.IsWhiteSpace () może tu pomóc
Sam Mackrill
To rozwiązanie jest dobre, jeśli argumenty są oddzielone pojedynczym odstępem, ale nie powiedzie się, jeśli argumenty są oddzielone wieloma spacjami. Link do prawidłowego rozwiązania: stackoverflow.com/a/59131568/3926504
Dilip Nannaware
13

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);
    }
oparów w alei
źródło
7

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.

Kevina Thacha
źródło
3
Przepraszamy za nekromancję, ale to rozwiązanie nadal pomija takie rzeczy, jak to, bla.exe aAAA"b\"ASDS\"c"dSADSDco powoduje, aAAAb"ASDS"cdSADSDże to rozwiązanie mogłoby się pojawić aAAA"b"ASDS"c"dSADSD. Mogę rozważyć zmianę na TrimMatchingQuotesa Regex("(?<!\\\\)\\\"")i używać go w ten sposób .
Scis
4

Environment.GetCommandLineArgs ()

Mark Cidade
źródło
2
Przydatne - ale dostaniesz tylko argumenty wiersza poleceń wysłane do bieżącego procesu. Wymaganiem było pobranie ciągu [] z ciągu „w taki sam sposób, jak zrobiłby to C #, gdyby polecenia zostały określone w wierszu polecenia”. Myślę, że moglibyśmy użyć dekompilatora, aby zobaczyć, jak MS to zaimplementowało ...
rohancragg
Jak odkrył również Jon Galloway ( weblogs.asp.net/jgalloway/archive/2006/09/13/… ), dekompilator niewiele pomaga, co sprowadza nas z powrotem do odpowiedzi Atif ( stackoverflow.com/questions/298830/ ... )
rohancragg
4

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 rozszerzenia string):

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();
}
Monoman
źródło
3

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!

Thomas Petersson
źródło
3

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.

TylerY86
źródło
1
Tak, wygląda na to, że wersja C # jest martwa. github.com/dotnet/runtime/blob/master/src/coreclr/src/utilcode/…
TylerY86
1
Odrodzenie ograniczone czasowo. pastebin.com/ajhrBS4t
TylerY86
2

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.

Zachary Yates
źródło
Próbowałem użyć reflektora do biblioteki C #, ale przechodziło to do natywnego wywołania C ++, dla którego nie mam kodu i nie widzę żadnego sposobu wywołania bez wywołania p. Nie chcę też biblioteki analizującej wiersz poleceń, chcę tylko ciąg [].
Anton
Refleksja .NET również nie przyniosła mi niczego. Spojrzenie na kod źródłowy Mono zasugerowało, że ten podział argumentów nie jest wykonywany przez środowisko CLR, ale raczej pochodzi z systemu operacyjnego. Pomyśl o parametrach argc, argv głównej funkcji C. Nie ma więc nic do ponownego wykorzystania poza interfejsem API systemu operacyjnego.
ygoe
1

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[] argsparametr, 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, hadQuotea prevCh, a wyjście jest pobierane z currentArgi args.

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""

cmdPolecenia 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 Regexi nie wykonuje żadnej konkatenacji ciągów, ale zamiast tego używa a StringBuilderdo 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();
}
ygoe
źródło
1

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:

  • to jest test
    • to
    • jest
    • za
    • test
  • to jest test
    • to
    • jest
    • test
  • ten ^ "to test ^"
    • to
    • "jest
    • za"
    • test
  • ten "" "to ^^ test"
    • to
    • Wcześniejsze
    • jest testem ^

Obsługuje również wiele spacji (łamie argumenty tylko raz na blok spacji).

Fabio Iotti
źródło
Ostatni z trzech w jakiś sposób koliduje z Markdown i nie jest renderowany zgodnie z przeznaczeniem.
Peter Mortensen,
Naprawiono spacją o zerowej szerokości.
Fabio Iotti
1

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

Mikescher
źródło
0

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.

Anton
źródło
0

To jest odpowiedź na kod Antona, który nie działa z cudzysłowami. Zmodyfikowałem 3 miejsca.

  1. Konstruktora dla StringBuilder w SplitCommandLineArguments , wymianą \” z \ r
  2. W pętli for w SplitCommandLineArguments zamieniam teraz znak \ r z powrotem na \ " .
  3. Zmieniono metodę SplitCommandLineArgument z prywatnej na publiczną statyczną .

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;
}
CS.
źródło
Zajmuję się tym samym problemem, można by pomyśleć, że w dzisiejszych czasach istnieje proste rozwiązanie dla ciągów argumentów wiersza poleceń testowania jednostkowego. Chcę tylko mieć pewność co do zachowania wynikającego z danego ciągu argumentu wiersza polecenia. Na razie rezygnuję i utworzę testy jednostkowe dla string [], ale mogę dodać kilka testów integracyjnych, aby to załatwić.
Charlie Barker
0

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();
}
Harry P.
źródło
0

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.)

Nolmë Informatique
źródło
0

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.

Lucas De Jesus
źródło
raczej dokumentacja jest portugalska
Enamul Hassan
@EnamulHassan Powiedziałbym, że kod jest również w języku portugalskim, np posicao_ponteiro += ((fim - posicao_ponteiro)+1);.
MEMark
0

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;
        }
    }
}
Vance McCorkle
źródło
0

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:

param = "a 15" "ekran nie jest zły" param2 = 'a 15 "ekran nie jest zły' param3 =" "param4 = / param5

wynik:

param = "ekran 15" nie jest zły "

param2 = '15 "ekran nie jest zły'

param3 = „”

param4 =

/ param5

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();
}
Louis Somers
źródło
0

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();
        }
    }
user2126375
źródło
0

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');
}
Dilip Nannaware
źródło
0

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
Robin Hartmann
źródło
-2

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 forpę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 <|>.

Israr Khan
źródło
Podwójne „” są, ponieważ jest to literał a @ "..", Podwójne „znajduje się wewnątrz ciągu @" .. "jest odpowiednikiem znaku \ escaped” w normalnym ciągu
Anton
"jedynym ograniczeniem (wierzę) jest to, że łańcuchy są rozdzielane spacjami, chyba że spacja występuje w" ... "bloku" -> Może strzelać do ptaka z bazooki, ale wstawiamy wartość logiczną, która brzmi "prawda" gdy wewnątrz cudzysłowu i jeśli w środku zostanie wykryta spacja, a „prawda”, kontynuuj, w przeciwnym razie <> = <|>
Israr Khan
-6

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.

Charles Bretana
źródło
1
Spowodowałoby to niepoprawne podzielenie części src: „C: \ tmp \ Some Folder \ Sub Folder”.
Anton
A co z cudzysłowami wewnątrz łańcucha, które tymczasowo wyłączają podział na spacje?
Daniel Earwicker,