Diviser la chaîne contenant les paramètres de ligne de commande en chaîne [] en C #


88

J'ai une seule chaîne qui contient les paramètres de ligne de commande à passer à un autre exécutable et je dois extraire la chaîne [] contenant les paramètres individuels de la même manière que C # si les commandes avaient été spécifiées sur la ligne de commande. La chaîne [] sera utilisée lors de l'exécution d'un autre point d'entrée d'assemblys via la réflexion.

Existe-t-il une fonction standard pour cela? Ou existe-t-il une méthode préférée (regex?) Pour diviser correctement les paramètres? Il doit gérer les chaînes délimitées par '"' qui peuvent contenir des espaces correctement, donc je ne peux pas simplement diviser sur" ".

Exemple de chaîne:

string parameterString = @"/src:""C:\tmp\Some Folder\Sub Folder"" /users:""abcdefg@hijkl.com"" tasks:""SomeTask,Some Other Task"" -someParam foo";

Exemple de résultat:

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

Je n'ai pas besoin d'une bibliothèque d'analyse en ligne de commande, juste un moyen d'obtenir la chaîne [] qui doit être générée.

Mise à jour : j'ai dû modifier le résultat attendu pour qu'il corresponde à ce qui est réellement généré par C # (supprimé les "supplémentaires" dans les chaînes séparées)


Google dit:
Analyseur d'

5
Chaque fois que quelqu'un répond, vous semblez avoir une objection basée sur des éléments qui ne figurent pas dans votre message. Je vous suggère de mettre à jour votre message avec ce matériel. Vous obtiendrez peut-être de meilleures réponses.
tvanfosson

1
Bonne question, à la recherche de la même chose. J'espérais trouver quelqu'un qui dise "hey .net expose ça ici ..." :) Si je tombe sur ça à un moment donné, je le posterai ici, même si ça fait 6 ans. Encore une question valable!
MikeJansen

J'ai créé une version purement gérée dans une réponse ci-dessous car j'avais également besoin de cette fonction.
ygoe

Réponses:


74

En plus de la bonne et pure solution gérée d' Earwicker , il peut être utile de mentionner, par souci d'exhaustivité, que Windows fournit également la CommandLineToArgvWfonction de décomposition d'une chaîne en un tableau de chaînes:

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

Analyse une chaîne de ligne de commande Unicode et retourne un tableau de pointeurs vers les arguments de ligne de commande, ainsi qu'un nombre de ces arguments, d'une manière similaire aux valeurs argv et argc standard d'exécution C.

Vous trouverez un exemple d’appel de cette API à partir de C # et de décompression du tableau de chaînes résultant dans du code managé à l’adresse « Conversion de la chaîne de ligne de commande en Args [] à l’aide de l’API CommandLineToArgvW () ». Voici une version légèrement plus simple du même code:

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

1
Cette fonction nécessite que vous échappiez la barre oblique inverse de fin d'un chemin entre guillemets. "C: \ Program Files \" doit être "C: \ Program Files \\" pour que cela fonctionne pour analyser correctement la chaîne.
Magnus Lindhe

8
Il est également intéressant de noter que CommandLineArgvW s'attend à ce que le premier argument soit le nom du programme, et la magie d'analyse appliquée n'est pas tout à fait la même si elle n'est pas transmise. Vous pouvez le simuler avec quelque chose comme:CommandLineToArgs("foo.exe " + commandLine).Skip(1).ToArray();
Scott Wegner

4
Par souci d'exhaustivité, MSVCRT n'utilise pas CommandLineToArgvW () pour convertir la ligne de commande en argc / argv. Il utilise son propre code, ce qui est différent. Par exemple, essayez d'appeler CreateProcess avec cette chaîne: a "b c" def. Dans main (), vous obtiendrez 3 arguments (comme documenté dans MSDN), mais le combo CommandLineToArgvW () / GetCommandLineW () vous en donnera 2.
LRN

7
OMG c'est un tel gâchis. soupe typique de MS. rien n'est canonisé, et jamais KISS n'est respecté dans le monde de la SP.
v.oddou

1
J'ai publié une version multiplateforme de l'implémentation MSVCRT traduite par Microsoft et une approximation de haute précision à l'aide de Regex. Je sais que c'est vieux, mais bon - pas de rouleaux de corps.
TylerY86

