Teilen Sie eine Zeichenfolge mit Befehlszeilenparametern in Zeichenfolge [] in C # auf

88

Ich habe eine einzelne Zeichenfolge, die die Befehlszeilenparameter enthält, die an eine andere ausführbare Datei übergeben werden sollen, und ich muss die Zeichenfolge [], die die einzelnen Parameter enthält, auf dieselbe Weise extrahieren wie C #, wenn die Befehle in der Befehlszeile angegeben worden wären. Die Zeichenfolge [] wird verwendet, wenn ein anderer Assembly-Einstiegspunkt über Reflektion ausgeführt wird.

Gibt es dafür eine Standardfunktion? Oder gibt es eine bevorzugte Methode (Regex?), Um die Parameter korrekt aufzuteilen? Es muss '' 'begrenzte Zeichenfolgen verarbeiten, die möglicherweise Leerzeichen enthalten, damit ich nicht einfach auf' 'teilen kann.

Beispielzeichenfolge:

string parameterString = @"/src:""C:\tmp\Some Folder\Sub Folder"" /users:""[email protected]"" tasks:""SomeTask,Some Other Task"" -someParam foo";

Beispielergebnis:

string[] parameterArray = new string[] { 
  @"/src:C:\tmp\Some Folder\Sub Folder",
  @"/users:[email protected]",
  @"tasks:SomeTask,Some Other Task",
  @"-someParam",
  @"foo"
};

Ich benötige keine Befehlszeilen-Analysebibliothek, nur eine Möglichkeit, den zu generierenden String [] abzurufen.

Update : Ich musste das erwartete Ergebnis so ändern, dass es mit dem übereinstimmt, was tatsächlich von C # generiert wird (die zusätzlichen "'s in den geteilten Zeichenfolgen wurden entfernt).

Anton
quelle
5
Jedes Mal, wenn jemand antwortet, scheinen Sie einen Einwand zu haben, der auf Material basiert, das nicht in Ihrem Beitrag enthalten ist. Ich schlage vor, dass Sie Ihren Beitrag mit diesem Material aktualisieren. Möglicherweise erhalten Sie bessere Antworten.
Tvanfosson
1
Gute Frage, auf der Suche nach dem gleichen. Ich hatte gehofft, jemanden zu finden, der sagt "Hey .net macht das hier sichtbar ..." :) Wenn ich irgendwann darauf stoße, werde ich es hier posten, obwohl es ungefähr 6 Jahre alt ist. Immer noch eine gültige Frage!
MikeJansen
Ich habe in einer Antwort unten eine rein verwaltete Version erstellt, da ich diese Funktion ebenfalls benötigte.
Ygoe

Antworten:

74

Neben der guten und rein verwalteten Lösung von Earwicker kann der Vollständigkeit halber erwähnt werden, dass Windows auch die CommandLineToArgvWFunktion zum Aufteilen einer Zeichenfolge in eine Reihe von Zeichenfolgen bietet :

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

Analysiert eine Unicode-Befehlszeilenzeichenfolge und gibt ein Array von Zeigern auf die Befehlszeilenargumente zusammen mit einer Anzahl solcher Argumente auf eine Weise zurück, die den Standardwerten für C-Laufzeit argv und argc ähnlich ist.

Ein Beispiel für das Aufrufen dieser API aus C # und das Entpacken des resultierenden Zeichenfolgenarrays in verwaltetem Code finden Sie unter " Konvertieren der Befehlszeilenzeichenfolge in Args [] mithilfe der CommandLineToArgvW () - API ". Unten finden Sie eine etwas einfachere Version desselben Codes:

