Get user-copyable CLI given a `Command`

I am building a std::process::Command using mainly args (args). No env or other settings. Given a Command value, how do I get the full command-line string out of it (for logging purposes)?

The idea is to print something that the user can copy-paste on their terminal to run the rough equivalent command manually.

I thought I could use the AsInner trait to get the underlying Command, but there are two problems: both the trait and the fields of this underlying struct are private!

Can this problem be solved at all?

Command::get_args()? As long as you don't want to log a "fully parsed" command line, the Debug impl (e.g. "{args:?}") should be enough, right?

You can use get_program and get_args to retrieve the parts of the command, but how you print them will depend on which shell it's going to be pasted into.

1 Like

Thanks, I hadn't seen that.

Here's a start at solving this. Certainly can be improved over time,

pub fn to_cli(cmd: &tokio::process::Command) -> String {
    use std::ffi::OsStr;
    let cli_args: Vec<String> = cmd
        .as_std()
        .get_args()
        .collect::<Vec<&OsStr>>()
        .into_iter()
        .map(|s| {
            let s = s.to_string_lossy().to_string();
            if s.contains(' ') {
                format!("\"{}\"", s)
            } else {
                s
            }
        })
        .collect();
    format!(
        "{} {}",
        cmd.as_std().get_program().to_string_lossy(),
        cli_args.join(" ")
    )
}

The shell-words crate can be used to quote strings following the rules of Unix (Bourne?) shells.

4 Likes

Just to cover this part: this is so hard it's unlikely to be worth doing it.

First, you have to figure out which shell is being used. You'd need to walk the process tree your process is part of to find the first ancestor process that is a shell. There is (AFAIK) no way to tell if a process is a shell or not, so you would need a comprehensive list of all shells on all platforms.

But even that is not guaranteed to work: there's no way to tell if the user is directly typing commands into a shell process. Sure, your parent is sh, but maybe that's because the user is running your program via a shell script, and their shell is actually fish. Or nu. Or posh. Or zsh. Or cmd.exe.

Then, you'd need to manually implement the quoting rules for all of those shells, because there is no common set of rules. If you're lucky, each shell has a concise and accurate description of its quoting rules available. You are not that lucky.

By the time you have finished this endeavour in a few months/years (and pulled most of your hair out), you now need to update the code every time someone comes along and thinks "I know, I'll make a new shell!"

And that's why this functionality doesn't exist. :slight_smile:

1 Like

FWIW, at work we've got a test suite which will run our CLI against a large range of inputs and we found the best way to let developers manually reproduce failing tests was to do println!("{cmd:?}"), rather than inspecting Command::get_args() and Command::get_program() and adding a bunch of our own formatting logic.

The Debug implementation isn't perfect (see @DanielKeep's answer for more), but at least the user will know the rough command that was executed and can manually tweak things to fit their shell if they need to. Most of us work on Linux/MacOS and use Bash or Zsh, so in practice you can copy/paste the command just fine.

1 Like