oconnor663 / duct.rs

a Rust library for running child processes
MIT License
795 stars 34 forks source link

Can't pass long format args with single quotes #83

Closed yuvadm closed 4 years ago

yuvadm commented 4 years ago

I'm trying to invoke a command of the following format:

$ foo --long-arg='A','B'

But when I try to do something like this:

cmd("foo", vec!["--long-arg='A','B','C'"])

It seems what actually gets executed is:

$ foo --long-arg=\'A\',\'B\',\'C\'

Where are these escape symbols coming from?

oconnor663 commented 4 years ago

In general, extra quotes are not required when using Duct. All of your command line arguments wind up directly in the child's argv without any substitution or escaping. So the equivalent of

foo --long-arg='A','B'

would be

cmd("foo", vec!["--long-arg=A,B"])

What's happening to the single quotes the first example is that the shell (probably Bash) is parsing them and removing them, so the child process ultimately sees --long-arg=A,B. In the second example, there is no shell in between. The child will see the exact bytes in your argument string. That is, the exact contents of str::as_bytes(). (Ignoring issues with interior null characters.)

Note that Rust itself might still requires you to escape " and \ characters with backslashes. Those backslashes get removed by the Rust compiler when it parses them, so the remark about .as_bytes() still holds.

It's hard to say where the backslashes come from in your printed example, because I don't know how you printed it.

yuvadm commented 4 years ago

@oconnor663 Thanks for the detailed response!

So here's the thing, actually my args are of the following format:

foo --long-arg='A: a','B: b'

(I'm basically passing custom HTTP headers to a media player)

What would be the right way to build the arg list in this case?

oconnor663 commented 4 years ago

You should be able to simply omit the single quotes, and it will just work:

cmd("foo", vec!["--long-arg=A: a,B: b"])

Or:

cmd!("foo", "--long-arg=A: a,B: b")

You might be worried about the spaces in there. Maybe you've seen a bunch of issues on the command line where whitespace breaks everything, unless you put just the right quotes in just the right places. (And of course, mpv is telling you to use single quotes in its documentation.) But the root cause of most of those issues is that you're going through a shell somehow, and the shell desperately wants to split everything on whitespace. That's the case when you paste something onto the command line of course, and in many programming languages it's also the case when you use a function like system() to execute a command that's a single large string. But that's not the case here. This is just a Rust program telling the OS to spawn some other program, with a list of arguments, without a shell anywhere in between.

(You will notice that if you try to put special characters like | or > into Duct commands, they have no special effect. They're just another character in the arguments list, and you usually wind up with a file "|" not found error or something like that. That's again because there's no shell involved, and the special meaning of those characters is something that comes from the shell.)

yuvadm commented 4 years ago

@oconnor663 Thanks again for the in-depth explanation! It's very counter-intuitive but you're absolutely correct and it seems that version works well :+1:

oconnor663 commented 4 years ago

Sure thing. I'm glad it's working :)

For another comparison, Python's subprocess.run() API also works this way. Unless you use the shell=True argument. You can play with spawning echo commands through that interface to get a feel for it.