[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
quelle
1
Diese Funktion erfordert, dass Sie dem abschließenden Backslash eines Pfads in Anführungszeichen entkommen. "C: \ Programme \" muss "C: \ Programme \\" sein, damit dies funktioniert, um die Zeichenfolge korrekt zu analysieren.
Magnus Lindhe
8
Es ist auch erwähnenswert, dass CommandLineArgvW erwartet, dass das erste Argument der Programmname ist, und dass die angewandte Parsing-Magie nicht ganz dieselbe ist, wenn man sie nicht übergibt. Sie können sie mit etwas fälschen wie:CommandLineToArgs("foo.exe " + commandLine).Skip(1).ToArray();
Scott Wegner
4
Der Vollständigkeit halber verwendet MSVCRT CommandLineToArgvW () nicht, um die Befehlszeile in argc / argv zu konvertieren. Es verwendet einen eigenen Code, der anders ist. Versuchen Sie beispielsweise, CreateProcess mit der folgenden Zeichenfolge aufzurufen: a "b c" def. In main () würden Sie 3 Argumente erhalten (wie in MSDN dokumentiert), aber die Kombination CommandLineToArgvW () / GetCommandLineW () gibt Ihnen 2.
LRN
7
Oh mein Gott, das ist so ein Durcheinander. typische MS-Suppe. Nichts wird kanonisiert und niemals wird KISS in der MS-Welt respektiert.
v.oddou
1
Ich habe eine plattformübergreifende Version der von Microsoft übersetzten MSVCRT-Implementierung und eine hochgenaue Approximation mit Regex veröffentlicht. Ich weiß, das ist alt, aber hey - keine Körperrollen.
TylerY86
100

Es ärgert mich, dass es keine Funktion gibt, eine Zeichenfolge basierend auf einer Funktion zu teilen, die jedes Zeichen untersucht. Wenn ja, könnten Sie es so schreiben:

    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));
    }

Obwohl Sie das geschrieben haben, warum nicht die notwendigen Erweiterungsmethoden schreiben? Okay, du hast mich dazu überredet ...

Erstens meine eigene Version von Split, die eine Funktion übernimmt, die entscheiden muss, ob das angegebene Zeichen die Zeichenfolge teilen soll:

    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);
    }

Abhängig von der Situation kann es zu leeren Zeichenfolgen kommen, aber in anderen Fällen sind diese Informationen möglicherweise hilfreich, sodass ich die leeren Einträge in dieser Funktion nicht entferne.

Zweitens (und allgemeiner) ein kleiner Helfer, der ein passendes Zitatpaar vom Anfang und Ende einer Zeichenfolge abschneidet. Es ist pingeliger als die Standard-Trimmmethode - es schneidet nur ein Zeichen von jedem Ende und nicht nur von einem Ende:

    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;
    }

Und ich nehme an, Sie möchten auch einige Tests. Na gut dann. Aber das muss absolut das Letzte sein! Zuerst eine Hilfsfunktion, die das Ergebnis der Aufteilung mit dem erwarteten Array-Inhalt vergleicht:

    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]);
    }

Dann kann ich solche Tests schreiben:

        Test("");
        Test("a", "a");
        Test(" abc ", "abc");
        Test("a b ", "a", "b");
        Test("a b \"c d\"", "a", "b", "c d");

Hier ist der Test für Ihre Anforderungen:

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

Beachten Sie, dass die Implementierung die zusätzliche Funktion hat, Anführungszeichen um ein Argument zu entfernen, wenn dies sinnvoll ist (dank der TrimMatchingQuotes-Funktion). Ich glaube, das ist Teil der normalen Befehlszeileninterpretation.

Daniel Earwicker
quelle
Ich musste dies als Antwort deaktivieren, da ich nicht die richtigen erwarteten Ergebnisse hatte. Die tatsächliche Ausgabe sollte nicht die "'s im endgültigen Array haben
Anton
16
Ich komme zu Stack Overflow, um mich von Anforderungen zu lösen, die sich ständig ändern! :) Sie können anstelle von TrimMatchingQuotes () Ersetzen ("\" "," ") verwenden, um alle Anführungszeichen zu entfernen. Windows unterstützt jedoch \", damit ein Anführungszeichen durchgelassen werden kann. Meine Split-Funktion kann das nicht.
Daniel Earwicker
1
Netter Earwicker :) Anton: Dies ist die Lösung, die ich Ihnen in meinem früheren Beitrag beschreiben wollte, aber Earwicker hat es viel besser aufgeschrieben;) Und es auch viel erweitert;)
Israr Khan
Ein Leerzeichen ist nicht das einzige Trennzeichen für Befehlszeilenargumente, oder?
Louis Rhys
@ Louis Rhys - Ich bin nicht sicher. Wenn das ein char.IsWhiteSpace== ' '
Problem
25

