web-dev-qa-db-fra.com

Échapper les arguments de ligne de commande en c #

Version courte:

Suffit-il de mettre l’argument entre guillemets et d’échapper \ et "?

Version du code

Je veux passer les arguments de la ligne de commande string[] args vers un autre processus utilisant ProcessInfo.Arguments.

ProcessStartInfo info = new ProcessStartInfo();
info.FileName = Application.ExecutablePath;
info.UseShellExecute = true;
info.Verb = "runas"; // Provides Run as Administrator
info.Arguments = EscapeCommandLineArguments(args);
Process.Start(info);

Le problème est que j'obtiens les arguments sous forme de tableau et que je dois les fusionner en une seule chaîne. Un argument pourrait être conçu pour tromper mon programme.

my.exe "C:\Documents and Settings\MyPath \" --kill-all-humans \" except fry"

Selon cette réponse J'ai créé la fonction suivante pour échapper à un seul argument, mais j'ai peut-être manqué quelque chose.

private static string EscapeCommandLineArguments(string[] args)
{
    string arguments = "";
    foreach (string arg in args)
    {
        arguments += " \"" +
            arg.Replace ("\\", "\\\\").Replace("\"", "\\\"") +
            "\"";
    }
    return arguments;
}

Est-ce suffisant ou existe-t-il une fonction de cadre pour cela?

71
hultqvist

C'est plus compliqué que ça!

J'avais un problème connexe (écrire un front-end .exe qui appellera le back-end avec tous les paramètres passés + certains supplémentaires) et j'ai donc regardé comment les gens font cela, j'ai rencontré votre question. Au début, tout semblait bien le faire comme vous le suggérez arg.Replace (@"\", @"\\").Replace(quote, @"\"+quote).

Cependant, lorsque j'appelle avec des arguments c:\temp a\\b, ceci est passé en tant que c:\temp et a\\b, ce qui conduit à appeler le back-end avec "c:\\temp" "a\\\\b" - ce qui est incorrect, car il y aura deux arguments c:\\temp et a\\\\b - pas ce que nous voulions! Nous avons été trop zélés dans les évasions (Windows n'est pas Unix!).

Et donc j'ai lu en détail http://msdn.Microsoft.com/en-us/library/system.environment.getcommandlineargs.aspx et il décrit en fait comment ces cas sont traités: les antislashs sont traités comme escape seulement devant les guillemets doubles.

Il y a une torsion dans la façon dont plusieurs \ y sont traités, l'explication peut laisser le vertige un moment. Je vais essayer de reformuler ladite règle de non-échappement ici: disons que nous avons une sous-chaîne de [~ # ~] n [~ # ~]\, suivi par ". Lors de l'échappement, nous remplaçons cette sous-chaîne par int (N/2)\ et ssi [~ # ~] n [~ # ~] était étrange, nous ajoutons " à la fin.

Le codage pour un tel décodage se passerait comme ça: pour un argument, trouver chaque sous-chaîne de 0 ou plus \ suivi par " et remplacez-le par deux fois \, suivi par \". Ce que nous pouvons faire comme ça:

s = Regex.Replace(arg, @"(\\*)" + "\"", @"$1$1\" + "\"");

C'est tout...

PS. ... pas . Attendez, attendez - il y a plus! :)

Nous avons fait l'encodage correctement mais il y a une torsion parce que vous mettez tous les paramètres entre guillemets (au cas où il y aurait des espaces dans certains d'entre eux). Il y a un problème de limite - dans le cas où un paramètre se termine sur \, ajouter " après cela rompra le sens de la citation de clôture. Exemple c:\one\ two analysé en c:\one\ et two seront ensuite réassemblés en "c:\one\" "two" qui me sera (mal) compris comme un argument c:\one" two (J'ai essayé ça, je n'invente rien). Donc, ce dont nous avons besoin en plus est de vérifier si l'argument se termine sur \ et si oui, double le nombre de barres obliques inverses à la fin, comme ceci:

s = "\"" + Regex.Replace(s, @"(\\+)$", @"$1$1") + "\"";
64
Nas Banov

Ma réponse était similaire à la réponse de Nas Banov mais je voulais des guillemets doubles seulement si nécessaire.

Suppression des guillemets doubles inutiles

Mon code évite de mettre inutilement des guillemets doubles tout le temps ce qui est important * lorsque vous vous approchez de la limite de caractères pour les paramètres.

/// <summary>
/// Encodes an argument for passing into a program
/// </summary>
/// <param name="original">The value that should be received by the program</param>
/// <returns>The value which needs to be passed to the program for the original value 
/// to come through</returns>
public static string EncodeParameterArgument(string original)
{
    if( string.IsNullOrEmpty(original))
        return original;
    string value = Regex.Replace(original, @"(\\*)" + "\"", @"$1\$0");
    value = Regex.Replace(value, @"^(.*\s.*?)(\\*)$", "\"$1$2$2\"");
    return value;
}

