在C#中转义命令行参数

83

简短版本:

将参数用引号括起来并转义\"是否足够?

代码版本:

我想使用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"

根据这个回答,我创建了下面的函数来转义一个参数,但我可能漏掉了一些东西。

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

这样足够好吗,还是有任何框架函数可以实现这个功能?


5
你尝试原样传递了吗?我认为如果它能被传递给你,它也可以被传递给另一个命令。如果出现错误,那么你可以考虑进行转义处理。 - Sanjeevakumar Hiremath
2
@Sanjeevakumar 是的,例如:"C:\Documents and Settings\MyPath \" --kill-all-humans \" except fry" 这样不是一个好主意,因为它会产生特权调用。 - hultqvist
1
@Sanjeevakumar Main(string[] args) 是一个未转义字符串的数组,因此如果我运行 my.exe "test\"test",arg[0] 将是 test"test - hultqvist
  1. 根据您的第一条评论看来你并不想进行转义,是只想进行逃避吗?
  2. 什么是未转义字符串?当您获得一个类似于 abc"def 的字符串时,它就是 abc"def,现在为什么要对其进行转义呢?如果您正在添加像 "abc" + """" + "def" 这样的内容,这是有意义的。请注意,"""" 转义了 "
- Sanjeevakumar Hiremath
你可能会对我的MedallionShell库感兴趣,它可以自动处理转义和连接进程参数。该实现基于此线程中的一个答案。 - ChaseMedallion
显示剩余7条评论
11个回答

71

但情况比那更为复杂!

我遇到了一个相关的问题(编写前端.exe文件,调用后端时传递所有参数以及一些额外参数),因此我查看了人们如何处理它,并遇到了您的问题。最初,按照您建议的方式进行操作,似乎一切正常 arg.Replace (@"\", @"\\").Replace(quote, @"\"+quote)

然而,当我使用参数c:\temp a\\b时,这将作为c:\tempa\\b传递,导致后端被调用为"c:\\temp" "a\\\\b" - 这是不正确的,因为这将有两个参数c:\\tempa\\\\b - 不是我们想要的!我们在转义方面过于热衷(Windows不是Unix!)。

因此,我详细阅读了http://msdn.microsoft.com/en-us/library/system.environment.getcommandlineargs.aspx,它实际上描述了如何处理这些情况:反斜杠仅在双引号前面被视为转义

在处理多个\时有一个扭曲的方法,解释可能会让人眼花缭乱。我将尝试在此重新表述所述的取消转义规则:假设我们有一个由N\子字符串组成,后跟"。在取消转义时,我们将该子字符串替换为N/2 \,并且如果N是奇数,则在末尾添加"

进行此类解码的编码如下:对于参数,查找每个由0个或多个\子串组成的,后跟",并将其替换为两倍的\,后跟\"。我们可以这样做:

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

就这些啦...

PS. ... 不要。等等,还有更多!:)

我们正确地进行了编码,但存在一个问题,因为您将所有参数都用双引号括起来(以防其中某些参数包含空格)。存在一个边界问题 - 如果参数以 \ 结尾,则在其后添加 " 将破坏闭合引号的含义。例如,c:\one\ two 解析为 c:\one\two,然后将重新组装为 "c:\one\" "two",这将被误解为一个参数 c:\one" two(我试过了,我没有编造)。所以除此之外我们还需要检查参数是否以 \ 结尾,如果是,则在结尾处将反斜杠的数量加倍,如下所示:

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

7
+1 为了解释这种疯狂的行为。但是在上面的匹配表达式中,*+ 不应该在分组括号内吗?否则,$1 替换将永远只是一个单个反斜杠。 - bobince
实际上,我认为这两个替换可以合并成:"\""+Regex.Replace(s, "(\\\\*)(\\\\$|\")", "$1$1\\$2")+"\""。但是我的脑子现在开始沉了,所以如果您能检查一下是否正确,那就太感谢了 :-) - bobince
3
FYI:http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx - quetzalcoatl
1
谢谢你的回答!你能否添加一个TL;DR静态方法来处理所有事情?我真的很喜欢你的回答,但每次需要这些信息时,我都必须阅读并理解它(因为我太蠢了,无法完全记住它)... - vojta
1
@vojta - 很抱歉,已经过去五年了,我不记得细节了。通过重新阅读我写的内容,我猜只需要调用那两行代码就可以了。但是你现在可能对这个案例有更好的理解,为什么不编辑答案并添加 TL-DNR 以供后人参考呢? - Nas Banov
显示剩余2条评论

33

我的回答与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和一个原有的双引号。即 '\' + 原有的反斜杠 + 原有的反斜杠 + '"'。我使用$1$0因为$0拥有原有的反斜杠和原有的双引号,所以使替换更易于阅读。

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

这只能匹配包含空格的整行。

如果匹配成功,则在开头和结尾添加双引号

如果原始参数末尾有反斜杠,则它们不会被引用。现在由于末尾有了双引号,所以需要将它们复制一次,将它们全部引用,并防止意外引用最后一个双引号