Der Windows-Befehlszeilenparser verhält sich genau so, wie Sie es sagen. Er wird auf den Speicherplatz aufgeteilt, es sei denn, davor steht ein nicht geschlossenes Anführungszeichen. Ich würde empfehlen, den Parser selbst zu schreiben. So etwas vielleicht:

    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
quelle
2
Am Ende hatte ich dasselbe, außer ich habe .Split (neues Zeichen [] {'\ n'}, StringSplitOptions.RemoveEmptyEntries) in der letzten Zeile verwendet, falls es zwischen den Parametern zusätzliche '' gab. Scheint zu funktionieren.
Anton
3
Ich gehe davon aus, dass Windows eine Möglichkeit haben muss, Anführungszeichen in den Parametern zu umgehen ... dieser Algorithmus berücksichtigt dies nicht.
Rmeador
Das Entfernen von Leerzeilen, das Entfernen von externen Anführungszeichen und das Behandeln von maskierten Anführungszeichen bleibt dem Leser als Übermaß.
Jeffrey L Whitledge
Char.IsWhiteSpace () könnte hier helfen
Sam Mackrill
Diese Lösung ist gut, wenn Argumente durch ein Leerzeichen getrennt sind, aber wenn Argumente durch mehrere Leerzeichen getrennt sind. Link zur richtigen Lösung: stackoverflow.com/a/59131568/3926504
Dilip Nannaware
13

Ich nahm die Antwort von Jeffrey L Whitledge und verbesserte sie ein wenig.

Es werden jetzt sowohl einfache als auch doppelte Anführungszeichen unterstützt. Sie können Anführungszeichen in den Parametern selbst verwenden, indem Sie andere typisierte Anführungszeichen verwenden.

Außerdem werden die Anführungszeichen aus den Argumenten entfernt, da diese nicht zur Argumentinformation beitragen.

    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);
    }
Dampf in der Gasse
quelle
7

Die gute und rein verwaltete Lösung von Earwicker konnte solche Argumente nicht verarbeiten:

Test("\"He whispered to her \\\"I love you\\\".\"", "He whispered to her \"I love you\".");

Es wurden 3 Elemente zurückgegeben:

"He whispered to her \"I
love
you\"."

Hier ist ein Fix, um das "zitierte" Escape "-Zitat zu unterstützen:

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));
}

Getestet mit 2 zusätzlichen Fällen:

Test("\"C:\\Program Files\"", "C:\\Program Files");
Test("\"He whispered to her \\\"I love you\\\".\"", "He whispered to her \"I love you\".");

Es wurde auch darauf hingewiesen, dass die akzeptierte Antwort von Atif Aziz, die CommandLineToArgvW verwendet, ebenfalls fehlgeschlagen ist. Es wurden 4 Elemente zurückgegeben:

He whispered to her \ 
I 
love 
you". 

Ich hoffe, dies hilft jemandem, der in Zukunft nach einer solchen Lösung sucht.

Kevin Thach
quelle
3
Entschuldigen Sie die Nekromantie, aber diese Lösung vermisst immer noch Dinge, bla.exe aAAA"b\"ASDS\"c"dSADSDdie dazu führen, aAAAb"ASDS"cdSADSDdass diese Lösung ausgegeben wird aAAA"b"ASDS"c"dSADSD. Ich könnte in Betracht ziehen, das TrimMatchingQuotesin a zu ändern Regex("(?<!\\\\)\\\"")und es so zu verwenden .
Scis
4

Environment.GetCommandLineArgs ()

Mark Cidade
quelle
2
Nützlich - aber dadurch erhalten Sie nur die Befehlszeilenargumente, die an den aktuellen Prozess gesendet werden. Die Anforderung bestand darin, eine Zeichenfolge [] aus einer Zeichenfolge abzurufen, "auf die gleiche Weise wie C #, wenn die Befehle in der Befehlszeile angegeben worden wären ". Ich denke, wir könnten einen Dekompiler verwenden, um zu sehen, wie MS dies implementiert hat ...
rohancragg
Wie Jon Galloway ebenfalls feststellte ( weblogs.asp.net/jgalloway/archive/2006/09/13/… ), hilft ein Dekompiler nicht viel, was uns direkt zu Atifs Antwort zurückbringt ( stackoverflow.com/questions/298830/… ).
Rohancragg
4

