diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f3e171d..b30411d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,8 +38,8 @@ jobs: test: needs: lint runs-on: ${{ matrix.os }} - timeout-minutes: 5 - + timeout-minutes: 10 + strategy: matrix: os: [ubuntu-latest, windows-latest] @@ -53,5 +53,11 @@ jobs: with: cache: false + - name: Setup WSL + if: ${{ matrix.os == 'windows-latest' }} + uses: caido/action-setup-wsl@v4 + with: + distribution: Ubuntu-22.04 + - name: Run tests - run: cargo test \ No newline at end of file + run: cargo test diff --git a/Cargo.toml b/Cargo.toml index 6c2f3e9..045cbfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shell_exec" -version = "0.1.0" +version = "0.2.0" authors = ["Caido Labs Inc. "] description = "Cross platform library to execute shell scripts" repository = "https://github.com/caido/shell_exec" @@ -18,10 +18,10 @@ strum = { version = "0.26", features = ["derive"] } tempfile = "3.12" thiserror = "1" tokio = { version = "1", features = [ - "time", - "process", - "io-util", - "macros", - "rt", + "time", + "process", + "io-util", + "macros", + "rt", ] } typed-builder = "0.20" diff --git a/README.md b/README.md index f7c5577..06f424b 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ use std::time::Duration; use shell_exec::{Execution, Shell}; let execution = Execution::builder() - .shell(Shell::Cmd) + .shell(Shell::Bash) .cmd( r#" - set /p input="" - echo hello %input% + INPUT=`cat -`; + echo "hello $INPUT" "# .to_string(), ) diff --git a/src/argument.rs b/src/argument.rs new file mode 100644 index 0000000..007f5e3 --- /dev/null +++ b/src/argument.rs @@ -0,0 +1,32 @@ +use std::ffi::OsStr; + +use tokio::process::Command; + +pub enum Argument<'a> { + Normal(&'a str), + Path(&'a OsStr), + Raw(&'a str), +} + +pub trait CommandArgument { + fn argument(&mut self, arg: &Argument<'_>) -> &mut Self; +} + +impl CommandArgument for Command { + fn argument(&mut self, arg: &Argument<'_>) -> &mut Self { + match arg { + Argument::Normal(value) => self.arg(value), + Argument::Path(value) => self.arg(value), + Argument::Raw(value) => { + #[cfg(windows)] + { + self.raw_arg(value) + } + #[cfg(unix)] + { + self.arg(value) + } + } + } + } +} diff --git a/src/execution.rs b/src/execution.rs index e667bdd..2409564 100644 --- a/src/execution.rs +++ b/src/execution.rs @@ -9,7 +9,7 @@ use tokio::process::Command; use tokio::time::timeout; use typed_builder::TypedBuilder; -use crate::{Result, Script, Shell, ShellError}; +use crate::{CommandArgument, Result, Script, Shell, ShellError}; #[derive(TypedBuilder)] pub struct Execution { @@ -33,13 +33,17 @@ impl Execution { V: AsRef, { // Prepare script - let full_cmd = Script::build(self.shell, self.cmd, self.init).await?; + let script = Script::build(self.shell, self.cmd, self.init).await?; // Spawn - // NOTE: If kill_on_drop is proven not sufficiently reliable, we might want to explicitly kill the process before exiting the function. This approach is slower since it awaits the process termination. - let mut cmd_handle = Command::new(self.shell.to_string()) - .arg(self.shell.command_arg()) - .arg(&full_cmd) + // NOTE: If kill_on_drop is proven not sufficiently reliable, we might want to explicitly kill the process + // before exiting the function. This approach is slower since it awaits the process termination. + let mut builder = Command::new(self.shell.to_string()); + for arg in self.shell.command_args() { + builder.argument(arg); + } + let mut cmd_handle = builder + .argument(&script.argument()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -95,7 +99,7 @@ mod tests { #[tokio::test] #[cfg(unix)] - async fn should_execute() { + async fn should_execute_sh() { let execution = Execution::builder() .shell(Shell::Sh) .cmd(r#"jq -r .hello"#.to_string()) @@ -107,6 +111,26 @@ mod tests { assert_eq!(b"world"[..], data); } + #[tokio::test] + #[cfg(unix)] + async fn should_execute_bash() { + let execution = Execution::builder() + .shell(Shell::Bash) + .cmd( + r#" + INPUT=`cat -`; + echo "hello $INPUT" + "# + .to_string(), + ) + .timeout(Duration::from_millis(10000)) + .build(); + + let data = execution.execute(b"world").await.unwrap(); + + assert_eq!(b"hello world"[..], data); + } + #[tokio::test] #[cfg(unix)] async fn should_execute_with_envs() { @@ -206,4 +230,24 @@ mod tests { assert_eq!(b"hello\n & WORLD"[..], data); } + + #[tokio::test] + #[cfg(windows)] + async fn should_execute_wsl() { + let execution = Execution::builder() + .shell(Shell::Wsl) + .cmd( + r#" + INPUT=$(cat); + echo "hello $INPUT" + "# + .to_string(), + ) + .timeout(Duration::from_millis(10000)) + .build(); + + let data = execution.execute(b"world").await.unwrap(); + + assert_eq!(b"hello world"[..], data); + } } diff --git a/src/lib.rs b/src/lib.rs index c6255be..9d23b45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,11 @@ +use self::argument::{Argument, CommandArgument}; use self::errors::Result; pub use self::errors::ShellError; pub use self::execution::Execution; use self::script::Script; pub use self::shell::Shell; +mod argument; mod errors; mod execution; mod script; diff --git a/src/script.rs b/src/script.rs index f1d73f9..55b742b 100644 --- a/src/script.rs +++ b/src/script.rs @@ -1,12 +1,11 @@ -use std::ffi::OsStr; use std::io::Write; use tempfile::TempPath; -use crate::{Result, Shell, ShellError}; +use crate::{Argument, Result, Shell, ShellError}; pub enum Script { - Inline(String), + Inline { raw: String, shell: Shell }, File(TempPath), } @@ -29,17 +28,18 @@ impl Script { let file = write_file(raw).await?; Self::File(file) } - _ => Self::Inline(raw), + _ => Self::Inline { raw, shell }, }; Ok(cmd) } -} -impl AsRef for &Script { - fn as_ref(&self) -> &OsStr { + pub fn argument(&self) -> Argument<'_> { match self { - Script::Inline(v) => v.as_ref(), - Script::File(v) => v.as_os_str(), + Script::Inline { raw, shell } => match shell { + Shell::Wsl => Argument::Raw(raw), + _ => Argument::Normal(raw), + }, + Script::File(path) => Argument::Path(path.as_os_str()), } } } @@ -48,7 +48,7 @@ fn init_line(script: &str, shell: Shell) -> String { match shell { Shell::Cmd => format!("{script} 2> nul"), Shell::Powershell => format!("{script} 2>$null"), - Shell::Bash | Shell::Zsh | Shell::Sh => format!("{script} > /dev/null 2>&1"), + Shell::Bash | Shell::Zsh | Shell::Sh | Shell::Wsl => format!("{script} > /dev/null 2>&1"), } } diff --git a/src/shell.rs b/src/shell.rs index ea33195..8d3c66a 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -1,5 +1,7 @@ use strum::{Display, EnumString}; +use crate::Argument; + #[derive(Debug, EnumString, Display, Copy, Clone)] pub enum Shell { #[strum(serialize = "zsh")] @@ -12,14 +14,17 @@ pub enum Shell { Cmd, #[strum(serialize = "powershell")] Powershell, + #[strum(serialize = "wsl")] + Wsl, } impl Shell { - pub fn command_arg<'a>(&self) -> &'a str { + pub fn command_args(&self) -> &[Argument<'static>] { match self { - Self::Cmd => "/C", - Self::Powershell => "-Command", - _ => "-c", + Self::Cmd => &[Argument::Normal("/C")], + Self::Powershell => &[Argument::Normal("-Command")], + Self::Wsl => &[Argument::Normal("bash"), Argument::Normal("-c")], + _ => &[Argument::Normal("-c")], } } }