oconnor663 / duct.rs

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

Is there any way to execute chain of commands? #106

Closed p32929 closed 1 year ago

p32929 commented 1 year ago

Hi, thanks for making this amazing library/package/crate ( I'm really new to rust ). Is there any way to implement multiple chain of commands? for instance:

  1. cd into a directory
  2. run some commands into the cded directory
  3. cd into another directory
  4. run some into the latest cded directory

When I try to do something like this:

fn string_to_static_str(s: String) -> &'static str {
    Box::leak(s.into_boxed_str())
}

// main
let commands = vec!["cd C:\\Users\\MegaMind\\Documents\\dynamodb_local_latest","ec"];
for command in commands {
    let s1 = string_to_static_str(command.to_string());
    let s2 = sh(s1).read().unwrap();
    println!("{}", s2);
}

It seems like, the ls command gets executed into the source directory instead of the C:\\Users\\MegaMind\\Documents\\dynamodb_local_latest directory. Is there any way to add multiple chain of commands like that? Thanks

Edit: What I'm trying to achieve is this: https://stackoverflow.com/questions/73822157/cd-has-no-effect-when-executing-series-of-commands Thanks

oconnor663 commented 1 year ago

It seems like, the ls command gets executed into the source directory instead

Indeed, this is a common source of confusion. It isn't really specific to Duct, but rather it's to do with how child process spawning works in general.

When you're in a regular terminal, you have one shell process. It looks like you're on Windows, so that's one cmd.exe process. (On Linux it might be one bash process.) Then when you run cd, your shell doesn't actually spawn any new processes. Instead, cd is a special command that makes your shell process change its own current working directory. After that, all the child processes you spawn through that shell will inherit its new working directory. This the familiar way of doing things.

When you're spawning shell commands as child processes from code -- Rust in this case, but most other languages will work similarly -- you end up creating multiple shell processes. Each time you run a command, that's a new shell. If we say that cmd.exe and bash "know" that cd is a special command that should affect the current process rather than spawning a child, then we can say that Rust and Duct don't "know" that. They'll spawn a new shell process to run the cd command, and then more new shell processes for all the other commands you want to run, and the cd won't end up having the desired effect.

So we basically have two options for fixing this:

  1. We could make it so that Rust only spawns a single shell, and then that shell does a cd followed by multiple commands.
  2. Or, rather than using the shell's cd command, we could specify the working directory of the children in a way that Rust understands.

Let's see an example of each approach. Here's the first approach, with a single shell process:

fn main() {
    let commands = ["cd /tmp/foo", "cat bar.txt", "cat baz.txt"];
    let script = commands.join("\n");
    let output = duct_sh::sh_dangerous(script).read().unwrap();
    println!("{}", output);
}

Here on my Linux machine I'm joining all of my commands with a newline character (on Windows you probably want "\r\n"), and then I'm feeding them to a single call to sh_dangerous. The "dangerous" part reflects the fact that because we're putting this string together at runtime, it would be super easy to let malicious user input slip in there and potentially run arbitrary shell code. (Your workaround with Box::leak let you use sh instead of sh_dangerous with dynamic shell strings, but I don't recommend that. The only difference is the name, and the name is telling the truth!)

Here's the second approach, specifying the working directory for the children in a way that Rust and Duct understand:

fn main() {
    let commands = ["cat bar.txt", "cat baz.txt"];
    for command in commands {
        let output = duct_sh::sh(command).dir("/tmp/foo").read().unwrap();
        println!("{}", output);
    }
}

The key here is Duct's dir method. If we were using std::process::Command instead, we could use its current_dir method to accomplish the same thing. Here because my command strings are constants, I can use sh instead of sh_dangerous.

Here's another version of that second approach, a which is actually my favorite of all. We're going to keep using the dir method, and then instead of duct_sh::sh we're going to use duct::cmd!, which is a macro.

use duct::cmd;

fn main() {
    let commands = [cmd!("cat", "bar.txt"), cmd!("cat", "baz.txt")];
    for command in commands {
        let output = command.dir("/tmp/foo").read().unwrap();
        println!("{}", output);
    }
}

This approach actually doesn't go through a shell at all. Instead, it's going to talk directly to the OS to spawn child processes, and the underlying OS API will take the arguments as a list of separate strings. That means that we don't have to worry about "dangerous" shell injection issues or annoying whitespace bugs, which I think is a lot nicer. If you really need shell special characters like $! or <<<EOF or whatever, then this approach won't work for you. But most of the time when you're spawning simple commands, this is a good option.

p32929 commented 1 year ago

Wow! Thank you so much for the amazing explanations! It is really helpful. Thanks a lot!

oconnor663 commented 1 year ago

No problem! By the way, how did you discover Duct? I'm curious about where/how new Rust programmers choose libraries.

p32929 commented 1 year ago

So, I'm really new to rust, & I thought why not make a small project. So, I wanted to make a CLI app where I can save chain of commands to run later ( BTW, I have already made the CLI app. source here: https://github.com/p32929/fay_cli/ though it still not how I want it to be, but for now, it gets the job done ). So, because I'm really new to rust, I've been having issues compiling my code, then I came across this discord channel ( https://discord.gg/rust-lang-community ) which was really helpful. In the channel, Remy ( Remy_Clarke#1198 ) and Cod1r ( cod1r#3465 ) helped me a lot while making the CLI app. But mostly I was googling stuffs and I came across some reddit posts and I also asked question in stackoverflow ( https://stackoverflow.com/questions/73822157/cd-has-no-effect-when-executing-series-of-commands ). but since I'm new to rust, I couldn't understand the answer very well. So, I kept looking into https://crates.io/ ( for a easier/better solution ) by rust shell and std::process::Command and cmd bash shell. That's how I came across this crate.