web-dev-qa-db-ja.com

C#でのコマンドライン引数のエスケープ

短縮版:

引数を引用符で囲み、\および"をエスケープするだけで十分ですか?

コードバージョン

ProcessInfo.Argumentsを使用して、コマンドライン引数string[] argsを別のプロセスに渡します。

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

問題は、引数を配列として取得し、それらを単一の文字列にマージする必要があることです。私のプログラムをだますための引数を作成することができます。

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

この答え によれば、1つの引数をエスケープするために次の関数を作成しましたが、何かを見落としているかもしれません。

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

これで十分ですか、それともフレームワーク機能がありますか?

71
hultqvist

それよりも複雑です!

私は関連する問題を抱えていました(すべてのパラメーターといくつかの余分なパラメーターを渡してバックエンドを呼び出すフロントエンド.exeを書く)ので、私は人々がそれをどのように行うかを見て、あなたの質問に遭遇しました。 arg.Replace (@"\", @"\\").Replace(quote, @"\"+quote)を提案するように、最初はすべてうまくやっているように見えました。

ただし、引数c:\temp a\\bで呼び出すと、これはc:\tempおよびa\\bとして渡され、"c:\\temp" "a\\\\b"でバックエンドが呼び出されます。それは2つの引数c:\\tempa\\\\bになります-私たちが望んだものではありません!私たちはエスケープに熱心でした(windowsはunixではありません!)。

そして、私は詳細に読んで http://msdn.Microsoft.com/en-us/library/system.environment.getcommandlineargs.aspx と実際にそれらのケースがどのように説明されています処理:バックスラッシュは、二重引用符の前のエスケープonlyとして扱われます。

そこに複数の\がどのように処理されるかにひねりがありますが、説明ではしばらくめまいがすることがあります。ここで、エスケープルールの言い直しを試みます:[〜#〜] n [〜#〜]\、その後に"が続きます。エスケープ解除するとき、その部分文字列をint(N/2)\およびiff[〜#〜] n [〜#〜]は奇妙でした。最後に"を追加します。

このようなデコードのエンコードは次のようになります。引数の場合、0以上の\に続く"の各部分文字列を見つけて、2回ごとの\に置き換えます。その後に\"が続きます。そのようにすることができます:

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

それで全部です...

PS。 ...not。待って、待って-さらにあります! :)

エンコードは正しく行いましたが、すべてのパラメーターを二重引用符で囲んでいるため(いくつかのパラメーターにスペースがある場合)、ひねりがあります。境界の問題があります-パラメーターが\で終わる場合、"を追加すると、引用符を閉じる意味が失われます。例c:\one\ twoc:\one\およびtwoに解析され、"c:\one\" "two"に再アセンブルされ、1つの引数として理解されます(誤)c:\one" two(私は試しましたそれ、私はそれを構成していません)。したがって、さらに必要なのは、引数が\で終わっているかどうかを確認することであり、そうであればdouble末尾のバックスラッシュの数です。

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

私の答えはNas Banovの答えに似ていましたが、必要な場合にのみ二重引用符が必要でした。

余分な二重引用符を削除する

私のコードは、二重引用符をいつでも不必要に配置することを節約します。これは重要です*パラメータの文字制限に近づいたとき。

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

説明

バックスラッシュおよび二重引用符を正しくエスケープするには、任意のインスタンスを置き換えることができます複数のバックスラッシュの後に単一の二重引用符が続く:

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

元のバックスラッシュ+ 1と元の二重引用符の2倍つまり、 '\' + originalbackslashes + originalbackslashes + '"'。$ 0には元のbackslashesと元のdouble quoteので、置換が読みやすくなります。

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

これは、空白を含む行全体にのみ一致します。

一致する場合、二重引用符を先頭と末尾に追加します。

引数の最後に元々バックスラッシュがあった場合、引用符は付けられませんが、必要な最後に二重引用符。したがって、それらは複製され、すべてを引用し、最終的な二重引用符を誤って引用することを防ぎます

最初のセクションに対して最小限のマッチングが行われるため、最後の。*?最後のバックスラッシュ

出力

したがって、これらの入力は次の出力を生成します

こんにちは

hello

\ hello\12\3 \

\ hello\12\3 \

こんにちは世界

"こんにちは世界"

\"こんにちは\"

\\ "hello \\\"

\"こんにちは世界

"\\" hello\world "

\"こんにちは世界\

"\\" hello \\\ world \\ "

こんにちは世界\\

"hello world \\\\"

29

私もこれに関する問題に直面していました。引数を解析する代わりに、オリジナルのコマンドライン全体を取り、実行可能ファイルを削除しました。これには、不要であるか使用されていない場合でも、呼び出しで空白を保持するという追加の利点がありました。実行可能ファイルでエスケープを追跡する必要がありますが、それは引数よりも簡単に思えました。

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

誰もがコマンドライン引数を間違った方法で引用しています 記事からC++関数を移植しました。

正常に動作しますが、cmd.exeは、コマンドラインを異なる方法で解釈します。 (とifのみで、前述の記事の元の著者のように)コマンドラインはcmd.exeまた、シェルのメタキャラクターをエスケープする必要があります。

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

コマンドラインのエンコード/エスケープに関するほとんどの問題を処理する小さなプロジェクトをGitHubに公開しました。

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

CommandLineEncoder.Utils.cs クラスと、エンコード/デコード機能を検証する単体テストがあります。

3
Eric P

コマンドラインでエスケープ文字を使用する方法を示す小さなサンプルを作成しました。

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

そして、ここにテスト方法があります:

    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

ポイントは、各引数を二重二重引用符( "" arg "")でラップし、arg値内のすべての引用符をエスケープされた引用符(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

代替アプローチ

ネストされたJSONなどの複雑なオブジェクトを渡し、コマンドライン引数を受け取るシステムを制御できる場合、コマンドラインarg/sをbase64としてエンコードし、受信システムからデコードする方がはるかに簡単です。

ここを参照してください: Base64との文字列のエンコード/デコード

ユースケース:エスケープが非常に複雑なプロパティの1つにXML文字列を含むJSONオブジェクトを渡す必要がありました。これで解決しました。

0
Dom

引数を追加するという素晴らしい仕事をしますが、エスケープしません。メソッドにエスケープシーケンスが必要なコメントを追加しました。

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