它对第一个部分进行最小匹配,以便最后的 .*? 不会吞噬匹配最后的反斜杠

输出

因此,这些输入会产生以下输出

hello

hello

\hello\12\3\

\hello\12\3\

hello world

"hello world"

\"hello\"

\\"hello\\\"

\"hello\ world

"\\"hello\ world"

\"hello\\\ world\

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

hello world\\

"hello world\\\\ "


1
一个小修复:当原始内容为空时,您需要返回一对双引号 "" 而不是空字符串,这样命令行就会知道有一个参数存在。除此之外,这个程序完美运行! - Joey Adams
一定有个 bug...输入: <a>\n <b/>\n</a>. 输出: <a>\n <b/>\n</a>. 看起来缺少外引号!我做错了什么吗?(\n 表示换行,当然,SO 评论并不真正支持换行) - vojta
我从未想过在参数中使用换行符。似乎无法在此处粘贴代码.. 我会更改我的答案,包括原始代码和处理换行符的代码。 - Matt Vukomanovic

8
我已经移植了一个C++函数,来自Everyone quotes command line arguments the wrong way文章。它运行良好,但你应该注意,cmd.exe解释命令行的方式不同。如果(而且只有如果),你的命令行将被cmd.exe解释,你还应该转义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();
}

7
我也遇到了这个问题。与其将参数解析成字符串,我采用了获取完整原始命令行并剪切掉可执行文件的方法。这样做的额外好处是即使调用中不需要或不使用空格,也可以保留调用中的空格。它仍然需要处理可执行文件中的转义字符,但似乎比处理参数更容易些。
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);

1
将您的代码转换为C语言函数: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

4

2
我为您编写了一个小示例,以向您展示如何在命令行中使用转义字符。
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"" ) 包装,并用转义引号 ( test\"123 ) 替换参数值中的所有引号。

你的例子是有效的,但是@"\test"不行,而@"test"会出现Win32Exception异常。当将路径作为参数传递时,后者在我的工作中非常常见。 - 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;
}

1

一种替代方法

如果您正在传递复杂的对象,例如嵌套的JSON,并且您可以控制接收命令行参数的系统,则更容易将命令行参数编码为base64,然后从接收系统解码。

请参见:将字符串编码/解码为Base64

用例:我需要传递一个包含XML字符串的JSON对象,其中一个属性过于复杂而难以转义。这个方法解决了这个问题。


1
现在有一个更好的选择来传递参数给Process.start。如果你像我一样找到了这个问题,并浪费了很多时间阅读这些答案并试图想出自己的解决方案,那就不用再搜索了。
using System;
using System.Diagnostics;
using System.IO;

class Program
{
    public static void Main()
    {
        foreach (var line in File.ReadLines("input.txt"))
        {
            var startInfo = new ProcessStartInfo
            {
                FileName = "/bin/bash",
                RedirectStandardOutput = true,
                UseShellExecute = false
            };
            startInfo.ArgumentList.Add("-c");
            startInfo.ArgumentList.Add("echo -n \"$@\"");
            startInfo.ArgumentList.Add("--");
            startInfo.ArgumentList.Add(line);

            var process = new Process { StartInfo = startInfo };

            process.Start();

            var output = process.StandardOutput.ReadToEnd();

            Console.WriteLine($"Original argument: {line}");
            Console.WriteLine($"Received by bash: {output}");
            Console.WriteLine();
        }
    }
}

input.txt:

hello
\hello\12\3\
hello world
\"hello\"
\"hello\ world
\"hello\\\ world\
hello world\\

输出:

Original argument: hello
Received by bash: hello

Original argument: \hello\12\3\
Received by bash: \hello\12\3\

Original argument: hello world
Received by bash: hello world

Original argument: \"hello\"
Received by bash: \"hello\"

Original argument: \"hello\ world
Received by bash: \"hello\ world

Original argument: \"hello\\\ world\
Received by bash: \"hello\\\ world\

Original argument: hello world\\
Received by bash: hello world\\

看起来是在.NET Code 2.1中引入的。 - undefined
它是否处理了顶部答案中描述的所有特殊情况? - undefined
@hultqvist 我已经更新了我的答案,加入了来自其他答案的测试案例。 - undefined

0

在添加参数方面做得很好,但没有进行转义。在应该进行转义序列的方法中添加了注释。

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

1
很抱歉,您的代码只是在参数周围加上引号,但它并没有进行任何转义。如果我运行 my.exe "arg1\" \"arg2" 并给出一个单一的参数 arg1" "arg2,您的代码将生成两个参数,即 arg1arg2 - hultqvist
好的,我还没有对此进行测试。虽然我无法想象为什么要这样做arg1" "arg2,但我认为肯定有原因。你是对的,我应该在其中加入转义字符,我会关注这个线程,看看谁能提出最好的机制来解决这个问题。 - Chuck Savage
我能想到两个。1:有人存心试图欺骗您的程序以执行危险命令。2:传递参数John“ The Boss”Smith - hultqvist

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接