Ich mag Iteratoren und heutzutage LINQ macht IEnumerable<String>so leicht verwendbar als Arrays von String, so mein nimmt nach dem Geist von Jeffrey L Whitledge Antwort ist (als Erweiterungsmethode 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
quelle
3

In Ihrer Frage haben Sie nach einer Regex gefragt, und ich bin ein großer Fan und Benutzer von Regex. Als ich also das gleiche Argument wie Sie aufteilen musste, schrieb ich meine eigene Regex, nachdem ich herumgegoogelt und keine einfache Lösung gefunden hatte. Ich mag kurze Lösungen, also habe ich eine gemacht und hier ist sie:

            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();

Es behandelt Leerzeichen und Anführungszeichen in Anführungszeichen und konvertiert beiliegende "" in ". Verwenden Sie den Code!

Thomas Petersson
quelle
3

Oh zum Teufel. Es ist alles ... Eugh. Aber das ist echt offiziell. Von Microsoft in C # für .NET Core, möglicherweise nur Windows, möglicherweise plattformübergreifend, aber MIT-lizenziert.

Wählen Sie Leckerbissen, Methodendeklarationen und bemerkenswerte Kommentare aus.

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

Dies ist Code, der von .NET Framework auf .NET Core portiert wird, von dem ich annehme, dass es sich entweder um die MSVC C-Bibliothek handelt oder CommandLineToArgvW.

Hier ist mein halbherziger Versuch, einige der Spielereien mit regulären Ausdrücken zu behandeln und das Argument Null-Bit zu ignorieren. Es ist ein bisschen zauberhaft.

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();
  }
}

Habe es ein bisschen auf verrückte generierte Ausgabe getestet. Die Ausgabe entspricht einem angemessenen Prozentsatz dessen, was die Affen eingegeben und durchlaufen haben CommandLineToArgvW.

TylerY86
quelle
1
Ja, es sieht so aus, als wäre die C # -Version tot. github.com/dotnet/runtime/blob/master/src/coreclr/src/utilcode/…
TylerY86
1
Zeitlich begrenzte Wiederbelebung. pastebin.com/ajhrBS4t
TylerY86
2

Diesen Artikel zum Code-Projekt habe ich in der Vergangenheit verwendet. Es ist ein gutes Stück Code, aber es könnte funktionieren.

Dieser MSDN-Artikel ist das einzige, was ich finden konnte, das erklärt, wie C # Befehlszeilenargumente analysiert.

Zachary Yates
quelle
Ich habe versucht, Reflektor in die C # -Bibliothek zu integrieren, aber es geht um einen nativen C ++ - Aufruf, für den ich keinen Code habe, und ich kann keine Möglichkeit zum Aufrufen sehen, ohne ihn p-aufzurufen. Ich möchte auch keine Befehlszeilen-Analysebibliothek, sondern nur die Zeichenfolge [].
Anton
Das Reflektieren von .NET brachte mich auch nirgendwo hin. Ein Blick in die Mono Source Code vorgeschlagen , dass dieses Argument Spaltung nicht von der CLR getan wird , sondern kommt bereits aus dem Betriebssystem. Denken Sie an die argc, argv-Parameter der C-Hauptfunktion. Es gibt also nichts anderes als die OS-API, die wiederverwendet werden kann.
Ygoe
1

Eine rein verwaltete Lösung kann hilfreich sein. Es gibt zu viele "Problem" -Kommentare für die WINAPI-Funktion und sie ist auf anderen Plattformen nicht verfügbar. Hier ist mein Code, der ein genau definiertes Verhalten aufweist (das Sie ändern können, wenn Sie möchten).

Es sollte dasselbe tun wie .NET / Windows, wenn dieser string[] argsParameter bereitgestellt wird, und ich habe es mit einer Reihe von "interessanten" Werten verglichen.

Dies ist eine klassische State-Machine-Implementierung, bei der jedes einzelne Zeichen aus der Eingabezeichenfolge entnommen und für den aktuellen Status interpretiert wird, wodurch eine Ausgabe und ein neuer Status erzeugt werden. Der Zustand wird in den Variablen definiert ist escape, inQuote, hadQuoteund prevCh, und der Ausgang wird in gesammelt currentArgund args.

