diff --git a/README.md b/README.md index e18a618..892870d 100644 --- a/README.md +++ b/README.md @@ -464,6 +464,10 @@ chmod +x example.csx ``` ## CHANGELOG +### v3.2.1 +* **Cmd()** on Windows passes the command to cmd.exe as typed. Every `"` used to arrive as `\"`, so quoted arguments, and any `|`, `&` or `>` inside them, broke (`Cmd("az webapp list --query \"[?name=='x'] | [0]\"")`) +* **Bash()** no longer escapes the command's quotes a second time, so `Bash("printf '%s' \"a b\"")` passes `a b` as one argument + ### v3.1.0 * **Cli** now models nested commands, the way `dotnet build` and `dotnet nuget push` do * `.Command(name, help, c => ...)` gives a verb a command line of its own, and commands nest to any depth diff --git a/Tests/CShell.Tests/Command.Tests.cs b/Tests/CShell.Tests/Command.Tests.cs index c937e11..91e1279 100644 --- a/Tests/CShell.Tests/Command.Tests.cs +++ b/Tests/CShell.Tests/Command.Tests.cs @@ -220,6 +220,30 @@ public async Task Test_Bash() Assert.AreEqual("TestA.txt", result.Trim(), "AsString"); } + [TestMethod] + public async Task Test_Cmd_QuotedArguments() + { + // Quotes, the spaces inside them and a | inside them all reach the program as typed. cmd.exe used to get + // every " as \", which also left the | outside any quotes. + CShell shell = new CShell(); + shell.cd(testFolder); + var result = await shell.Cmd("echo x \"[?name=='S'].value | [0]\" y").AsString(); + + // cmd.exe's echo prints the quotes; bash's drops them. + Assert.AreEqual(IsWindows ? "x \"[?name=='S'].value | [0]\" y" : "x [?name=='S'].value | [0] y", result.Trim()); + } + + [TestMethod] + public async Task Test_Bash_QuotedArguments() + { + // "a b" is one argument. Bash used to get \"a b\": two arguments, with literal quotes. + CShell shell = new CShell(); + shell.cd(testFolder); + var result = await shell.Bash("printf '%s\\n' \"a b\" c").AsString(); + + Assert.AreEqual("a b\nc", result.Trim().Replace("\r\n", "\n")); + } + [TestMethod] public async Task Test_Start() { diff --git a/src/CShell.cs b/src/CShell.cs index 9b61756..3cdc03c 100644 --- a/src/CShell.cs +++ b/src/CShell.cs @@ -346,15 +346,16 @@ public Command Cmd(string cmd) Console.WriteLine(cmd); } - List args = new List(); - if (!Echo) + // cmd.exe doesn't split its command line the way other programs do: it runs everything after /C as + // typed. Passed as an argument, the command was escaped for argv parsing, so every " reached cmd.exe + // as \" and quoted arguments (and any |, & or > inside them) broke. Hand cmd.exe the command line + // as-is instead; /S strips just the outer pair of quotes and leaves the command's own alone. + var arguments = $"{(Echo ? string.Empty : "/Q ")}/S /C \"{cmd}\""; + return Command.Run("cmd.exe", Array.Empty(), opt => { - args.Add("/Q"); - } - args.Add("/C"); - args.Add(cmd); - - return Command.Run("cmd.exe", args, SetCommandOptions); + SetCommandOptions(opt); + opt.StartInfo(info => info.Arguments = arguments); + }); } else { @@ -373,11 +374,9 @@ public Command Bash(string cmd) { Console.WriteLine(cmd); } - var args = new List(); - - var escapedArgs = cmd.Replace("\"", "\\\""); - args.Add("-c"); - args.Add(escapedArgs); + // Command.Run escapes each argument, so the command goes as typed. (Escaping its quotes here as well left + // bash seeing \" -- a literal quote -- instead of quotes around an argument.) + var args = new List { "-c", cmd }; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return Command.Run("bash.exe", args, SetCommandOptions); diff --git a/src/CShell.csproj b/src/CShell.csproj index d55e61e..fb16256 100644 --- a/src/CShell.csproj +++ b/src/CShell.csproj @@ -16,8 +16,8 @@ git scripting dotnet csharp CShell - 3.2.0 - 3.2.0 + 3.2.1 + 3.2.1 true snupkg