命令行参数中的反斜杠和引号

29
这种行为是C# .NET中的某个特性还是一个bug?
测试应用程序:
using System;
using System.Linq;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Arguments:");
            foreach (string arg in args)
            {
                Console.WriteLine(arg);
            }

            Console.WriteLine();
            Console.WriteLine("Command Line:");
            var clArgs = Environment.CommandLine.Split(' ');
            foreach (string arg in clArgs.Skip(clArgs.Length - args.Length))
            {
                Console.WriteLine(arg);
            }

            Console.ReadKey();
        }
    }
}

使用命令行参数运行它:

a "b" "\\x\\" "\x\"
我理解您需要的翻译如下:

我收到的结果是:

Arguments:
a
b
\\x\
\x"

Command Line:
a
"b"
"\\x\\"
"\x\"

在传递给方法 Main() 的参数中缺少反斜杠并且未移除引号,除手动解析 Environment.CommandLine 外,有什么正确的解决方法吗?


2
一般来说,代码中出现问题的可能性要比语言本身存在问题的可能性高得多。 - Daniel Mann
4
请在原始帖子中所示的程序中找出错误,我会非常感激。 - Mykola Kovalchuk
2
这似乎是一个特性: http://msdn.microsoft.com/en-us/library/system.environment.getcommandlineargs.aspx 如果在两个或偶数数量的反斜杠后面跟着双引号,则每个连续的反斜杠对将被替换为一个反斜杠,且双引号将被移除。如果在奇数数量的反斜杠(包括一个)后面跟着双引号,则每个前面的反斜杠对将被替换为一个反斜杠,并移除剩余的反斜杠;但在这种情况下,双引号不会被移除。 - Mykola Kovalchuk
5个回答

33
根据Jon Galloway的文章,在使用命令行参数时可能会遇到奇怪的行为。
特别是它提到:“大多数应用程序(包括.NET应用程序)使用CommandLineToArgvW来解码其命令行。它使用疯狂的转义规则,这解释了你所看到的行为。”
文章解释说,第一组反斜杠不需要转义,但是在字母(也许数字也是?)字符之后出现的反斜杠需要转义,并且引号始终需要转义。
基于这些规则,我认为要获得想要的参数,您需要将它们传递为:
a "b" "\\x\\\\" "\x\\"

“Whacky”确实很古怪。

2011年,一篇微软博客文章讲述了关于疯狂转义规则的完整故事:“每个人都在错误地引用命令行参数

雷蒙德(Raymond)在2010年就曾对此发表过看法:“CommandLineToArgvW为什么会奇怪地处理引号和反斜杠符号

到2020年,情况仍然存在,而每个人都在错误地引用命令行参数中描述的转义规则仍然适用于2020年和Windows 10。


4
未来的读者注意:CommandLineToArgvW文档 可能误导了,或者在我的观点中是错误的。幸运的是,CommandLineToArgvW和 .Net 的行为与 VS文档 描述的完全相同。更好的描述请参考:每个人都以错误的方式引用命令行参数 - T S
1
附带说明:如果您在快捷方式中添加命令行占位符"%1"(包括双引号),则会将其转换为文件或文件夹的完整路径。但对于驱动器,则会返回C:"(3个字符)。应该是C:\,就像您拖放该项时一样。与文章一致。奇怪的是,Environment.CommandLine返回:[ "executable" C: ](没有双引号和尾随反斜杠的参数)。 - Andrei Kalantarian

6
我最近遇到了同样的问题,我很难解决它。在搜索过程中,我发现了与我的应用程序语言VB.NET相关的这篇文章,该文章解决了这个问题,并且不需要更改我的其他代码。
在那篇文章中,他提到了原始文章是为C#编写的。以下是实际代码,你需要将Environment.CommandLine()传递给它:
C#
class CommandLineTools
{
    /// <summary>
    /// C-like argument parser
    /// </summary>
    /// <param name="commandLine">Command line string with arguments. Use Environment.CommandLine</param>
    /// <returns>The args[] array (argv)</returns>
    public static string[] CreateArgs(string commandLine)
    {
        StringBuilder argsBuilder = new StringBuilder(commandLine);
        bool inQuote = false;

        // Convert the spaces to a newline sign so we can split at newline later on
        // Only convert spaces which are outside the boundries of quoted text
        for (int i = 0; i < argsBuilder.Length; i++)
        {
            if (argsBuilder[i].Equals('"'))
            {
                inQuote = !inQuote;
            }

            if (argsBuilder[i].Equals(' ') && !inQuote)
            {
                argsBuilder[i] = '\n';
            }
        }

        // Split to args array
        string[] args = argsBuilder.ToString().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);