101

Cela m'ennuie qu'il n'y ait pas de fonction pour diviser une chaîne en fonction d'une fonction qui examine chaque caractère. S'il y en avait, vous pourriez l'écrire comme ceci:

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

Bien qu'ayant écrit cela, pourquoi ne pas écrire les méthodes d'extension nécessaires. Ok, tu m'as parlé de ça ...

Tout d'abord, ma propre version de Split qui prend une fonction qui doit décider si le caractère spécifié doit diviser la chaîne:

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

Cela peut produire des chaînes vides selon la situation, mais peut-être que ces informations seront utiles dans d'autres cas, donc je ne supprime pas les entrées vides dans cette fonction.

Deuxièmement (et plus banalement) un petit assistant qui coupera une paire de guillemets correspondante du début et de la fin d'une chaîne. C'est plus difficile que la méthode Trim standard - elle ne coupera qu'un seul caractère à chaque extrémité, et elle ne coupera pas d'une seule extrémité:

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

Et je suppose que vous voudrez aussi des tests. Eh bien, très bien alors. Mais ce doit être absolument la dernière chose! Tout d'abord, une fonction d'assistance qui compare le résultat de la division avec le contenu attendu du tableau:

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

Ensuite, je peux écrire des tests comme celui-ci:

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

Voici le test pour vos besoins:

        Test(@"/src:""C:\tmp\Some Folder\Sub Folder"" /users:""abcdefg@hijkl.com"" tasks:""SomeTask,Some Other Task"" -someParam",
             @"/src:""C:\tmp\Some Folder\Sub Folder""", @"/users:""abcdefg@hijkl.com""", @"tasks:""SomeTask,Some Other Task""", @"-someParam");

Notez que l'implémentation a la fonctionnalité supplémentaire qu'elle supprimera les guillemets autour d'un argument si cela a du sens (grâce à la fonction TrimMatchingQuotes). Je pense que cela fait partie de l'interprétation normale de la ligne de commande.


J'ai dû décocher ceci comme réponse car je n'avais pas les bons résultats attendus. La sortie réelle ne devrait pas avoir les "'s dans le tableau final
Anton

16
Je viens chez Stack Overflow pour m'éloigner des exigences qui changent tout le temps! :) Vous pouvez utiliser Replace ("\" "," ") au lieu de TrimMatchingQuotes () pour supprimer tous les guillemets. Mais Windows prend en charge \" pour autoriser le passage d'un caractère de guillemet. Ma fonction Split ne peut pas faire cela.
Daniel Earwicker

1
Gentil Earwicker :) Anton: C'est la solution que j'essayais de vous décrire dans mon article précédent, mais Earwicker a fait un bien meilleur travail en l'écrivant;) Et l'a également beaucoup étendue;)
Israr Khan

un espace n'est pas le seul caractère de séparation pour les arguments de ligne de commande, n'est-ce pas?
Louis Rhys

@Louis Rhys - Je ne suis pas sûr. Si c'est un problème, il est assez facile à résoudre: utilisez à la char.IsWhiteSpaceplace de== ' '
Daniel Earwicker

25

L'analyseur de ligne de commande Windows se comporte comme vous le dites, divisé en espace, sauf s'il y a un guillemet non fermé devant lui. Je recommanderais d'écrire l'analyseur vous-même. Quelque chose comme ça peut-être:

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

2
Je me suis retrouvé avec la même chose, sauf que j'ai utilisé .Split (new char [] {'\ n'}, StringSplitOptions.RemoveEmptyEntries) dans la dernière ligne au cas où il y aurait des '' supplémentaires entre les paramètres. Semble fonctionner.
Anton

3
Je suppose que Windows doit avoir un moyen d'échapper aux guillemets dans les paramètres ... cet algorithme ne prend pas cela en compte.
rmeador

La suppression des lignes vides, la suppression des guillemets extérieurs et la gestion des guillemets échappés sont laissées en tant qu'excersize pour le lecteur.
Jeffrey L Whitledge

Char.IsWhiteSpace () pourrait vous aider ici
Sam Mackrill

