Escape-Befehlszeilenargumente in c #

74

Kurzfassung:

Reicht es aus, das Argument in Anführungszeichen zu setzen und zu entkommen \und "?

Code-Version

Ich möchte die Befehlszeilenargumente string[] argsmit ProcessInfo.Arguments an einen anderen Prozess übergeben.

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

Das Problem ist, dass ich die Argumente als Array erhalte und sie zu einer einzigen Zeichenfolge zusammenführen muss. Es könnten Argumente formuliert werden, um mein Programm auszutricksen.

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

Nach dieser Antwort habe ich die folgende Funktion erstellt, um einem einzelnen Argument zu entgehen, aber ich habe möglicherweise etwas übersehen.

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

Ist das gut genug oder gibt es dafür eine Rahmenfunktion?

hultqvist
quelle
5
Hast du versucht, so wie es ist zu bestehen? Ich denke, wenn es an Sie übergeben wird, kann es an einen anderen Befehl übergeben werden. Wenn Sie auf Fehler stoßen, können Sie über eine Flucht nachdenken.
Sanjeevakumar Hiremath
2
@ Sanjeevakumar ja, zum Beispiel: "C:\Documents and Settings\MyPath \" --kill-all-humans \" except fry"wäre keine gute Sache, da ich einen privilegierten Anruf tätige .
Hultqvist
1
@ Sanjeevakumar Main (string [] args) ist ein Array von nicht entkappten Strings. Wenn ich also my.exe "test\"test"arg [0] test"test
ausführe,
1. Wenn Sie nur aufgrund Ihres ersten Kommentars entkommen möchten, sieht es so aus, als ob Sie nicht entkommen möchten. 2. Was sind nicht entflohene Saiten? Wenn du einen String wie abc"defdiesen bekommst, abc"defwarum willst du ihn jetzt entkommen? Wenn Sie etwas wie "abc" + "" "+" def "hinzufügen, ist dies sinnvoll. beobachten """"entkommt"
Sanjeevakumar Hiremath
Ja abc"defist angesichts der Eingabe korrekt. Wenn ich sie jedoch an einen anderen Prozess übergeben möchte, muss ich sie maskieren, bevor ich sie dem Argument für eine einzelne Zeichenfolge hinzufüge. Weitere Informationen finden Sie in der aktualisierten Frage.
Hultqvist

Antworten:

67

Es ist jedoch komplizierter!

Ich hatte ein ähnliches Problem (das Schreiben der Front-End-EXE-Datei, die das Back-End mit allen übergebenen Parametern + einigen zusätzlichen Parametern aufruft) und ich habe mir angesehen, wie die Leute das machen, und bin auf Ihre Frage gestoßen. Anfangs schien alles gut zu sein, wie Sie vorschlagen arg.Replace (@"\", @"\\").Replace(quote, @"\"+quote).

Wenn ich jedoch mit Argumenten aufrufe c:\temp a\\b, wird dies als c:\tempund übergeben a\\b, was dazu führt, dass das Back-End mit aufgerufen wird "c:\\temp" "a\\\\b"- was falsch ist, weil es zwei Argumente gibt c:\\tempund a\\\\b- nicht das, was wir wollten! Wir waren übereifrig bei Fluchten (Windows ist nicht Unix!).

Und so las ich im Detail http://msdn.microsoft.com/en-us/library/system.environment.getcommandlineargs.aspx und es beschreibt dort tatsächlich, wie diese Fälle behandelt werden: Backslashes werden nur vor Double behandelt Zitat.

Es gibt eine Wendung darin, wie mehrere \dort behandelt werden. Die Erklärung kann einen für eine Weile schwindelig machen. Ich werde versuchen, diese Unescape-Regel hier neu zu formulieren: Sagen wir, wir haben eine Teilzeichenfolge von N \ , gefolgt von ". Beim Entspannen ersetzen wir diesen Teilstring durch int (N / 2), \ und wenn N ungerade war, fügen wir "am Ende hinzu.

Die Codierung für eine solche Decodierung würde folgendermaßen aussehen: Suchen Sie für ein Argument jede Teilzeichenfolge von 0 oder mehr, \gefolgt von "und ersetzen Sie sie durch doppelt so viele \, gefolgt von \". Was wir so machen können:

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

Das ist alles...

PS. ... nicht . Warten Sie, warten Sie - es gibt noch mehr! :) :)

Wir haben die Codierung korrekt durchgeführt, aber es gibt eine Wendung, da Sie alle Parameter in doppelte Anführungszeichen setzen (falls in einigen Leerzeichen Leerzeichen enthalten sind). Es gibt ein Grenzproblem - falls ein Parameter endet \, wird durch Hinzufügen eines Parameters "die Bedeutung des schließenden Anführungszeichens aufgehoben. Das Beispiel wurde c:\one\ twoanalysiert c:\one\und twodann wieder zusammengesetzt "c:\one\" "two", damit ich (falsch) als ein Argument verstanden werde c:\one" two(das habe ich versucht, ich mache es nicht nach). Wir müssen also zusätzlich prüfen, ob das Argument endet, \und wenn ja, die Anzahl der Backslashes am Ende verdoppeln , wie folgt:

s = "\"" + Regex.Replace(s, @"(\\+)$", @"$1$1") + "\"";
Nas Banov
quelle
6
+1 für die Erklärung dieses Wahnsinns. Sollte das *und das +nicht in den Gruppierungsklammern in den obigen Übereinstimmungsausdrücken stehen? Andernfalls wird der $1Ersatz immer nur ein einziger Backslash sein.
Bobince
Eigentlich denke ich, dass die beiden Ersetzungen kombiniert werden können in : "\""+Regex.Replace(s, "(\\\\*)(\\\\$|\")", "$1$1\\$2")+"\"". Allerdings beginnt mein Gehirn jetzt zu sinken, so sehr geschätzt, wenn Sie die Richtigkeit überprüfen könnten :-)
Bobince
1
Danke für deine Antwort! Könnten Sie bitte TL hinzufügen; DR statische Methode, die alles handhabt? Ich mag Ihre Antwort wirklich, aber ich muss sie jedes Mal lesen und verstehen, wenn ich die Informationen brauche (weil ich zu dumm bin, um mich vollständig daran zu erinnern) ...
vojta
@vojta - ich entschuldige mich, aber es ist fünf Jahre her und ich erinnere mich nicht an die Details. Wenn ich das, was ich geschrieben habe, noch einmal lese, war es wohl nur nötig, diese beiden Zeilen aufzurufen. Aber Sie haben jetzt wahrscheinlich ein besseres Verständnis für den Fall. Warum bearbeiten Sie nicht die Antwort und fügen für die Nachwelt den TL-DNR hinzu?
Nas Banov
31

Meine Antwort war ähnlich wie die von Nas Banov, aber ich wollte nur bei Bedarf doppelte Anführungszeichen .

Schneiden Sie zusätzliche unnötige doppelte Anführungszeichen aus

Mein Code spart unnötig unnötige doppelte Anführungszeichen, was wichtig ist *, wenn Sie sich der Zeichenbeschränkung für Parameter nähern.

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

Erläuterung

Um den Backslashes und doppelten Anführungszeichen zu entkommen korrekt zu können Sie einfach alle Instanzen mehrerer Backslashes gefolgt von einem einfachen doppelten Anführungszeichen durch Folgendes ersetzen :

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

Ein zusätzliches Doppel der ursprünglichen Backslashes + 1 und des ursprünglichen doppelten Anführungszeichens . dh '\' + originalbackslashes + originalbackslashes + '"'. Ich habe $ 1 $ 0 verwendet, da $ 0 das Original hat Backslashes und das ursprüngliche doppelte Anführungszeichen enthält, sodass der Ersatz besser lesbar ist.

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

Dies kann immer nur mit einer ganzen Zeile übereinstimmen, die ein Leerzeichen enthält.

Wenn es übereinstimmt , werden am Anfang und am Ende doppelte Anführungszeichen hinzugefügt.

Wenn es ursprünglich Backslashes gab am Ende des Arguments sie nicht zitiert, jetzt, wo es am Ende ein doppeltes Anführungszeichen gibt, müssen sie sein. Sie werden also dupliziert, was sie alle zitiert und verhindert, dass das endgültige doppelte Anführungszeichen unbeabsichtigt zitiert wird

Es wird eine minimale Übereinstimmung für den ersten Abschnitt vorgenommen, so dass der letzte. *? frisst nicht in Übereinstimmung mit den endgültigen Backslashes

Ausgabe

Diese Eingänge erzeugen also die folgenden Ausgänge

Hallo

Hallo

\ hallo \ 12 \ 3 \

\ hallo \ 12 \ 3 \

Hallo Welt

"Hallo Welt"

\"Hallo\"

\\"Hallo\\\"

\"Hallo Welt

"\\"Hallo Welt"

\"Hallo Welt\

"\\"Hallo Welt\\"

Hallo Welt\\

"Hallo Welt\\\\"

Matt Vukomanovic
quelle
1
Eine kleine Korrektur: Wenn das Original leer ist, müssen Sie ein Paar doppelte Anführungszeichen ""anstelle einer leeren Zeichenfolge zurückgeben, damit die Befehlszeile weiß, dass ein Argument vorhanden ist. Davon abgesehen funktioniert das perfekt!
Joey Adams
Es muss ein Fehler vorliegen ... Eingabe : <a>\n <b/>\n</a>. Ausgabe : <a>\n <b/>\n</a>. Sieht so aus, als würden äußere Qouten fehlen! Mache ich etwas falsch? ( \nbedeutet Newline, natürlich, SO Kommentare sind nicht wirklich Newline-freundlich)
Vojta
Ich hatte noch nie daran gedacht, mit einer neuen Zeile zu streiten. Ich kann hier anscheinend keinen Code einfügen. Ich werde meine Antwort so ändern, dass sie sowohl das Original als auch eine enthält, die neue Zeilen verarbeitet
Matt Vukomanovic
6

Ich hatte auch Probleme damit. Anstatt Argumente zu analysieren, habe ich die vollständige ursprüngliche Befehlszeile übernommen und die ausführbare Datei abgeschnitten. Dies hatte den zusätzlichen Vorteil, dass Leerzeichen im Aufruf beibehalten wurden, auch wenn sie nicht benötigt / verwendet werden. Es muss immer noch Fluchten in der ausführbaren Datei jagen, aber das schien einfacher als die Argumente.

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);
Jeremy Murray
quelle
1
Verwandelte Ihren Code in eine C-Funktion:LPWSTR GetArgStrFromCommandLine(LPWSTR c) {if (*c++ != L'"') c = wcspbrk(--c, L" \t\r\n\v\f"); else while (*c && *c++ != L'"') if (*c == L'\\') ++c; return c;}
7vujy0f0hy
6

Ich habe eine C ++ - Funktion aus den Befehlszeilenargumenten "Jeder zitiert" falsch portiert .

Es funktioniert gut, aber Sie sollten beachten, dass cmd.exedie Befehlszeile unterschiedlich interpretiert wird. Wenn ( und nur wenn , wie der ursprüngliche Autor des Artikels angegeben hat) Ihre Befehlszeile von cmd.exeIhnen interpretiert wird , sollten Sie auch Shell-Metazeichen maskieren.

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

Ich habe Ihnen ein kleines Beispiel geschrieben, um Ihnen zu zeigen, wie Sie Escapezeichen in der Befehlszeile verwenden.

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

Und hier ist eine Testmethode:

    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

Der Punkt ist, jedes Argument mit doppelten Anführungszeichen ("" arg "") zu versehen und alle Anführungszeichen innerhalb des arg-Werts durch ein maskiertes Anführungszeichen (test "123") zu ersetzen.

HABJAN
quelle
Ihre Beispiele funktionieren, @ "\ test" jedoch nicht und @ "test \" bricht mit Win32Exception ab. Letzteres ist in meiner Arbeit durchaus üblich, wenn ich Pfade als Argumente übergebe.
Hultqvist
1
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;
}
Macropas
quelle
0

Fügt gute Argumente hinzu, entkommt aber nicht. Kommentar in Methode hinzugefügt, wohin die Escape-Sequenz gehen soll.

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();
}
Chuck Savage
quelle
1
Ich fürchte, Ihr Code umschließt die Argumente nur in Anführungszeichen, aber er entgeht überhaupt nicht. Wenn ich my.exe "arg1\" \"arg2"ein einziges Argument geben arg1" "arg2würde, würde Ihr Code zwei Argumente erzeugen arg1undarg2
hultqvist
Ok, ich habe nicht dagegen getestet. Ich nehme an, es gibt einen Grund dafür, arg1" "arg2obwohl ich mir nicht vorstellen kann, warum. Ihr Recht, ich hätte sowieso dort fliehen sollen, ich werde diesen Thread sehen, um zu sehen, wer den besten Mechanismus dafür findet.
Chuck Savage
Ich kann mir zwei vorstellen. 1: Jemand mit schlechten Absichten versucht, Ihr Programm dazu zu bringen, gefährliche Befehle auszuführen. 2: Übergeben des ArgumentsJohn "The Boss" Smith
hultqvist
0

Ein alternativer Ansatz

Wenn Sie ein komplexes Objekt wie verschachteltes JSON übergeben und die Kontrolle über das System haben, das die Befehlszeilenargumente empfängt, ist es viel einfacher, die Befehlszeilenargumente einfach als base64 zu codieren und sie dann vom empfangenden System zu decodieren.

Siehe hier: Encode / Decode String zu / von Base64

Anwendungsfall: Ich musste ein JSON-Objekt übergeben, das eine XML-Zeichenfolge in einer der Eigenschaften enthielt, deren Flucht zu kompliziert war. Das hat es gelöst.

Dom
quelle