        // Clean the '"' signs from the args as needed.
        for (int i = 0; i < args.Length; i++)
        {
            args[i] = ClearQuotes(args[i]);
        }

        return args;
    }

    /// <summary>
    /// Cleans quotes from the arguments.<br/>
    /// All signle quotes (") will be removed.<br/>
    /// Every pair of quotes ("") will transform to a single quote.<br/>
    /// </summary>
    /// <param name="stringWithQuotes">A string with quotes.</param>
    /// <returns>The same string if its without quotes, or a clean string if its with quotes.</returns>
    private static string ClearQuotes(string stringWithQuotes)
    {
        int quoteIndex;
        if ((quoteIndex = stringWithQuotes.IndexOf('"')) == -1)
        {
            // String is without quotes..
            return stringWithQuotes;
        }

        // Linear sb scan is faster than string assignemnt if quote count is 2 or more (=always)
        StringBuilder sb = new StringBuilder(stringWithQuotes);
        for (int i = quoteIndex; i < sb.Length; i++)
        {
            if (sb[i].Equals('"'))
            {
                // If we are not at the last index and the next one is '"', we need to jump one to preserve one
                if (i != sb.Length - 1 && sb[i + 1].Equals('"'))
                {
                    i++;
                }

                // We remove and then set index one backwards.
                // This is because the remove itself is going to shift everything left by 1.
                sb.Remove(i--, 1);
            }
        }

        return sb.ToString();
    }
}

VB.NET:

Imports System.Text