// This is an EDIT
// Note that this version does the same but handles new lines in the arugments
public static string EncodeParameterArgumentMultiLine(string original)
{
    if (string.IsNullOrEmpty(original))
        return original;
    string value = Regex.Replace(original, @"(\\*)" + "\"", @"$1\$0");
    value = Regex.Replace(value, @"^(.*\s.*?)(\\*)$", "\"$1$2$2\"", RegexOptions.Singleline);

    return value;
}

explication

Pour échapper aux barres obliques inverses et entre guillemets correctement, vous pouvez simplement remplacer toutes les instances de plusieurs barres obliques inverses suivies d'une simple citation double avec:

string value = Regex.Replace(original, @"(\\*)" + "\"", @"\$1$0");

Deux fois plus les barres obliques inversées d'origine + 1 et la citation double d'origine . c'est-à-dire, '\' + originalbackslashes + originalbackslashes + '"'. J'ai utilisé $ 1 $ 0 puisque $ 0 a les contre-obliques d'origine et l'original guillemet double afin que le remplacement soit plus agréable à lire.

value = Regex.Replace(value, @"^(.*\s.*?)(\\*)$", "\"$1$2$2\"");

Cela ne peut correspondre qu'à une ligne entière contenant un espace.

S'il correspond, il ajoute des guillemets doubles au début et à la fin.

S'il y avait à l'origine des barres obliques inverses à la fin de l'argument, elles n'auront pas été citées, maintenant qu'il y a un guillemet double à la fin, ils doivent être. Ils sont donc dupliqués, ce qui les cite tous, et empêche de citer involontairement le guillemet final

Il fait une correspondance minimale pour la première section afin que la dernière. *? ne mange pas en faisant correspondre les barres obliques inverses finales

Production

Ces entrées produisent donc les sorties suivantes

bonjour

bonjour

\ bonjour\12\3 \

\ bonjour\12\3 \

bonjour le monde

"Bonjour le monde"

\"Bonjour\"

\\ "bonjour \\\"

\"Bonjour le monde

"\\" bonjour\monde "

\"Bonjour le monde\

"\\" bonjour \\\ monde \\ "

bonjour le monde\\

"bonjour le monde \\\\"

29
Matt Vukomanovic

Je rencontrais aussi des problèmes avec cela. Au lieu de décompresser les arguments, j'ai choisi de prendre la ligne de commande originale complète et de supprimer l'exécutable. Cela avait l'avantage supplémentaire de conserver des espaces dans l'appel, même s'il n'est pas nécessaire/utilisé. Il doit toujours chasser les échappées dans l'exécutable, mais cela semblait plus facile que les arguments.

var commandLine = Environment.CommandLine;
var argumentsString = "";

if(args.Length > 0)
{
    // Re-escaping args to be the exact same as they were passed is hard and misses whitespace.
    // Use the original command line and trim off the executable to get the args.
    var argIndex = -1;
    if(commandLine[0] == '"')
    {
        //Double-quotes mean we need to Dig to find the closing double-quote.
        var backslashPending = false;
        var secondDoublequoteIndex = -1;
        for(var i = 1; i < commandLine.Length; i++)
        {
            if(backslashPending)
            {
                backslashPending = false;
                continue;
            }
            if(commandLine[i] == '\\')
            {
                backslashPending = true;
                continue;
            }
            if(commandLine[i] == '"')
            {
                secondDoublequoteIndex = i + 1;
                break;
            }
        }
        argIndex = secondDoublequoteIndex;
    }
    else
    {
        // No double-quotes, so args begin after first whitespace.
        argIndex = commandLine.IndexOf(" ", System.StringComparison.Ordinal);
    }
    if(argIndex != -1)
    {
        argumentsString = commandLine.Substring(argIndex + 1);
    }
}

Console.WriteLine("argumentsString: " + argumentsString);
6
Jeremy Murray

J'ai porté une fonction C++ à partir de l'article Tout le monde cite les arguments de la ligne de commande dans le mauvais sens article.

Cela fonctionne bien, mais vous devez noter que cmd.exe interprète la ligne de commande différemment. Si ( et seulement si , comme l'auteur de l'article d'origine l'a noté), votre ligne de commande sera interprétée par cmd.exe vous devez également échapper les métacaractères Shell.

/// <summary>
///     This routine appends the given argument to a command line such that
///     CommandLineToArgvW will return the argument string unchanged. Arguments
///     in a command line should be separated by spaces; this function does
///     not add these spaces.
/// </summary>
/// <param name="argument">Supplies the argument to encode.</param>
/// <param name="force">
///     Supplies an indication of whether we should quote the argument even if it 
///     does not contain any characters that would ordinarily require quoting.
/// </param>
private static string EncodeParameterArgument(string argument, bool force = false)
{
    if (argument == null) throw new ArgumentNullException(nameof(argument));

    // Unless we're told otherwise, don't quote unless we actually
    // need to do so --- hopefully avoid problems if programs won't
    // parse quotes properly
    if (force == false
        && argument.Length > 0
        && argument.IndexOfAny(" \t\n\v\"".ToCharArray()) == -1)
    {
        return argument;
    }

    var quoted = new StringBuilder();
    quoted.Append('"');

    var numberBackslashes = 0;

    foreach (var chr in argument)
    {
        switch (chr)
        {
            case '\\':
                numberBackslashes++;
                continue;
            case '"':
                // Escape all backslashes and the following
                // double quotation mark.
                quoted.Append('\\', numberBackslashes*2 + 1);
                quoted.Append(chr);
                break;
            default:
                // Backslashes aren't special here.
                quoted.Append('\\', numberBackslashes);
                quoted.Append(chr);
                break;
        }
        numberBackslashes = 0;
    }

    // Escape all backslashes, but let the terminating
    // double quotation mark we add below be interpreted
    // as a metacharacter.
    quoted.Append('\\', numberBackslashes*2);
    quoted.Append('"');

    return quoted.ToString();
}
4
stil

