Barra invertida e citação em argumentos de linha de comando

Está seguindo comportamento algum recurso ou um bug no c # .net?

Aplicação de teste:

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

Execute-o com argumentos de linha de comando:

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

Em resultado recebe:

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

Há barras invertidas ausentes e as cotações não removidas em argumentos passados ​​para o método Main (). Alguém sabe sobre a solução correta, exceto a análise manual da CommandLine?

De acordo com este artigo de Jon Galloway , pode haver um comportamento estranho ao usar barras invertidas em argumentos de linha de comando.
Mais notavelmente ele menciona que ” A maioria dos aplicativos (incluindo aplicativos .Net) usam CommandLineToArgvW para decodificar suas linhas de comando. Ele usa regras de escape malucas que explicam o comportamento que você está vendo.

Ele explica que o primeiro conjunto de barras invertidas não requer escape, mas as barras invertidas que vêm depois de caracteres alfa (talvez numéricos também?) Exigem escape e que aspas sempre precisam ser escapadas.

Com base nessas regras, acredito que para obter os argumentos que você deseja, você teria que passá-los como:

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

“Na verdade”.

Eu escapei do problema da outra maneira … Em vez de obter argumentos já pareados, estou obtendo a cadeia de argumentos como está e, em seguida, estou usando meu próprio analisador:

 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 = "\\-(?\\w+)\\s*=\\s*(\"(?[^\"]*)\"|(?[^\\-]*))\\s*"; private static Dictionary ParseString(string value) { var regex = new Regex(ParameterQuery); return regex.Matches(value).Cast().ToDictionary(m => m.Groups["key"].Value, m => m.Groups["value"].Value); } 

este conceito permite que você digite citações sem prefixo de escape

Depois de muita experimentação, isso funcionou para mim. Eu estou tentando criar um comando para enviar para a linha de comando do Windows. Um nome de pasta vem depois da opção -graphical no comando, e como ele pode ter espaços, ele deve ser colocado entre aspas duplas. Quando usei barras invertidas para criar as cotações, elas saíram como literais no comando. Então, é isso. . . .

 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 é uma string contendo apenas um caracter de aspas duplas. É precedido com @ para torná-lo literal literalmente .

O modelo de comando também é um literal de cadeia literal, e o método string.Format é usado para compilar tudo em strCmdText .

Eu me deparei com este mesmo problema no outro dia e tive um tempo difícil passar por isso. No meu Google, me deparei com este artigo sobre VB.NET (a linguagem do meu aplicativo) que resolveu o problema sem ter que alterar qualquer um dos meus outros códigos com base nos argumentos.

Nesse artigo, ele se refere ao artigo original que foi escrito para C #. Aqui está o código real, você passa isso Environment.CommandLine() :

C #

 class CommandLineTools { ///  /// C-like argument parser ///  /// Command line string with arguments. Use Environment.CommandLine /// The args[] array (argv) 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; } ///  /// Cleans quotes from the arguments.
/// All signle quotes (") will be removed.
/// Every pair of quotes ("") will transform to a single quote.
///
/// A string with quotes. /// The same string if its without quotes, or a clean string if its with quotes. 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 '''  ''' C-like argument parser '''  ''' Command line string with arguments. Use Environment.CommandLine ''' The args[] array (argv) 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 '''  ''' Cleans quotes from the arguments.
''' All signle quotes (") will be removed.
''' Every pair of quotes ("") will transform to a single quote.
'''
''' A string with quotes. ''' The same string if its without quotes, or a clean string if its with quotes. 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

Isso funciona para mim:

Editar: agora funciona corretamente com o exemplo da pergunta.

  ///  /// https://www.pinvoke.net/default.aspx/shell32/CommandLineToArgvW.html ///  ///  ///  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(); }