Std::process is escaping a raw string literal when I don't want it to

playground: Rust Playground

fn main() {
    let test = r#"powershell -command "Get-WmiObject -class Win32_Product""#;
    println!("{:?}",test);
}

gives "powershell -command \"Get-WmiObject -class Win32_Product\""

How do I get it to stop inserting the \ ?

Try println! with the {} (e.g Display) format instead. The &str {:?} (Debug) format is quoted with escapes added for clarity.

1 Like

The problem is I'm sending it to std::process::Command::new as an args and it's genuinely sending the \

I'll put up example code on the playground (it'll be a little weird because the playground uses linux and it's going to echo a windows command, but it should do as an example

ok, it only manifests in windows, the linux version seems to work correctly.

I've put a working example on the playground that you (being anyone on windows who wants to help) can save and run on a windows computer to verify this behavior

this is the output I get with this version on windows

test \"something in quotes\"

Given the differing behavior I'm going to guess it's a problem in std::process not in the raw string literals?

Note that quotes exist in the shell syntax, but not the logical command. The quotes are only part of the keyboard-based UI, and not semantics of the command.

For example:

ls -l "bar baz" quz

is

Command::new("ls").arg("-l").arg("bar baz").arg("quz")

In Unix the command can't even know about existence of the quotes:

ls "-"'l' "b"'ar b'"az" q'u'z

is 100% identical as the first example. The ls command has no way of knowing where the quotes were.

Windows is weirder, since cmd.exe has MSDOS legacy. I don't know if it has been "fixed" in powershell.

Anyway, this:

powershell -command "Get-WmiObject -class Win32_Product"

should be quivalent to:

Command::new("powershell").arg("-command").arg("Get-WmiObject -class Win32_Product")
1 Like

But it's not, that's the problem

an example command

let test: String = simple_run_command::run("cmd",     &["/c", r#"powershell -command "Get-WmiObject -class Win3

to verify it’s escaping the quotes compare the output of these commands in cmd.exe to the output of the above command

powershell -command \"Get-WmiObject -class Win32_Product\"
powershell -command "Get-WmiObject -class Win32_Product"

Moving over to internals.rust as this seems like a bug

No, it's not a bug.

it's escaping on windows, but only inner quotes, but doesn't escape on linux

You're trying to execute a command as if it was interpreted using shell syntax, and you don't understand the concept of command executed without a shell.

and if I can post a .vbs file that shows this should work would you believe me then?

Proof it's launching escaped without a shell

fn test_stdin(){
    let program = "echo_test"; // this is a custom program that takes a line from stdin and prints it to stdout
    let args = [r#"test "something in quotes" "#];
    let std_in_string = "test";
    //let std_in_string = "test\n";
    let test = run(&program,&args,&std_in_string);
    println!("{}",test);
}

1 Like

Sorry, I see, you are executing cmd.exe!

In that case, the problem is technically unsolvable, and that's a design flaw in Windows that has no solution due to backwards compatibility going all the way back to MS-DOS.

Commands executed in cmd.exe have no concept of quoting, no concept of escaping, because separation of arguments technically doesn't even exist. The command gets entire command as a single string (GetCommandLineW).

So what other systems that emulate concept of separate arguments do, is they hope command won't use GetCommandLineW literally, and instead will use CommandLineToArgvW, which defines its own (very quirky) syntax of separating arguments.

Your problem is that cmd.exe does not use CommandLineToArgvW, but parses its arguments itself, and the switch /c is magical and changes syntax of the rest of the line. Supporting this in a standard library like Rust's just doesn't make sense, since it would have to hardcode a specific exceptions for knowledge that this cmd.exe with this /c argument uses a custom parser, which is not compatible with CommandLineToArgvW.

2 Likes

cmd isn't the problem. I just posted an example where there's a shell-less exe getting \" escaped arguments

I can provide the source for echo_test if you want to check that yourself, but it's irrelevant for this test since in this instance it just sits there waiting for a newline it never gets.

Command::new("echo").args(r#"this is "quoted""#);

has to execute command echo with 1 argument this is "quoted". It has to make it different from 3-arg:

Command::new("echo").args("this").arg("is").("quoted");

This in general is impossible to express in cmd.exe, since there's no API to pass separate arguments. What the executed command gets is just one string, and its syntax is undefined (the command can interpret it however it wants, and parses any number of arguments out of it however it imagines).

But in the best effort to preserve the spaces in one arg, the stdlib has to put them in a quoted string. But then it has to differentiate between quote ending the string, and quote inside. To do that it assumes CommandLineToArgvW syntax for it, so it adds a backslash between non-string-terminating quote.

can you more precisely define the "it" here? are you referring to cmd.exe or stdlib

Here's a cmd free example (it tries to open notepad with a bad path)
You can check the arguments it got with Microsoft's Process Explorer

use std::process::{Command, Stdio};
use std::{thread, time};


fn main() {
    let mut child = Command::new("notepad")
        .args(&[r#"test "something in quotes" "#])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .expect("failed to execute child");
    let check_every = time::Duration::from_millis(10);
    loop {
        match child.try_wait() {
            Ok(Some(_status)) => {break;},  // finished running
            Ok(None) => {}                  // still running
            Err(e) => {panic!("error attempting to wait: {}", e)},
        }
        thread::sleep(check_every);
    }

}

keep in mind it properly passes the quotes on linux, it's only doing the extra escaping on windows

Rust's stdlib.

When you execute

Command::new("notepad")
        .args(&[r#"test "something in quotes" "#])
        .stdin(Stdio::piped())

the Notepad gets GetCommandLineW with

notepad "test \"something in quotes\" "

and it could call CommandLineToArgvW to get:

  1. notepad
  2. test "something in quotes"

as separate parsed arguments.

Note that if that Command execution was serialized as:

notepad "test "something in quotes" "

or

notepad test "something in quotes" 

then the meaning of that command would be different for CommandLineToArgvW. It'd be ["test ", "something in quotes",""] or ["test", "something in quotes"], instead of [r#"test "something in quotes" "#].

Rust has no way of knowing if the executed command will use GetCommandLineW, or some other parser.

2 Likes