Einige der Besonderheiten, die ich durch Experimente an einer echten Eingabeaufforderung (Windows 7) entdeckt habe: \\produziert \, \"produziert ", ""innerhalb eines angegebenen Bereichs produziert ".

Der ^Charakter scheint auch magisch zu sein: Er verschwindet immer, wenn er nicht verdoppelt wird. Andernfalls hat dies keine Auswirkungen auf eine echte Befehlszeile. Meine Implementierung unterstützt dies nicht, da ich in diesem Verhalten kein Muster gefunden habe. Vielleicht weiß jemand mehr darüber.

Etwas, das nicht in dieses Muster passt, ist der folgende Befehl:

cmd /c "argdump.exe "a b c""

Der cmdBefehl scheint die äußeren Anführungszeichen zu erfassen und den Rest wörtlich zu nehmen. Darin muss eine spezielle magische Sauce sein.

Ich habe keine Benchmarks für meine Methode durchgeführt, halte sie jedoch für relativ schnell. Es verwendet Regexkeine Verkettung von Zeichenfolgen und führt keine Verkettung durch, sondern verwendet a StringBuilder, um die Zeichen für ein Argument zu sammeln und sie in eine Liste aufzunehmen.

/// <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
quelle
1

Verwenden:

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' });
}

Basierend auf Vapor in der Antwort der Alley unterstützt diese auch ^ Escape.

Beispiele:

  • dies ist ein Test
    • Dies
    • ist
    • ein
    • Prüfung
  • dies ist ein Test
    • Dies
    • ist ein
    • Prüfung
  • Dies ist ein Test
    • Dies
    • "ist
    • ein"
    • Prüfung
  • dieses "" "ist ein ^^ Test"
    • Dies
    • Wenn Sie
    • ist ein ^ Test

Es werden auch mehrere Leerzeichen unterstützt (Argumente werden nur einmal pro Leerzeichenblock unterbrochen).

Fabio Iotti
quelle
Der letzte der drei stört Markdown irgendwie und wird nicht wie beabsichtigt gerendert.
Peter Mortensen
Mit einem Leerzeichen von Null behoben.
Fabio Iotti
0

Derzeit ist dies der Code, den ich habe:

    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;
    }

Es funktioniert nicht mit maskierten Anführungszeichen, aber es funktioniert für die Fälle, auf die ich bisher gestoßen bin.

Anton
quelle
0

Dies ist eine Antwort auf Antons Code, der nicht mit maskierten Anführungszeichen funktioniert. Ich habe 3 Stellen geändert.

  1. Der Konstruktor für StringBuilder in SplitCommandLineArguments , der jedes "" durch \ r ersetzt
  2. In der for-Schleife in SplitCommandLineArguments ersetze ich jetzt das Zeichen \ r wieder durch \ " .
  3. Die SplitCommandLineArgument- Methode wurde von privat in öffentlich statisch geändert .

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.
quelle
Ich gehe das gleiche Problem an. Sie hätten gedacht, dass es heutzutage eine einfache Lösung für das Testen von Befehlszeilenargumentzeichenfolgen für Komponententests gibt. Ich möchte nur sicher sein, welches Verhalten sich aus einer bestimmten Befehlszeilenargumentzeichenfolge ergibt. Ich gebe vorerst auf und werde Unit-Tests für string [] erstellen, kann aber einige Integrationstests hinzufügen, um dies abzudecken.
Charlie Barker
0

Ich glaube nicht, dass es einfache Anführungszeichen oder Anführungszeichen für C # -Anwendungen gibt. Die folgende Funktion funktioniert gut für mich:

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();
}
HarryP
quelle
0

Sie können sich den Code ansehen, den ich gestern gepostet habe:

[C #] Pfad- und Argumentzeichenfolgen

Es teilt einen Dateinamen + Argumente in string [] auf. Kurze Pfade, Umgebungsvariablen und fehlende Dateierweiterungen werden behandelt.

(Ursprünglich war es für UninstallString in Registry.)

Nolmë Informatique
quelle
0

Versuchen Sie diesen Code:

    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;
    }

Es ist in Portugiesisch geschrieben.

Lucas De Jesus
quelle
Vielmehr ist die Dokumentation portugiesisch
Enamul Hassan
@EnamulHassan Ich würde sagen, der Code ist auch in Portugiesisch, z posicao_ponteiro += ((fim - posicao_ponteiro)+1);.
MEMark
0