Cette solution est bonne si les arguments sont séparés par un seul espace, mais échoue si les arguments sont séparés par plusieurs espaces. Lien vers la bonne solution: stackoverflow.com/a/59131568/3926504
Dilip Nannaware

13

J'ai pris la réponse de Jeffrey L Whitledge et l' ai un peu améliorée.

Il prend désormais en charge les guillemets simples et doubles. Vous pouvez utiliser des guillemets dans les paramètres eux-mêmes en utilisant d'autres guillemets tapés.

Il supprime également les guillemets des arguments car ceux-ci ne contribuent pas aux informations sur les arguments.

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

7

La bonne et pure solution gérée par Earwicker n'a pas réussi à gérer des arguments comme celui-ci:

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

Il a renvoyé 3 éléments:

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

Voici donc un correctif pour prendre en charge le "quote \" escape \ "quote":

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

Testé avec 2 cas supplémentaires:

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

A également noté que la réponse acceptée par Atif Aziz qui utilise CommandLineToArgvW a également échoué. Il a renvoyé 4 éléments:

He whispered to her \ 
I 
love 
you". 

J'espère que cela aidera quelqu'un à la recherche d'une telle solution à l'avenir.


3
Désolé pour la nécromancie, mais cette solution manque toujours des éléments tels bla.exe aAAA"b\"ASDS\"c"dSADSDque les résultats de la aAAAb"ASDS"cdSADSDsortie de cette solution aAAA"b"ASDS"c"dSADSD. Je pourrais envisager de changer le TrimMatchingQuotesen a Regex("(?<!\\\\)\\\"")et l'utiliser comme ça .
Scis

4

2
Utile - mais cela ne vous donnera que les arguments de ligne de commande envoyés au processus en cours. L'exigence était d'obtenir une chaîne [] à partir d'une chaîne "de la même manière que C # le ferait si les commandes avaient été spécifiées sur la ligne de commande". Je suppose que nous pourrions utiliser un décompilateur pour voir comment MS a implémenté cela ...
rohancragg

Comme Jon Galloway l'a également trouvé ( weblogs.asp.net/jgalloway/archive/2006/09/13/… ), un décompilateur n'aide pas beaucoup ce qui nous ramène directement à la réponse d'Atif ( stackoverflow.com/questions/298830/… )
rohancragg

4

J'aime les itérateurs, et de nos jours, LINQ rend IEnumerable<String>aussi facilement utilisable que des tableaux de chaînes, donc mon point de vue suivant l'esprit de la réponse de Jeffrey L Whitledge est (en tant que méthode d'extension de 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();
}

3

Dans votre question, vous avez demandé une expression régulière, et j'en suis un grand fan et utilisateur, alors quand j'ai eu besoin de faire ce même argument partagé que vous, j'ai écrit ma propre expression régulière après avoir cherché sur Google et ne pas trouver une solution simple. J'aime les solutions courtes, alors j'en ai fait une et la voici:

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

Il gère les espaces et les guillemets entre guillemets et convertit les "" en "inclus. N'hésitez pas à utiliser le code!


3

Oh diable. C'est tout ... Eugh. Mais c'est officiel légitime. De Microsoft en C # pour .NET Core, peut-être Windows uniquement, peut-être multiplate-forme, mais sous licence MIT.

Sélectionnez des bribes, des déclarations de méthode et des commentaires notables;

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

Il s'agit de code porté sur .NET Core à partir de .NET Framework à partir de ce que je suppose être la bibliothèque MSVC C ou CommandLineToArgvW.

Voici ma tentative sans enthousiasme de gérer certaines des manigances avec des expressions régulières et d'ignorer l'argument zéro bit. C'est un peu sorcier.

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

Je l'ai testé un peu sur la sortie générée farfelue. Sa sortie correspond à un bon pourcentage de ce que les singes ont tapé et parcouru CommandLineToArgvW.



1
Ouais, on dirait que la version C # est morte. github.com/dotnet/runtime/blob/master/src/coreclr/src/utilcode/…
TylerY86

1
Réveil à durée limitée. pastebin.com/ajhrBS4t
TylerY86

2

Cet article The Code Project est ce que j'ai utilisé dans le passé. C'est un bon morceau de code, mais cela pourrait fonctionner.

