【问题标题】:Backslash and quote in command-line arguments命令行参数中的反斜杠和引号
【发布时间】:2012-03-06 11:10:49
【问题描述】:

以下行为是 C# .NET 中的某个特性还是错误?

测试应用:

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,正确的解决方法是什么?

【问题讨论】:

  • 一般来说,与语言中的错误相比,代码中的错误更多
  • 请在程序中找到一个错误,其来源在原始帖子中显示。我会很感激的。
  • 它看起来是一个特征:msdn.microsoft.com/en-us/library/… 如果双引号跟在两个或偶数个反斜杠之后,则将每个前面的反斜杠对替换为一个反斜杠并删除双引号。如果双引号后跟奇数个反斜杠,包括一个,则前面的每一对都被一个反斜杠替换,其余的反斜杠被删除;但是,在这种情况下,双引号不会被删除。

标签: c# .net


【解决方案1】:

根据article by Jon Galloway,在命令行参数中使用反斜杠时可能会出现奇怪的行为。

最值得注意的是,它提到“大多数应用程序(包括 .NET 应用程序)使用 CommandLineToArgvW 来解码它们的命令行。它使用疯狂的转义规则来解释您所看到的行为。

它解释了第一组反斜杠不需要转义,但是在 alpha(可能也是数字?)字符之后的反斜杠需要转义,并且引号总是需要转义。

根据这些规则,我相信要获得您想要的参数,您必须将它们传递为:

a "b" "\\x\\\\" "\x\\"

确实“怪诞”。


2011 年,MS blog entry 讲述了疯狂转义规则的完整故事:“每个人都以错误的方式引用命令行参数

Raymond also had something to say on the matter(早在 2010 年):“CommandLineToArgvW 对引号和反斜杠的奇怪处理是怎么回事

这种情况一直持续到 2020 年,Everyone quotes command line arguments the wrong way 中描述的转义规则在 2020 年和 Windows 10 中仍然正确。

【讨论】:

  • 对于未来的读者:CommandLineToArgvW documentation 要么具有误导性,要么恕我直言错误。幸运的是,CommandLineToArgvW 和 .Net 的行为与 VS documentation 所描述的完全一样。更好的描述:Everyone quotes command line arguments the wrong way
  • 附带说明:如果您将命令行占位符“%1”(包括双引号)添加到快捷方式,则它会转换为文件或文件夹的完整路径。但是,对于驱动器,它返回 C:"(3 个字符)。假设是 C:\,就像您拖放该项目时一样。符合文章。奇怪的是,Environment.CommandLine 返回:["可执行文件" C: ] (不带双引号和去除尾随反斜杠的参数)。
【解决方案2】:

前几天我遇到了同样的问题,并且很难解决。在我的谷歌搜索中,我遇到了this article regarding VB.NET(我的应用程序的语言),它解决了这个问题,而无需根据参数更改我的任何其他代码。

在那篇文章中,他提到了为 C# 编写的 original article。这是实际的代码,你把它传递给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" char 30 更改为记录拆分分隔符。这使我能够支持引号中的多行参数,但也可以使用不会在用户输入中找到的分隔符。
【解决方案3】:

我已经以另一种方式逃避了这个问题......

我没有得到已经解析的参数,而是按原样获取参数字符串,然后我使用自己的解析器:

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

此概念允许您键入不带转义前缀的引号。

【讨论】:

    【解决方案4】:

    经过大量实验,这对我有用。我正在尝试创建一个发送到 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 是一个只包含双引号字符的字符串。它前面带有@,使其成为verbatim string literal

    命令模板也是一字不差的字符串字面量,使用string.Format方法将所有内容编译成strCmdText

    【讨论】:

    • 你认为@"" + (char) 34 在做什么?为什么不直接写"\""?而且,为什么您首先需要用户 string.Format 来插入它们?您可以将" 在逐字字符串中写为@"-graphical ""{0}"" ..."
    【解决方案5】:

    这对我有用,并且与问题中的示例一起正常工作。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-08-26
      • 1970-01-01
      • 2016-06-15
      • 2015-03-25
      • 2019-12-05
      • 1970-01-01
      • 2016-03-24
      相关资源
      最近更新 更多