Hier ist ein Einzeiler, der die Arbeit erledigt (siehe die eine Zeile, die die gesamte Arbeit innerhalb der BurstCmdLineArgs (...) -Methode erledigt).

Nicht das, was ich als die am besten lesbare Codezeile bezeichnen würde, aber Sie können sie aus Gründen der Lesbarkeit ausbrechen. Es ist absichtlich einfach und funktioniert nicht für alle Argumentfälle (wie Dateinamenargumente, die das Trennzeichen für geteilte Zeichenfolgen enthalten).

Diese Lösung hat in meinen Lösungen, die sie verwenden, gut funktioniert. Wie ich bereits sagte, wird die Aufgabe ohne das Code-Nest einer Ratte erledigt, um jedes mögliche Argumentformat n-Fakultät zu verarbeiten.

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
quelle
0

Ich konnte hier nichts finden, was mir gefallen hat. Ich hasse es, den Stapel mit Ertragsmagie für eine kleine Befehlszeile durcheinander zu bringen (wenn es ein Terabyte-Stream wäre, wäre es eine andere Geschichte).

Hier ist meine Meinung, es unterstützt Anführungszeichen mit doppelten Anführungszeichen wie diesen:

param = "a 15" "Bildschirm ist nicht schlecht" param2 = 'a 15 "Bildschirm ist nicht schlecht' param3 =" "param4 = / param5

Ergebnis:

param = "Ein 15" Bildschirm ist nicht schlecht "

param2 = 'Ein 15 "Bildschirm ist nicht schlecht'

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
quelle
0

Ich habe State Machine so implementiert, dass dieselben Parser-Ergebnisse erzielt werden, als würden Argumente an die .NET-Anwendung übergeben und in der static void Main(string[] args)Methode verarbeitet .

    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
quelle
0

Hier ist die Lösung, die Leerzeichen (einzelne oder mehrere Leerzeichen) als Trennzeichen für Befehlszeilenparameter behandelt und die tatsächlichen Befehlszeilenargumente zurückgibt:

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
quelle
-2

Ich bin mir nicht sicher, ob ich dich verstanden habe, aber ist das Problem, dass das als Splitter verwendete Zeichen auch im Text zu finden ist? (Abgesehen davon wird es mit double "?"

In diesem forFall würde ich eine Schleife erstellen und alle Instanzen, in denen <"> vorhanden ist, durch <|> (oder ein anderes" sicheres "Zeichen) ersetzen, aber sicherstellen, dass nur <"> und nicht <""> ersetzt wird

Nach dem Iterieren der Zeichenfolge würde ich wie zuvor angegeben die Zeichenfolge teilen, jetzt jedoch das Zeichen <|>.

Israr Khan
quelle
Die doppelten "" werden angezeigt, weil es sich um ein @ ".." - Zeichenfolgenliteral handelt. Die doppelten "" innerhalb der @ ".." - Zeichenfolge entsprechen einem "Escape" in einer normalen Zeichenfolge
Anton
"Die einzige Einschränkung (ich glaube) ist, dass die Zeichenfolgen durch Leerzeichen getrennt sind, es sei denn, das Leerzeichen tritt innerhalb eines" ... "Blocks auf -> Könnte einen Vogel mit einer Panzerfaust schießen, aber einen Booleschen Wert setzen, der" wahr "ist. Wenn Sie sich in einem Zitat befinden und ein Leerzeichen in "true" erkannt wird, fahren Sie fort, sonst <> = <|>
Israr Khan,
-6

Ja, das Zeichenfolgenobjekt verfügt über eine integrierte Funktion namens Split(), die einen einzelnen Parameter verwendet, der das zu suchende Zeichen als Trennzeichen angibt, und ein Array von Zeichenfolgen (Zeichenfolge []) mit den einzelnen Werten zurückgibt.

Charles Bretana
quelle
1
Dies würde den Teil src: "C: \ tmp \ Some Folder \ Sub Folder" falsch aufteilen.
Anton
Was ist mit Anführungszeichen in der Zeichenfolge, die die Aufteilung auf Leerzeichen vorübergehend deaktivieren?
Daniel Earwicker