Cet article MSDN est la seule chose que j'ai pu trouver qui explique comment C # analyse les arguments de ligne de commande.


J'ai essayé le réflecteur dans la bibliothèque C #, mais il va à un appel C ++ natif pour lequel je n'ai pas le code, et je ne vois aucun moyen d'appeler sans l'invoquer p. Je ne veux pas non plus de bibliothèque d'analyse en ligne de commande, je veux juste la chaîne [].
Anton

Le reflet de .NET ne m'a amené nulle part non plus. L'examen du code source Mono a suggéré que ce fractionnement d'argument n'est pas effectué par le CLR mais provient déjà du système d'exploitation. Pensez aux paramètres argc, argv de la fonction principale C. Il n'y a donc rien à réutiliser à part l'API du système d'exploitation.
ygoe

1

Une solution purement gérée peut être utile. Il y a trop de commentaires "problème" pour la fonction WINAPI et elle n'est pas disponible sur d'autres plates-formes. Voici mon code qui a un comportement bien défini (que vous pouvez changer si vous le souhaitez).

Il devrait faire la même chose que ce que fait .NET / Windows lors de la fourniture de ce string[] argsparamètre, et je l'ai comparé à un certain nombre de valeurs «intéressantes».

Il s'agit d'une implémentation de machine à états classique qui prend chaque caractère de la chaîne d'entrée et l'interprète pour l'état actuel, produisant une sortie et un nouvel état. L'état est défini dans les variables escape, inQuote, hadQuoteet prevCh, et la sortie est recueillie dans currentArget args.

Certaines des spécialités que j'ai découvertes par des expériences sur une vraie invite de commande (Windows 7): \\produit \, \"produit ", ""dans une plage citée produit ".

Le ^personnage semble aussi magique: il disparaît toujours lorsqu'il ne le double pas. Sinon, cela n'a aucun effet sur une vraie ligne de commande. Mon implémentation ne prend pas en charge cela, car je n'ai pas trouvé de modèle dans ce comportement. Peut-être que quelqu'un en sait plus.

Quelque chose qui ne rentre pas dans ce modèle est la commande suivante:

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

La cmdcommande semble attraper les guillemets extérieurs et prendre le reste textuellement. Il doit y avoir une sauce magique spéciale là-dedans.

Je n'ai fait aucun benchmark sur ma méthode, mais considérez-la raisonnablement rapide. Il n'utilise Regexet ne fait aucune concaténation de chaînes mais utilise à la place a StringBuilderpour collecter les caractères d'un argument et les met dans une liste.

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

1

Utilisation:

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

Basé sur la réponse de Vapor in the Alley , celui-ci prend également en charge ^ échappe.

Exemples:

  • c'est un test
    • ce
    • est
    • une
    • tester
  • c'est un test
    • ce
    • est un
    • tester
  • ce ^ "est un ^" test
    • ce
    • "est
    • une"
    • tester
  • ce "" "est un ^^ test"
    • ce
    • est un ^ test