' Original version by Jonathan Levison (C#)'
' http://sleepingbits.com/2010/01/command-line-arguments-with-double-quotes-in-net/
' converted using http://www.developerfusion.com/tools/convert/csharp-to-vb/
' and then some manual effort to fix language discrepancies
Friend Class CommandLineHelper


    ''' <summary>
    ''' C-like argument parser
    ''' </summary>
    ''' <param name="commandLine">Command line string with arguments. Use Environment.CommandLine</param>
    ''' <returns>The args[] array (argv)</returns>
    Public Shared Function CreateArgs(commandLine As String) As String()
        Dim argsBuilder As New StringBuilder(commandLine)
        Dim inQuote As Boolean = False

        ' Convert the spaces to a newline sign so we can split at newline later on
        ' Only convert spaces which are outside the boundries of quoted text
        For i As Integer = 0 To argsBuilder.Length - 1
            If argsBuilder(i).Equals(""""c) Then
                inQuote = Not inQuote
            End If

            If argsBuilder(i).Equals(" "c) AndAlso Not inQuote Then
                argsBuilder(i) = ControlChars.Lf
            End If
        Next

        ' Split to args array
        Dim args As String() = argsBuilder.ToString().Split(New Char() {ControlChars.Lf}, StringSplitOptions.RemoveEmptyEntries)

        ' Clean the '"' signs from the args as needed.
        For i As Integer = 0 To args.Length - 1
            args(i) = ClearQuotes(args(i))
        Next

        Return args
    End Function


    ''' <summary>
    ''' Cleans quotes from the arguments.<br/>
    ''' All signle quotes (") will be removed.<br/>
    ''' Every pair of quotes ("") will transform to a single quote.<br/>
    ''' </summary>
    ''' <param name="stringWithQuotes">A string with quotes.</param>
    ''' <returns>The same string if its without quotes, or a clean string if its with quotes.</returns>
    Private Shared Function ClearQuotes(stringWithQuotes As String) As String
        Dim quoteIndex As Integer = stringWithQuotes.IndexOf(""""c)
        If quoteIndex = -1 Then Return stringWithQuotes

        ' Linear sb scan is faster than string assignemnt if quote count is 2 or more (=always)
        Dim sb As New StringBuilder(stringWithQuotes)
        Dim i As Integer = quoteIndex
        Do While i < sb.Length
            If sb(i).Equals(""""c) Then
                ' If we are not at the last index and the next one is '"', we need to jump one to preserve one
                If i <> sb.Length - 1 AndAlso sb(i + 1).Equals(""""c) Then
                    i += 1
                End If

                ' We remove and then set index one backwards.
                ' This is because the remove itself is going to shift everything left by 1.
                sb.Remove(System.Math.Max(System.Threading.Interlocked.Decrement(i), i + 1), 1)
            End If
            i += 1
        Loop

        Return sb.ToString()
    End Function
End Class

这对我的目的来说很有效,我将新行分隔符从“\n”字符30更改为记录分隔符。这使我能够在引号中支持多行参数,并使用不会在用户输入中找到的分隔符。 - b.pell

2

我用另一种方法解决了这个问题...

我没有使用已经解析好的参数,而是直接获取参数字符串,然后使用自己的解析器:

static void Main(string[] args)
{
    var param = ParseString(Environment.CommandLine);
    ...
}

// The following template implements the following notation:
// -key1 = some value   -key2 = "some value even with '-' character "  ...
private const string ParameterQuery = "\\-(?<key>\\w+)\\s*=\\s*(\"(?<value>[^\"]*)\"|(?<value>[^\\-]*))\\s*";

private static Dictionary<string, string> ParseString(string value)
{
   var regex = new Regex(ParameterQuery);
   return regex.Matches(value).Cast<Match>().ToDictionary(m => m.Groups["key"].Value, m => m.Groups["value"].Value);
}

这个概念允许您在不使用转义前缀的情况下输入引号。

0

这对我来说可行,而且它与问题中的示例正确地工作。

    /// <summary>
    /// https://www.pinvoke.net/default.aspx/shell32/CommandLineToArgvW.html
    /// </summary>
    /// <param name="unsplitArgumentLine"></param>
    /// <returns></returns>
    static string[] SplitArgs(string unsplitArgumentLine)
    {
        int numberOfArgs;
        IntPtr ptrToSplitArgs;
        string[] splitArgs;

        ptrToSplitArgs = CommandLineToArgvW(unsplitArgumentLine, out numberOfArgs);

        // CommandLineToArgvW returns NULL upon failure.
        if (ptrToSplitArgs == IntPtr.Zero)
            throw new ArgumentException("Unable to split argument.", new Win32Exception());

        // Make sure the memory ptrToSplitArgs to is freed, even upon failure.
        try
        {
            splitArgs = new string[numberOfArgs];

            // ptrToSplitArgs is an array of pointers to null terminated Unicode strings.
            // Copy each of these strings into our split argument array.
            for (int i = 0; i < numberOfArgs; i++)
                splitArgs[i] = Marshal.PtrToStringUni(
                    Marshal.ReadIntPtr(ptrToSplitArgs, i * IntPtr.Size));

            return splitArgs;
        }
        finally
        {
            // Free memory obtained by CommandLineToArgW.
            LocalFree(ptrToSplitArgs);
        }
    }

    [DllImport("shell32.dll", SetLastError = true)]
    static extern IntPtr CommandLineToArgvW(
        [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine,
        out int pNumArgs);

    [DllImport("kernel32.dll")]
    static extern IntPtr LocalFree(IntPtr hMem);

    static string Reverse(string s)
    {
        char[] charArray = s.ToCharArray();
        Array.Reverse(charArray);
        return new string(charArray);
    }

    static string GetEscapedCommandLine()
    {
        StringBuilder sb = new StringBuilder();
        bool gotQuote = false;
        foreach (var c in Environment.CommandLine.Reverse())
        {
            if (c == '"')
                gotQuote = true;
            else if (gotQuote && c == '\\')
            {
                // double it
                sb.Append('\\');
            }
            else
                gotQuote = false;

            sb.Append(c);
        }

        return Reverse(sb.ToString());
    }

    static void Main(string[] args)
    {
        // Crazy hack
        args = SplitArgs(GetEscapedCommandLine()).Skip(1).ToArray();
    }

0
经过多次尝试,这对我有效。我正在尝试创建一个要发送到Windows命令行的命令。在命令中,-graphical选项之后是一个文件夹名称,由于它可能包含空格,所以必须用双引号括起来。当我使用反斜杠来创建引号时,它们在命令中被视为文字。因此,就变成了这样……
string q = @"" + (char) 34;
string strCmdText = string.Format(@"/C cleartool update -graphical {1}{0}{1}", this.txtViewFolder.Text, q);
System.Diagnostics.Process.Start("CMD.exe", strCmdText);

q 是一个仅包含双引号字符的字符串。它前面加上 @ 以使其成为 逐字字符串文字

命令模板也是逐字字符串文字,使用 string.Format 方法将所有内容编译成 strCmdText


2
你认为@"" + (char)34是做什么的? 为什么不直接写"\""?而且,为什么需要使用string.Format来进行插入?你可以在逐字字符串中写入 @"- graphical ""{0}""..." - Mike Caron

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