J'ai publié un petit projet sur GitHub qui gère la plupart des problèmes d'encodage/d'échappement de la ligne de commande:

https://github.com/ericpopivker/Command-Line-Encoder

Il existe une classe CommandLineEncoder.Utils.cs , ainsi que des tests unitaires qui vérifient la fonctionnalité d'encodage/décodage.

3
Eric P

Je vous ai écrit un petit exemple pour vous montrer comment utiliser les caractères d'échappement en ligne de commande.

public static string BuildCommandLineArgs(List<string> argsList)
{
    System.Text.StringBuilder sb = new System.Text.StringBuilder();

    foreach (string arg in argsList)
    {
        sb.Append("\"\"" + arg.Replace("\"", @"\" + "\"") + "\"\" ");
    }

    if (sb.Length > 0)
    {
        sb = sb.Remove(sb.Length - 1, 1);
    }

    return sb.ToString();
}

Et voici une méthode de test:

    List<string> myArgs = new List<string>();
    myArgs.Add("test\"123"); // test"123
    myArgs.Add("test\"\"123\"\"234"); // test""123""234
    myArgs.Add("test123\"\"\"234"); // test123"""234

    string cmargs = BuildCommandLineArgs(myArgs);

    // result: ""test\"123"" ""test\"\"123\"\"234"" ""test123\"\"\"234""

    // when you pass this result to your app, you will get this args list:
    // test"123
    // test""123""234
    // test123"""234

Le point consiste à encapsuler chaque argument avec des guillemets doubles-doubles ("" arg "") et à remplacer toutes les citations à l'intérieur de la valeur arg par une citation échappée (test\"123).

2
HABJAN
static string BuildCommandLineFromArgs(params string[] args)
{
    if (args == null)
        return null;
    string result = "";

    if (Environment.OSVersion.Platform == PlatformID.Unix 
        || 
        Environment.OSVersion.Platform == PlatformID.MacOSX)
    {
        foreach (string arg in args)
        {
            result += (result.Length > 0 ? " " : "") 
                + arg
                    .Replace(@" ", @"\ ")
                    .Replace("\t", "\\\t")
                    .Replace(@"\", @"\\")
                    .Replace(@"""", @"\""")
                    .Replace(@"<", @"\<")
                    .Replace(@">", @"\>")
                    .Replace(@"|", @"\|")
                    .Replace(@"@", @"\@")
                    .Replace(@"&", @"\&");
        }
    }
    else //Windows family
    {
        bool enclosedInApo, wasApo;
        string subResult;
        foreach (string arg in args)
        {
            enclosedInApo = arg.LastIndexOfAny(
                new char[] { ' ', '\t', '|', '@', '^', '<', '>', '&'}) >= 0;
            wasApo = enclosedInApo;
            subResult = "";
            for (int i = arg.Length - 1; i >= 0; i--)
            {
                switch (arg[i])
                {
                    case '"':
                        subResult = @"\""" + subResult;
                        wasApo = true;
                        break;
                    case '\\':
                        subResult = (wasApo ? @"\\" : @"\") + subResult;
                        break;
                    default:
                        subResult = arg[i] + subResult;
                        wasApo = false;
                        break;
                }
            }
            result += (result.Length > 0 ? " " : "") 
                + (enclosedInApo ? "\"" + subResult + "\"" : subResult);
        }
    }

    return result;
}
1
macropas

Une approche alternative

Si vous passez un objet complexe tel que JSON imbriqué et que vous avez le contrôle sur le système qui reçoit les arguments de ligne de commande, il est beaucoup plus facile de simplement coder les arguments de ligne de commande en base64, puis de les décoder à partir du système récepteur.

Voir ici: Encode/Decode String to/from Base64

Cas d'utilisation: j'avais besoin de passer un objet JSON qui contenait une chaîne XML dans l'une des propriétés qui était trop compliquée à échapper. Cela l'a résolu.

0
Dom

Fait un bon travail d'ajout d'arguments, mais n'échappe pas. Ajout d'un commentaire dans la méthode où la séquence d'échappement doit aller.

public static string ApplicationArguments()
{
    List<string> args = Environment.GetCommandLineArgs().ToList();
    args.RemoveAt(0); // remove executable
    StringBuilder sb = new StringBuilder();
    foreach (string s in args)
    {
        // todo: add escape double quotes here
        sb.Append(string.Format("\"{0}\" ", s)); // wrap all args in quotes
    }
    return sb.ToString().Trim();
}
0
Chuck Savage