Il prend également en charge plusieurs espaces (casse les arguments une seule fois par bloc d'espaces).


Le dernier des trois interfère d'une manière ou d'une autre avec Markdown et n'est pas rendu comme prévu.
Peter Mortensen

Fixé avec un espace de largeur zéro.
Fabio Iotti

0

Actuellement, c'est le code que j'ai:

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

Cela ne fonctionne pas avec les citations échappées, mais cela fonctionne pour les cas auxquels je me suis heurté jusqu'à présent.


0

Ceci est une réponse au code d'Anton, qui ne fonctionne pas avec des guillemets échappés. J'ai modifié 3 lieux.

  1. Le constructeur de StringBuilder dans SplitCommandLineArguments , en remplaçant tout \ " par \ r
  2. Dans la boucle for de SplitCommandLineArguments , je remplace maintenant le caractère \ r par \ " .
  3. Changement de la méthode SplitCommandLineArgument de privé à public statique .

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

J'aborde ce même problème, vous auriez pensé qu'à cette époque, une solution simple existerait pour les chaînes d'argument de ligne de commande de test unitaire. Tout ce dont je veux être sûr est le comportement qui résultera d'une chaîne d'argument de ligne de commande donnée. J'abandonne pour l'instant et je vais créer des tests unitaires pour string [] mais je pourrais ajouter quelques tests d'intégration pour couvrir cela.
Charlie Barker

0

Je ne pense pas qu'il existe des guillemets simples ou des guillemets ^ pour les applications C #. La fonction suivante fonctionne bien pour moi:

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

0

Vous pouvez consulter le code que j'ai publié hier:

[C #] Chaînes de chemin et d'arguments

Il divise un nom de fichier + des arguments en chaîne []. Les chemins d'accès courts, les variables d'environnement et les extensions de fichier manquantes sont gérés.

(Au départ, c'était pour UninstallString dans le registre.)


0

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

C'est écrit en portugais.


plutôt la documentation est portugaise
Enamul Hassan

@EnamulHassan Je dirais que le code est également en portugais, par exemple posicao_ponteiro += ((fim - posicao_ponteiro)+1);.
MEMark

0

Voici une ligne unique qui fait le travail (voir la ligne qui fait tout le travail à l'intérieur de la méthode BurstCmdLineArgs (...)).

Pas ce que j'appellerais la ligne de code la plus lisible, mais vous pouvez la décomposer pour des raisons de lisibilité. C'est volontairement simple et ne fonctionne pas bien pour tous les cas d'argument (comme les arguments de nom de fichier qui contiennent le délimiteur de caractère de chaîne divisée).

Cette solution a bien fonctionné dans mes solutions qui l'utilisent. Comme je l'ai dit, cela fait le travail sans un nid de code pour gérer tous les formats d'argument n-factoriels possibles.

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

0

Je n'ai rien trouvé que j'aimais ici. Je déteste gâcher la pile avec de la magie de rendement pour une petite ligne de commande (si c'était un flux d'un téraoctet, ce serait une autre histoire).

Voici mon avis, il prend en charge les échappements de guillemets avec des guillemets doubles comme ceux-ci:

param = "un écran 15" "n'est pas mauvais" param2 = "un écran 15" n'est pas mauvais "param3 =" "param4 = / param5

résultat:

param = "un écran de 15" n'est pas mauvais "

param2 = 'un écran de 15 pouces n'est pas mauvais'

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

0

J'ai implémenté la machine d'état pour avoir les mêmes résultats d'analyseur que si les arguments étaient passés dans l'application .NET et traités dans la static void Main(string[] args)méthode.

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

0

Voici la solution qui traite les espaces (espaces simples ou multiples) comme séparateur de paramètres de ligne de commande et renvoie les arguments réels de la ligne de commande:

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

-2

Je ne suis pas sûr de vous avoir compris, mais est-ce que le problème est que le caractère utilisé comme séparateur se trouve également à l'intérieur du texte? (Sauf que c'est échappé avec double "?)

Si tel est le cas, je créerais une forboucle et remplacerais toutes les instances où <"> est présent par <|> (ou un autre caractère" sûr ", mais assurez-vous qu'il remplace uniquement <">, et non <"">

Après avoir itéré la chaîne, je ferais comme indiqué précédemment, diviser la chaîne, mais maintenant sur le caractère <|>.


Les doubles "" sont parce que c'est une chaîne littérale @ "..", Les doubles "'dans la chaîne @" .. "sont équivalentes à un \ échappé" dans une chaîne normale
Anton

"la seule restriction (je crois) est que les chaînes sont délimitées par des espaces, à moins que l'espace ne se trouve dans un" ... "bloc" -> Peut-être tirer sur un oiseau avec un bazooka, mais mettre un booléen qui devient "vrai" à l'intérieur d'un guillemet, et si un espace est détecté à l'intérieur alors que "true", continuez, sinon <> = <|>
Israr Khan

-6

Oui, l'objet string a une fonction intégrée appelée Split()qui prend un seul paramètre spécifiant le caractère à rechercher comme délimiteur, et renvoie un tableau de chaînes (string []) avec les valeurs individuelles.


1
Cela diviserait la partie src: "C: \ tmp \ Some Folder \ Sub Folder" de manière incorrecte.
Anton

Qu'en est-il des guillemets à l'intérieur de la chaîne qui désactivent temporairement le fractionnement sur les espaces?
Daniel Earwicker
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.