diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd5d122..7fe5db7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,8 +41,10 @@ jobs: uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust }} - - name: Install just - uses: taiki-e/install-action@just + - name: Setup homebrew + uses: Homebrew/actions/setup-homebrew@master + - name: Install hyperfine and just + run: brew install hyperfine just - name: Install rust-script run: cargo install --path . - name: Test examples diff --git a/Cargo.lock b/Cargo.lock index 795f032..1b8041f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,6 +444,7 @@ dependencies = [ "regex", "scan-rules", "sha1", + "shell-words", "tempfile", "toml", "winreg", @@ -491,9 +492,9 @@ checksum = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac" [[package]] name = "serde" -version = "1.0.180" +version = "1.0.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +checksum = "6d3e73c93c3240c0bda063c239298e633114c69a888c3e37ca8bb33f343e9890" [[package]] name = "serde_spanned" @@ -515,6 +516,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "strcursor" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 21602bc..32d4258 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ log = "0.4" pulldown-cmark = "0.9" regex = "1" sha1 = "0.10" +shell-words = "1" tempfile = "3" toml = "0.7" diff --git a/docs/README.md b/docs/README.md index b09cd72..a0c42d5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -107,6 +107,7 @@ Useful command-line arguments: - `--force`: Force the script to be rebuilt. Useful if you want to force a recompile with a different toolchain. - `--package`: Generate the Cargo package and print the path to it - but don't compile or run it. Effectively "unpacks" the script into a Cargo package. - `--test`: Compile and run tests. +- `--wrapper`: Add a wrapper around the executable. Can be used to run debugging with e.g. `rust-script --wrapper rust-lldb my-script.rs` or benchmarking with `rust-script --wrapper "hyperfine --runs 100" my-script.rs` ## Executable Scripts diff --git a/examples/fib.ers b/examples/fib.ers new file mode 100755 index 0000000..d0fe2bb --- /dev/null +++ b/examples/fib.ers @@ -0,0 +1,13 @@ +#!/usr/bin/env rust-script + +fn fib(n: i32) -> i32 { + match n { + 1 | 2 => 1, + _ => fib(n-1) + fib(n-2) + } +} + +fn main() { + assert_eq!(fib(30), 832040); +} + diff --git a/examples/test-examples.sh b/examples/test-examples.sh index 11aaee3..2d411e0 100755 --- a/examples/test-examples.sh +++ b/examples/test-examples.sh @@ -11,3 +11,14 @@ assert_equals() { assert_equals "result: 3" "$(just -f justfile/Justfile)" assert_equals "hello, rust" "$(./hello.ers)" assert_equals "hello, rust" "$(./hello-without-main.ers)" + +HYPERFINE_OUTPUT=$(rust-script --wrapper "hyperfine --runs 99" fib.ers) + +case "$HYPERFINE_OUTPUT" in + *"99 runs"*) + ;; + *) + echo "Hyperfine output: $HYPERFINE_OUTPUT" + exit 1 + ;; +esac diff --git a/src/arguments.rs b/src/arguments.rs index 6eb3030..828c83d 100644 --- a/src/arguments.rs +++ b/src/arguments.rs @@ -25,6 +25,7 @@ pub struct Args { pub install_file_association: bool, #[cfg(windows)] pub uninstall_file_association: bool, + pub wrapper: Option, } impl Args { @@ -75,7 +76,7 @@ impl Args { .help("Base path for resolving dependencies") .short('b') .long("base-path") - .num_args(1..=1) + .num_args(1) ) .arg(Arg::new("cargo-output") .help("Show output from cargo when building") @@ -163,6 +164,12 @@ impl Args { .num_args(1) // Benchmarking currently requires nightly: .conflicts_with("bench") + ) + .arg(Arg::new("wrapper") + .help("Wrapper injected before the command to run, e.g. 'rust-lldb' or 'hyperfine --runs 100'") + .long("wrapper") + .short('w') + .num_args(1) ); #[cfg(windows)] @@ -240,6 +247,7 @@ impl Args { install_file_association: m.get_flag("install-file-association"), #[cfg(windows)] uninstall_file_association: m.get_flag("uninstall-file-association"), + wrapper: m.get_one::("wrapper").map(Into::into), } } } diff --git a/src/main.rs b/src/main.rs index 4fa13ce..5bbc695 100644 --- a/src/main.rs +++ b/src/main.rs @@ -183,15 +183,14 @@ fn try_main() -> MainResult { return Ok(0); } + let mut cmd = action.command_to_execute(&args.script_args, args.wrapper)?; #[cfg(unix)] { - let mut cmd = action.cargo(&args.script_args)?; let err = cmd.exec(); Err(MainError::from(err)) } #[cfg(not(unix))] { - let mut cmd = action.cargo(&args.script_args)?; let exit_code = cmd.status().map(|st| st.code().unwrap_or(1))?; Ok(exit_code) } @@ -332,7 +331,11 @@ impl InputAction { self.pkg_path.join("Cargo.toml") } - fn cargo(&self, script_args: &[String]) -> MainResult { + fn command_to_execute( + &self, + script_args: &[String], + wrapper: Option, + ) -> MainResult { let release_mode = !self.debug && !matches!(self.build_kind, BuildKind::Bench); let built_binary_path = platform::binary_cache_path() @@ -351,9 +354,25 @@ impl InputAction { let manifest_path = self.manifest_path(); let execute_command = || { - let mut cmd = Command::new(&built_binary_path); - cmd.args(script_args.iter()); - cmd + if let Some(wrapper) = wrapper { + let wrapper_words = shell_words::split(&wrapper).unwrap(); + if wrapper_words.is_empty() { + return MainResult::Err(MainError::OtherBorrowed( + "The wrapper cannot be empty", + )); + } + let mut cmd = Command::new(&wrapper_words[0]); + if wrapper_words.len() > 1 { + cmd.args(wrapper_words[1..].iter()); + } + cmd.arg(&built_binary_path); + cmd.args(script_args.iter()); + Ok(cmd) + } else { + let mut cmd = Command::new(&built_binary_path); + cmd.args(script_args.iter()); + Ok(cmd) + } }; if matches!(self.build_kind, BuildKind::Normal) && !self.force_compile { @@ -376,7 +395,7 @@ impl InputAction { && built_binary_time.cmp(&manifest_mtime).is_ge() { debug!("Keeping old binary"); - return Ok(execute_command()); + return execute_command(); } else { debug!("Old binary too old - rebuilding"); } @@ -423,7 +442,7 @@ impl InputAction { if matches!(self.build_kind, BuildKind::Normal) { if cmd.status()?.code() == Some(0) { - cmd = execute_command(); + cmd = execute_command()?; } else { return Err(MainError::OtherOwned("Could not execute cargo".to_string())); }