Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
run: cargo test
12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "shell_exec"
version = "0.1.0"
version = "0.2.0"
authors = ["Caido Labs Inc. <dev@caido.io>"]
description = "Cross platform library to execute shell scripts"
repository = "https://github.com/caido/shell_exec"
Expand All @@ -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"
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
Expand Down
32 changes: 32 additions & 0 deletions src/argument.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
}
58 changes: 51 additions & 7 deletions src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -33,13 +33,17 @@ impl Execution {
V: AsRef<OsStr>,
{
// 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())
Expand Down Expand Up @@ -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())
Expand All @@ -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() {
Expand Down Expand Up @@ -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);
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
20 changes: 10 additions & 10 deletions src/script.rs
Original file line number Diff line number Diff line change
@@ -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),
}

Expand All @@ -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<OsStr> 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()),
}
}
}
Expand All @@ -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"),
}
}

Expand Down
13 changes: 9 additions & 4 deletions src/shell.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use strum::{Display, EnumString};

use crate::Argument;

#[derive(Debug, EnumString, Display, Copy, Clone)]
pub enum Shell {
#[strum(serialize = "zsh")]
Expand All @@ -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")],
}
}
}
Expand Down
Loading