├── CNAME ├── docs ├── CNAME ├── _config.yml ├── favicon.ico └── README.md ├── _config.yml ├── tests ├── data │ ├── slow-build │ │ ├── slow-build-lib.rs │ │ ├── .gitignore │ │ ├── slow-build-build.rs │ │ └── Cargo.toml │ ├── file-to-be-included.txt │ ├── script-module.rs │ ├── script-test.rs │ ├── question-mark.rs │ ├── script-has.weird§chars!.rs │ ├── script-no-deps.rs │ ├── script-main-with-spaces.rs │ ├── script-test-extra-args.rs │ ├── whitespace-before-main.rs │ ├── time.rs │ ├── script-using-anyhow-and-serde.rs │ ├── same-flags.rs │ ├── script-using-env.rs │ ├── script-unstable-feature.rs │ ├── script-explicit.rs │ ├── script-args.rs │ ├── script-slow-output.rs │ ├── templates │ │ ├── boolinate.rs │ │ ├── override │ │ │ └── expr.rs │ │ └── shout.rs │ ├── pub-fn-main.rs │ ├── outer-line-doc.rs │ ├── script-invalid-doc-comment.rs │ ├── script-full-line.rs │ ├── script-full-block.rs │ ├── script-full-line-without-main.rs │ ├── script-including-relative.rs │ ├── script-short-without-main.rs │ ├── script-async-main.rs │ ├── script-short.rs │ ├── cargo-target-dir-env.rs │ ├── script-cs-env.rs │ └── extern-c-main.rs ├── scripts │ ├── base-path-tmp.expected │ ├── arg0.expected │ ├── avoid-toolchain-files.expected │ ├── basic-eval.expected │ ├── gen-only.expected │ ├── force-rebuild.expected │ ├── loop-to-prefix.expected │ ├── basic-eval.script │ ├── loop-to-prefix.script │ ├── base-path-tmp.script │ ├── gen-only.script │ ├── avoid-toolchain-files.script │ ├── arg0.script │ ├── force-rebuild.script │ └── test-runner.sh ├── tests │ ├── others.rs │ ├── expr.rs │ └── script.rs ├── integration.rs └── util │ └── mod.rs ├── .gitignore ├── examples ├── hello-without-main.ers ├── hello.ers ├── justfile │ ├── dummy │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ └── Justfile ├── fib.ers ├── time-main.ers ├── time-without-main.ers └── test-examples.sh ├── clippy.toml ├── src ├── build_kind.rs ├── defer.rs ├── templates.rs ├── error.rs ├── platform.rs ├── file_assoc.rs ├── consts.rs ├── arguments.rs ├── main.rs └── manifest.rs ├── CHANGELOG.md ├── LICENSE-MIT ├── Cargo.toml ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── README.md ├── LICENSE-APACHE └── Cargo.lock /CNAME: -------------------------------------------------------------------------------- 1 | rust-script.org -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | rust-script.org -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-hacker -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-hacker -------------------------------------------------------------------------------- /tests/data/slow-build/slow-build-lib.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/slow-build/.gitignore: -------------------------------------------------------------------------------- 1 | /Cargo.lock 2 | -------------------------------------------------------------------------------- /tests/scripts/base-path-tmp.expected: -------------------------------------------------------------------------------- 1 | /tmp 2 | -------------------------------------------------------------------------------- /tests/data/file-to-be-included.txt: -------------------------------------------------------------------------------- 1 | hello, including script -------------------------------------------------------------------------------- /tests/data/script-module.rs: -------------------------------------------------------------------------------- 1 | pub const A_VALUE: i32 = 1; -------------------------------------------------------------------------------- /tests/scripts/arg0.expected: -------------------------------------------------------------------------------- 1 | ["./subdir/myscript.rs"] 2 | -------------------------------------------------------------------------------- /tests/scripts/avoid-toolchain-files.expected: -------------------------------------------------------------------------------- 1 | hello, world 2 | -------------------------------------------------------------------------------- /tests/scripts/basic-eval.expected: -------------------------------------------------------------------------------- 1 | hello 2 | 2 3 | 3 4 | 4 5 | 3 6 | -------------------------------------------------------------------------------- /tests/scripts/gen-only.expected: -------------------------------------------------------------------------------- 1 | About to cargo run 2 | hello, world 3 | -------------------------------------------------------------------------------- /tests/data/script-test.rs: -------------------------------------------------------------------------------- 1 | fn main() {} 2 | 3 | #[test] 4 | fn test() {} 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | /local 3 | /.cargo 4 | /.idea 5 | /tests/scripts/*.actual* 6 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fornwall/rust-script/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /tests/scripts/force-rebuild.expected: -------------------------------------------------------------------------------- 1 | msg = undefined 2 | msg = undefined 3 | msg = hello 4 | -------------------------------------------------------------------------------- /examples/hello-without-main.ers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rust-script 2 | 3 | println!("hello, rust"); 4 | -------------------------------------------------------------------------------- /examples/hello.ers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rust-script 2 | 3 | fn main() { 4 | println!("hello, rust"); 5 | } 6 | -------------------------------------------------------------------------------- /tests/data/question-mark.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | File::open("__rust-script-this-file-does-not-exist.txt")?; 4 | -------------------------------------------------------------------------------- /tests/data/script-has.weird§chars!.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("--output--"); 3 | println!("Ok"); 4 | } 5 | -------------------------------------------------------------------------------- /tests/data/script-no-deps.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("--output--"); 3 | println!("Hello, World!"); 4 | } 5 | -------------------------------------------------------------------------------- /tests/data/script-main-with-spaces.rs: -------------------------------------------------------------------------------- 1 | fn main () { 2 | println!("--output--"); 3 | println!("Hello, World!"); 4 | } 5 | -------------------------------------------------------------------------------- /tests/data/script-test-extra-args.rs: -------------------------------------------------------------------------------- 1 | fn main() {} 2 | 3 | #[test] 4 | fn test() { 5 | println!("Hello, world!"); 6 | } 7 | -------------------------------------------------------------------------------- /tests/scripts/loop-to-prefix.expected: -------------------------------------------------------------------------------- 1 | First: 2 | 1: line1 3 | 2: line2 4 | Second: 5 | 1: line1 6 | 2: line2 7 | -------------------------------------------------------------------------------- /tests/data/whitespace-before-main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("--output--"); 3 | println!("hello, world"); 4 | } 5 | -------------------------------------------------------------------------------- /tests/data/time.rs: -------------------------------------------------------------------------------- 1 | // cargo-deps: chrono 2 | extern crate chrono; 3 | fn main() { 4 | println!("--output--"); 5 | println!("Hello"); 6 | } -------------------------------------------------------------------------------- /tests/data/script-using-anyhow-and-serde.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use serde_json::from_str; 3 | 4 | println!("--output--"); 5 | println!("Ok"); 6 | -------------------------------------------------------------------------------- /tests/data/same-flags.rs: -------------------------------------------------------------------------------- 1 | println!("--output--"); 2 | if let Some(arg) = std::env::args().skip(1).next() { 3 | println!("Argument: {}", arg); 4 | } 5 | -------------------------------------------------------------------------------- /tests/data/script-using-env.rs: -------------------------------------------------------------------------------- 1 | let msg = option_env!("_RUST_SCRIPT_TEST_MESSAGE").unwrap_or("undefined"); 2 | 3 | println!("--output--"); 4 | println!("msg = {}", msg); -------------------------------------------------------------------------------- /tests/data/script-unstable-feature.rs: -------------------------------------------------------------------------------- 1 | #![feature(lang_items)] 2 | 3 | fn main() { 4 | println!("--output--"); 5 | println!("`#![feature]` *may* be used!"); 6 | } 7 | -------------------------------------------------------------------------------- /tests/data/script-explicit.rs: -------------------------------------------------------------------------------- 1 | extern crate boolinator; 2 | use boolinator::Boolinator; 3 | fn main() { 4 | println!("--output--"); 5 | println!("{:?}", true.as_some(1)); 6 | } 7 | -------------------------------------------------------------------------------- /tests/data/script-args.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("--output--"); 3 | for (i, arg) in std::env::args().enumerate() { 4 | println!("{:>4}: {:?}", format!("[{}]", i), arg); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/data/slow-build/slow-build-build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Sleeping for 2 seconds..."); 3 | std::thread::sleep(std::time::Duration::from_millis(2000)); 4 | println!("Done."); 5 | } 6 | -------------------------------------------------------------------------------- /tests/data/script-slow-output.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | ```cargo 3 | [dependencies] 4 | slow-build = { version = "0.1.0", path = "slow-build" } 5 | ``` 6 | */ 7 | fn main() { 8 | println!("--output--"); 9 | println!("Ok"); 10 | } 11 | -------------------------------------------------------------------------------- /examples/justfile/dummy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dummy" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /examples/fib.ers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rust-script 2 | 3 | fn fib(n: i32) -> i32 { 4 | match n { 5 | 1 | 2 => 1, 6 | _ => fib(n-1) + fib(n-2) 7 | } 8 | } 9 | 10 | fn main() { 11 | assert_eq!(fib(30), 832040); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /tests/data/templates/boolinate.rs: -------------------------------------------------------------------------------- 1 | // cargo-deps: boolinator="0.1.0" 2 | #{prelude} 3 | 4 | extern crate boolinator; 5 | use boolinator::Boolinator; 6 | 7 | fn main() { 8 | println!("{:?}", Boolinator::as_option({#{script}})); 9 | } 10 | -------------------------------------------------------------------------------- /tests/data/pub-fn-main.rs: -------------------------------------------------------------------------------- 1 | //! ```cargo 2 | //! [dependencies] 3 | //! boolinator = "=0.1.0" 4 | //! ``` 5 | use boolinator::Boolinator; 6 | 7 | pub fn main() { 8 | println!("--output--"); 9 | println!("{:?}", true.as_some(1)); 10 | } 11 | -------------------------------------------------------------------------------- /tests/data/templates/override/expr.rs: -------------------------------------------------------------------------------- 1 | // cargo-deps: boolinator="0.1.0" 2 | #{prelude} 3 | 4 | extern crate boolinator; 5 | use boolinator::Boolinator; 6 | 7 | fn main() { 8 | println!("{:?}", Boolinator::as_option({#{script}})); 9 | } 10 | -------------------------------------------------------------------------------- /tests/data/outer-line-doc.rs: -------------------------------------------------------------------------------- 1 | /// ```cargo 2 | /// [dependencies] 3 | /// boolinator = "=0.1.0" 4 | /// ``` 5 | use boolinator::Boolinator; 6 | 7 | pub fn main() { 8 | println!("--output--"); 9 | println!("{:?}", true.as_some(1)); 10 | } 11 | -------------------------------------------------------------------------------- /tests/data/slow-build/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "slow-build" 3 | version = "0.1.0" 4 | authors = ["Daniel Keep "] 5 | 6 | build = "slow-build-build.rs" 7 | 8 | [lib] 9 | name = "slow_build" 10 | path = "slow-build-lib.rs" 11 | -------------------------------------------------------------------------------- /tests/data/script-invalid-doc-comment.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("--output--"); 3 | println!("Hello, World!"); 4 | } 5 | 6 | /** 7 | ```cargo 8 | [dependencies] 9 | i-cant-decide-whether-you-should = ["live", "die"] 10 | ``` 11 | */ 12 | fn dummy() {} 13 | -------------------------------------------------------------------------------- /tests/data/templates/shout.rs: -------------------------------------------------------------------------------- 1 | #{prelude} 2 | 3 | fn main() { 4 | match {#{script}} { 5 | script_result => { 6 | let text = script_result.to_string(); 7 | let text = text.to_uppercase(); 8 | println!("{}", text); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/scripts/basic-eval.script: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e -u 3 | 4 | rust-script -e 'println!("hello");' 5 | rust-script -e '1+1' 6 | rust-script -e '1+2' 7 | rust-script -e '1+3' 8 | rust-script -d 'unicode-segmentation' -e 'unicode_segmentation::UnicodeSegmentation::graphemes("a̐éö̲", true).count()' 9 | -------------------------------------------------------------------------------- /examples/justfile/dummy/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub fn add(left: usize, right: usize) -> usize { 2 | left + right 3 | } 4 | 5 | #[cfg(test)] 6 | mod tests { 7 | use super::*; 8 | 9 | #[test] 10 | fn it_works() { 11 | let result = add(2, 2); 12 | assert_eq!(result, 4); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/justfile/Justfile: -------------------------------------------------------------------------------- 1 | demo: 2 | #!/usr/bin/env -S rust-script --base-path {{justfile_directory()}} 3 | //! ```cargo 4 | //! [dependencies] 5 | //! dummy = { path = "./dummy" } 6 | //! ``` 7 | fn main() { 8 | let result = dummy::add(1, 2); 9 | println!("result: {}", result); 10 | } 11 | 12 | -------------------------------------------------------------------------------- /tests/data/script-full-line.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This is merged into a default manifest in order to form the full package manifest: 3 | 4 | ```cargo 5 | [dependencies] 6 | boolinator = "=0.1.0" 7 | ``` 8 | */ 9 | use boolinator::Boolinator; 10 | fn main() { 11 | println!("--output--"); 12 | println!("{:?}", true.as_some(1)); 13 | } 14 | -------------------------------------------------------------------------------- /tests/data/script-full-block.rs: -------------------------------------------------------------------------------- 1 | //! This is merged into a default manifest in order to form the full package manifest: 2 | //! 3 | //! ```cargo 4 | //! [dependencies] 5 | //! boolinator = "=0.1.0" 6 | //! ``` 7 | use boolinator::Boolinator; 8 | fn main() { 9 | println!("--output--"); 10 | println!("{:?}", true.as_some(1)); 11 | } 12 | -------------------------------------------------------------------------------- /tests/scripts/loop-to-prefix.script: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e -u 3 | 4 | echo "First:" 5 | echo "line1\nline2" | rust-script --loop \ 6 | "let mut n=0; move |l| {n+=1; println!(\"{:>6}: {}\",n,l.trim_end())}" 7 | 8 | echo "Second:" 9 | echo "line1\nline2" | rust-script --count --loop \ 10 | "|l,n| println!(\"{:>6}: {}\", n, l.trim_end())" 11 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | allowed-duplicate-crates = [ 2 | "getrandom", 3 | "wasi", 4 | "windows_aarch64_gnullvm", 5 | "windows_aarch64_msvc", 6 | "windows_i686_gnu", 7 | "windows_i686_gnullvm", 8 | "windows_i686_msvc", 9 | "windows-sys", 10 | "windows-targets", 11 | "windows_x86_64_gnu", 12 | "windows_x86_64_gnullvm", 13 | "windows_x86_64_msvc", 14 | ] 15 | -------------------------------------------------------------------------------- /tests/data/script-full-line-without-main.rs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rust-script 2 | /// This is merged into a default manifest in order to form the full package manifest: 3 | /// 4 | /// ```cargo 5 | /// [dependencies] 6 | /// boolinator = "=0.1.0" 7 | /// ``` 8 | use boolinator::Boolinator; 9 | 10 | println!("--output--"); 11 | println!("{:?}", true.as_some(1)); 12 | -------------------------------------------------------------------------------- /examples/time-main.ers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rust-script 2 | //! This is a regular crate doc comment, but it also contains a partial 3 | //! Cargo manifest. Note the use of a *fenced* code block, and the 4 | //! `cargo` "language". 5 | //! 6 | //! ```cargo 7 | //! [dependencies] 8 | //! time = "0.1.25" 9 | //! ``` 10 | fn main() { 11 | println!("{}", time::now().rfc822z()); 12 | } 13 | -------------------------------------------------------------------------------- /examples/time-without-main.ers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rust-script 2 | /// This is a regular crate doc comment, but it also contains a partial 3 | /// Cargo manifest. Note the use of a *fenced* code block, and the 4 | /// `cargo` "language". 5 | /// 6 | /// ```cargo 7 | /// [dependencies] 8 | /// time = "0.1.25" 9 | /// ``` 10 | 11 | use time::now; 12 | 13 | println!("{}", now().rfc822z()); 14 | -------------------------------------------------------------------------------- /tests/scripts/base-path-tmp.script: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | WITHOUT_BASE_PATH=$(rust-script -e 'println!("{}", std::env::var("RUST_SCRIPT_BASE_PATH").unwrap());') 4 | PWD=$(pwd) 5 | if [ "$WITHOUT_BASE_PATH" != "$PWD" ]; then 6 | echo "Error: Expected $PWD, was $WITHOUT_BASE_PATH" 7 | fi 8 | 9 | rust-script --base-path /tmp -e 'println!("{}", std::env::var("RUST_SCRIPT_BASE_PATH").unwrap());' 10 | -------------------------------------------------------------------------------- /tests/scripts/gen-only.script: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e -u 3 | 4 | # https://unix.stackexchange.com/questions/30091/fix-or-alternative-for-mktemp-in-os-x 5 | mytmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir') 6 | 7 | cd "$mytmpdir" 8 | 9 | printf 'println!("hello, world");' > script.rs 10 | 11 | cd $(rust-script --package script.rs) 12 | 13 | echo "About to cargo run" 14 | cargo run -q 15 | -------------------------------------------------------------------------------- /tests/data/script-including-relative.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | mod script_module { 4 | include!(concat!(env!("RUST_SCRIPT_BASE_PATH"), "/script-module.rs")); 5 | } 6 | 7 | fn main() { 8 | println!("--output--"); 9 | let s = include_str!(concat!(env!("RUST_SCRIPT_BASE_PATH"), "/file-to-be-included.txt")); 10 | assert_eq!(script_module::A_VALUE, 1); 11 | println!("{}", s); 12 | } 13 | -------------------------------------------------------------------------------- /tests/data/script-short-without-main.rs: -------------------------------------------------------------------------------- 1 | // cargo-deps: boolinator="=0.1.0" 2 | // You can also leave off the version number, in which case, it's assumed 3 | // to be "*". Also, the `cargo-deps` comment *must* be a single-line 4 | // comment, and it *must* be the first thing in the file, after the 5 | // shebang. 6 | use boolinator::Boolinator; 7 | 8 | println!("--output--"); 9 | println!("{:?}", true.as_some(1)); 10 | -------------------------------------------------------------------------------- /tests/scripts/avoid-toolchain-files.script: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e -u 3 | 4 | # https://unix.stackexchange.com/questions/30091/fix-or-alternative-for-mktemp-in-os-x 5 | mytmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir') 6 | 7 | cd "$mytmpdir" 8 | 9 | printf '[toolchain]\nchannel = "non-existing"' > rust-toolchain.toml 10 | 11 | printf 'println!("hello, world");' > script.rs 12 | 13 | rust-script script.rs 14 | -------------------------------------------------------------------------------- /tests/data/script-async-main.rs: -------------------------------------------------------------------------------- 1 | //! This is merged into a default manifest in order to form the full package manifest: 2 | //! 3 | //! ```cargo 4 | //! [dependencies] 5 | //! boolinator = "=0.1.0" 6 | //! tokio = { version = "1", features = ["full"] } 7 | //! ``` 8 | use boolinator::Boolinator; 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | println!("--output--"); 13 | println!("{:?}", true.as_some(1)); 14 | } 15 | -------------------------------------------------------------------------------- /tests/data/script-short.rs: -------------------------------------------------------------------------------- 1 | // cargo-deps: boolinator="=0.1.0" 2 | // You can also leave off the version number, in which case, it's assumed 3 | // to be "*". Also, the `cargo-deps` comment *must* be a single-line 4 | // comment, and it *must* be the first thing in the file, after the 5 | // shebang. 6 | use boolinator::Boolinator; 7 | fn main() { 8 | println!("--output--"); 9 | println!("{:?}", true.as_some(1)); 10 | } 11 | -------------------------------------------------------------------------------- /tests/tests/others.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_version() { 3 | let out = rust_script!("--version").unwrap(); 4 | assert!(out.success()); 5 | scan!(&out.stdout; 6 | ("rust-script", &::std::env::var("CARGO_PKG_VERSION").unwrap(), .._) => () 7 | ) 8 | .unwrap(); 9 | } 10 | 11 | #[test] 12 | fn test_clear_cache() { 13 | let out = rust_script!("--clear-cache").unwrap(); 14 | assert!(out.success()); 15 | } 16 | -------------------------------------------------------------------------------- /tests/scripts/arg0.script: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e -u 3 | 4 | # See https://github.com/fornwall/rust-script/issues/113 5 | 6 | mytmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir') 7 | mkdir $mytmpdir/subdir 8 | cd "$mytmpdir" 9 | 10 | cat > subdir/myscript.rs << EOF 11 | use std::env; 12 | 13 | fn main() { 14 | let args: Vec = env::args().collect(); 15 | println!("{:?}", args); 16 | } 17 | EOF 18 | 19 | rust-script ./subdir/myscript.rs 20 | -------------------------------------------------------------------------------- /tests/data/cargo-target-dir-env.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | pub fn main() { 4 | // Test that CARGO_TARGET_DIR is not set by rust-script to avoid 5 | // interfering with cargo calls done by the script. 6 | // See https://github.com/fornwall/rust-script/issues/27 7 | let env_variable = env::var("CARGO_TARGET_DIR"); 8 | println!("--output--"); 9 | println!( 10 | "{:?}", 11 | matches!(env_variable, Err(env::VarError::NotPresent)) 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /tests/scripts/force-rebuild.script: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e -u 3 | 4 | # https://unix.stackexchange.com/questions/30091/fix-or-alternative-for-mktemp-in-os-x 5 | mytmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir') 6 | 7 | cd "$mytmpdir" 8 | 9 | printf 'let msg = option_env!("_RUST_SCRIPT_TEST_MESSAGE").unwrap_or("undefined"); println!("msg = {}", msg);' > script.rs 10 | 11 | rust-script script.rs 12 | 13 | export _RUST_SCRIPT_TEST_MESSAGE=hello 14 | 15 | rust-script script.rs 16 | 17 | rust-script --force script.rs 18 | -------------------------------------------------------------------------------- /tests/data/script-cs-env.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | println!("--output--"); 5 | let path = env::var("RUST_SCRIPT_PATH").expect("CSSP wasn't set"); 6 | assert!(path.ends_with("script-cs-env.rs")); 7 | assert_eq!(env::var("RUST_SCRIPT_SAFE_NAME"), Ok("script-cs-env".into())); 8 | assert_eq!(env::var("RUST_SCRIPT_PKG_NAME"), Ok("script-cs-env".into())); 9 | let base_path = env::var("RUST_SCRIPT_BASE_PATH").expect("CSBP wasn't set"); 10 | assert!(base_path.ends_with("data")); 11 | println!("Ok"); 12 | } 13 | -------------------------------------------------------------------------------- /examples/test-examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e -u 3 | 4 | assert_equals() { 5 | if [ "$1" != "$2" ]; then 6 | echo "Invalid output: Expected '$1', was '$2'" 7 | exit 1 8 | fi 9 | } 10 | 11 | assert_equals "result: 3" "$(just -f justfile/Justfile)" 12 | assert_equals "hello, rust" "$(./hello.ers)" 13 | assert_equals "hello, rust" "$(./hello-without-main.ers)" 14 | 15 | HYPERFINE_OUTPUT=$(rust-script --wrapper "hyperfine --runs 99" fib.ers) 16 | 17 | case "$HYPERFINE_OUTPUT" in 18 | *"99 runs"*) 19 | ;; 20 | *) 21 | echo "Hyperfine output: $HYPERFINE_OUTPUT" 22 | exit 1 23 | ;; 24 | esac 25 | -------------------------------------------------------------------------------- /src/build_kind.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone, Debug)] 2 | pub enum BuildKind { 3 | Normal, 4 | Test, 5 | Bench, 6 | } 7 | 8 | impl BuildKind { 9 | pub const fn exec_command(&self) -> &'static str { 10 | match *self { 11 | Self::Normal => "build", 12 | Self::Test => "test", 13 | Self::Bench => "bench", 14 | } 15 | } 16 | 17 | pub fn from_flags(test: bool, bench: bool) -> Self { 18 | match (test, bench) { 19 | (false, false) => Self::Normal, 20 | (true, false) => Self::Test, 21 | (false, true) => Self::Bench, 22 | _ => panic!("got both test and bench"), 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/data/extern-c-main.rs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rust-script 2 | //! ```cargo 3 | //! [dependencies] 4 | //! libc = { version = "0.2", default-features = false } 5 | //! 6 | //! [profile.release] 7 | //! strip = true 8 | //! lto = true 9 | //! opt-level = "s" # "z" 10 | //! codegen-units = 1 11 | //! panic = "abort" 12 | //! ``` 13 | 14 | #![no_std] 15 | #![no_main] 16 | 17 | #[panic_handler] 18 | fn my_panic(_info: &core::panic::PanicInfo) -> ! { 19 | loop {} 20 | } 21 | 22 | #[no_mangle] 23 | pub extern "C" fn main(_argc: isize, _argv: *const *const u8) -> isize { 24 | unsafe { 25 | libc::printf("--output--\n\0".as_ptr() as *const _); 26 | libc::printf("hello, world\n\0".as_ptr() as *const _); 27 | } 28 | 0 29 | } 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.36.0](https://github.com/fornwall/rust-script/releases/tag/0.36.0) 2025-08-16 4 | ### Fixed 5 | - Fix issue with `--clear-cache` when "projects" directory doesn't exist ([#152](https://github.com/fornwall/rust-script/pull/152)). 6 | 7 | ### Added 8 | - Allow passing additional arguments to `cargo test` and `cargo bench` ([#146](https://github.com/fornwall/rust-script/pull/146)). 9 | 10 | ### Internal 11 | - Update dependencies. 12 | 13 | ## [0.35.0](https://github.com/fornwall/rust-script/releases/tag/0.35.0) 2024-09-03 14 | ### Fixed 15 | - Make `RUST_SCRIPT_BASE_PATH` report the correct path when `rust-script` executes with `--base-path` ([#136](https://github.com/fornwall/rust-script/pull/136)). 16 | - Bump dependencies, raising MSRV from `1.64` to `1.74` ([#138](https://github.com/fornwall/rust-script/pull/138)). 17 | 18 | ## [0.34.0](https://github.com/fornwall/rust-script/releases/tag/0.34.0) 2023-09-27 19 | ### Added 20 | - Publish binaries on GitHub releases, for use with e.g. `cargo binstall`. 21 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | # Why is this here? 3 | 4 | Because *both* Cargo and Rust both know better than I do and won't let me tell them to stop running the tests in parallel. This is a problem because they do not, in fact, know better than me: Cargo doesn't do *any* locking, which causes random failures as two tests try to update the registry simultaneously (quite *why* Cargo needs to update the registry so fucking often I have no damn idea). 5 | 6 | *All* integration tests have to be glommed into a single runner so that we can use locks to prevent Cargo from falling over and breaking both its legs as soon as a gentle breeze comes along. I *would* do this "properly" using file locks, except that's apparently impossible in Rust without writing the whole stack yourself directly on native OS calls, and I just can't be arsed to go to *that* much effort just to get some bloody tests to work. 7 | */ 8 | #[macro_use] 9 | extern crate lazy_static; 10 | #[macro_use] 11 | extern crate scan_rules; 12 | #[macro_use] 13 | mod util; 14 | 15 | mod tests { 16 | mod expr; 17 | mod others; 18 | mod script; 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /tests/scripts/test-runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -u 3 | 4 | ANY_ERROR=0 5 | 6 | # Make sure newly built binary is first in PATH: 7 | cargo build &> /dev/null || { 8 | echo "ERROR: Compilation failed" 9 | exit 1 10 | } 11 | export PATH=$PWD/target/debug/:$PATH 12 | cd tests/scripts 13 | 14 | for TEST_SCRIPT in *.script; do 15 | EXPECTED_STDOUT=${TEST_SCRIPT/.script/.expected} 16 | ACTUAL_STDOUT=${TEST_SCRIPT/.script/.actual-stdout} 17 | ACTUAL_STDERR=${TEST_SCRIPT/.script/.actual-stderr} 18 | echo -n "Running $TEST_SCRIPT ... " 19 | 20 | ./$TEST_SCRIPT > $ACTUAL_STDOUT 2> $ACTUAL_STDERR || { 21 | ANY_ERROR=1 22 | echo "Failed to run!" 23 | } 24 | 25 | if cmp -s "$EXPECTED_STDOUT" "$ACTUAL_STDOUT"; then 26 | echo "Ok" 27 | else 28 | ANY_ERROR=1 29 | echo "Failed!" 30 | echo "######################## Expected:" 31 | cat $EXPECTED_STDOUT 32 | echo "######################## Actual:" 33 | cat $ACTUAL_STDOUT 34 | echo "######################## Error output:" 35 | cat $ACTUAL_STDERR 36 | echo "########################" 37 | fi 38 | done 39 | 40 | exit $ANY_ERROR 41 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-script" 3 | version = "0.36.0" 4 | edition = "2021" 5 | rust-version = "1.74" 6 | authors = ["Fredrik Fornwall "] 7 | description = "Command-line tool to run Rust \"scripts\" which can make use of crates." 8 | homepage = "https://rust-script.org" 9 | documentation = "https://rust-script.org" 10 | repository = "https://github.com/fornwall/rust-script" 11 | readme = "README.md" 12 | license = "MIT/Apache-2.0" 13 | keywords = ["cargo", "script"] 14 | categories = ["command-line-utilities", "development-tools"] 15 | 16 | exclude = [ 17 | "_config.yml", 18 | "CNAME", 19 | ".github", 20 | "target" 21 | ] 22 | 23 | [dependencies] 24 | clap = "4" 25 | dirs = "6" 26 | env_logger = "0.11" 27 | log = "0.4" 28 | pulldown-cmark = "0.13" 29 | regex = "1" 30 | sha1 = "0.10" 31 | shell-words = "1" 32 | tempfile = "3" 33 | toml = "0.9" 34 | 35 | [target.'cfg(windows)'.dependencies] 36 | winreg = "0.55" 37 | 38 | [dev-dependencies] 39 | lazy_static = "1" 40 | scan-rules = "0.2" 41 | 42 | [profile.release] 43 | lto = true 44 | 45 | [features] 46 | default=[] 47 | online_tests=[] 48 | -------------------------------------------------------------------------------- /src/defer.rs: -------------------------------------------------------------------------------- 1 | //! This module contains an implementation of Defer. 2 | use log::error; 3 | use std::error::Error; 4 | use std::marker::PhantomData; 5 | 6 | /// Used to defer a closure until the value is dropped. 7 | /// 8 | /// The closure *must* return a `Result<(), _>`, as a reminder to *not* panic; doing so will abort your whole program if it happens during another panic. If the closure returns an `Err`, then it is logged as an `error`. 9 | /// 10 | /// A `Defer` can also be "disarmed", preventing the closure from running at all. 11 | #[must_use] 12 | pub struct Defer<'a, F, E>(Option, PhantomData<&'a F>) 13 | where 14 | F: 'a + FnOnce() -> Result<(), E>, 15 | E: Error; 16 | 17 | impl<'a, F, E> Defer<'a, F, E> 18 | where 19 | F: 'a + FnOnce() -> Result<(), E>, 20 | E: Error, 21 | { 22 | /// Create a new `Defer` with the given closure. 23 | pub fn new(f: F) -> Defer<'a, F, E> { 24 | Defer(Some(f), PhantomData) 25 | } 26 | 27 | /// Consume this `Defer` *without* invoking the closure. 28 | pub fn disarm(mut self) { 29 | self.0 = None; 30 | drop(self); 31 | } 32 | } 33 | 34 | impl<'a, F, E> ::std::ops::Drop for Defer<'a, F, E> 35 | where 36 | F: 'a + FnOnce() -> Result<(), E>, 37 | E: Error, 38 | { 39 | fn drop(&mut self) { 40 | if let Some(f) = self.0.take() { 41 | if let Err(err) = f() { 42 | error!("deferred function failed: {}", err); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/templates.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This module contains code related to template support. 3 | */ 4 | use crate::error::{MainError, MainResult}; 5 | use regex::Regex; 6 | use std::collections::HashMap; 7 | 8 | pub fn expand(src: &str, subs: &HashMap<&str, &str>) -> MainResult { 9 | let re_sub = Regex::new(r"#\{([A-Za-z_][A-Za-z0-9_]*)}").unwrap(); 10 | 11 | // The estimate of final size is the sum of the size of all the input. 12 | let sub_size = subs.values().map(|v| v.len()).sum::(); 13 | let est_size = src.len() + sub_size; 14 | 15 | let mut anchor = 0; 16 | let mut result = String::with_capacity(est_size); 17 | 18 | for m in re_sub.captures_iter(src) { 19 | // Concatenate the static bit just before the match. 20 | let (m_start, m_end) = { 21 | let m_0 = m.get(0).unwrap(); 22 | (m_0.start(), m_0.end()) 23 | }; 24 | let prior_slice = anchor..m_start; 25 | anchor = m_end; 26 | result.push_str(&src[prior_slice]); 27 | 28 | // Concat the substitution. 29 | let sub_name = m.get(1).unwrap().as_str(); 30 | match subs.get(sub_name) { 31 | Some(s) => result.push_str(s), 32 | None => { 33 | return Err(MainError::OtherOwned(format!( 34 | "substitution `{}` in template is unknown", 35 | sub_name 36 | ))) 37 | } 38 | } 39 | } 40 | result.push_str(&src[anchor..]); 41 | Ok(result) 42 | } 43 | -------------------------------------------------------------------------------- /tests/tests/expr.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_expr_0() { 3 | let out = rust_script!("-e", with_output_marker!("0")).unwrap(); 4 | scan!(out.stdout_output(); 5 | ("0") => () 6 | ) 7 | .unwrap() 8 | } 9 | 10 | #[test] 11 | fn test_expr_comma() { 12 | let out = rust_script!("-e", with_output_marker!("[1, 2, 3]")).unwrap(); 13 | scan!(out.stdout_output(); 14 | ("[1, 2, 3]") => () 15 | ) 16 | .unwrap() 17 | } 18 | 19 | #[test] 20 | fn test_expr_dnc() { 21 | let out = rust_script!("-e", "swing begin").unwrap(); 22 | assert!(!out.success()); 23 | } 24 | 25 | #[test] 26 | fn test_expr_temporary() { 27 | let out = rust_script!("-e", "[1].iter().max()").unwrap(); 28 | assert!(out.success()); 29 | } 30 | 31 | #[cfg_attr(not(feature = "online_tests"), ignore)] 32 | #[test] 33 | fn test_expr_dep() { 34 | let out = rust_script!( 35 | "-d", 36 | "boolinator=0.1.0", 37 | "-e", 38 | with_output_marker!( 39 | prelude "use boolinator::Boolinator;"; 40 | "true.as_some(1)" 41 | ) 42 | ) 43 | .unwrap(); 44 | scan!(out.stdout_output(); 45 | ("Some(1)") => () 46 | ) 47 | .unwrap(); 48 | } 49 | 50 | #[test] 51 | fn test_expr_panic() { 52 | let out = rust_script!("-e", with_output_marker!("panic!()")).unwrap(); 53 | assert!(!out.success()); 54 | } 55 | 56 | #[test] 57 | fn test_expr_qmark() { 58 | let code = with_output_marker!("\"42\".parse::()?.wrapping_add(1)"); 59 | let out = rust_script!("-e", code).unwrap(); 60 | scan!(out.stdout_output(); 61 | ("43") => () 62 | ) 63 | .unwrap(); 64 | } 65 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Definition of the program's main error type. 3 | */ 4 | 5 | use std::borrow::Cow; 6 | use std::error::Error; 7 | use std::fmt; 8 | use std::io; 9 | use std::result::Result; 10 | 11 | /// Shorthand for the program's common result type. 12 | pub type MainResult = Result; 13 | 14 | /// An error in the program. 15 | #[derive(Debug)] 16 | pub enum MainError { 17 | Io(io::Error), 18 | Tag(Cow<'static, str>, Box), 19 | Other(Box), 20 | OtherOwned(String), 21 | OtherBorrowed(&'static str), 22 | } 23 | 24 | impl fmt::Display for MainError { 25 | fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { 26 | use self::MainError::*; 27 | use std::fmt::Display; 28 | match self { 29 | Io(err) => Display::fmt(err, fmt), 30 | Tag(msg, ref err) => write!(fmt, "{}: {}", msg, err), 31 | Other(err) => Display::fmt(err, fmt), 32 | OtherOwned(err) => Display::fmt(err, fmt), 33 | OtherBorrowed(err) => Display::fmt(err, fmt), 34 | } 35 | } 36 | } 37 | 38 | impl Error for MainError {} 39 | 40 | macro_rules! from_impl { 41 | ($src_ty:ty => $dst_ty:ty, $src:ident -> $e:expr) => { 42 | impl From<$src_ty> for $dst_ty { 43 | fn from($src: $src_ty) -> $dst_ty { 44 | $e 45 | } 46 | } 47 | }; 48 | } 49 | 50 | from_impl! { io::Error => MainError, v -> MainError::Io(v) } 51 | from_impl! { String => MainError, v -> MainError::OtherOwned(v) } 52 | from_impl! { &'static str => MainError, v -> MainError::OtherBorrowed(v) } 53 | 54 | impl From> for MainError 55 | where 56 | T: 'static + Error, 57 | { 58 | fn from(src: Box) -> Self { 59 | Self::Other(src) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.*' 7 | 8 | jobs: 9 | create-release: 10 | name: "Create GitHub release" 11 | # only publish from the origin repository 12 | if: github.repository_owner == 'fornwall' 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v5 16 | - uses: taiki-e/create-gh-release-action@v1 17 | with: 18 | changelog: CHANGELOG.md 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | crates: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v5 26 | - name: publish package to crates 27 | run: | 28 | cargo package 29 | cargo publish --token ${{ secrets.CARGO_TOKEN }} 30 | 31 | binaries: 32 | name: "Upload release binaries" 33 | needs: 34 | - create-release 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | include: 39 | - target: x86_64-unknown-linux-gnu 40 | os: ubuntu-latest 41 | - target: x86_64-unknown-linux-musl 42 | os: ubuntu-latest 43 | - target: x86_64-apple-darwin 44 | os: macos-latest 45 | - target: x86_64-pc-windows-msvc 46 | os: windows-latest 47 | - target: aarch64-unknown-linux-gnu 48 | os: ubuntu-latest 49 | - target: aarch64-unknown-linux-musl 50 | os: ubuntu-latest 51 | - target: aarch64-apple-darwin 52 | os: macos-latest 53 | - target: universal-apple-darwin 54 | os: macos-latest 55 | runs-on: ${{ matrix.os }} 56 | steps: 57 | - uses: actions/checkout@v5 58 | - uses: dtolnay/rust-toolchain@stable 59 | - uses: taiki-e/upload-rust-binary-action@v1 60 | with: 61 | bin: rust-script 62 | target: ${{ matrix.target }} 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![CI](https://github.com/fornwall/rust-script/workflows/CI/badge.svg)](https://github.com/fornwall/rust-script/actions?query=workflow%3ACI) 3 | [![Crates.io](https://img.shields.io/crates/v/rust-script.svg)](https://crates.io/crates/rust-script) 4 | [![MSRV](https://img.shields.io/badge/rustc-1.74.0+-ab6000.svg)](https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html) 5 | 6 | # rust-script 7 | Run Rust script files without any setup or explicit compilation step, with seamless use of crates specified as dependencies inside the scripts. 8 | 9 | ```sh 10 | $ cargo install rust-script 11 | [...] 12 | 13 | $ cat script.rs 14 | #!/usr/bin/env rust-script 15 | //! Dependencies can be specified in the script file itself as follows: 16 | //! 17 | //! ```cargo 18 | //! [dependencies] 19 | //! rand = "0.8.0" 20 | //! ``` 21 | 22 | use rand::prelude::*; 23 | 24 | fn main() { 25 | let x: u64 = random(); 26 | println!("A random number: {}", x); 27 | } 28 | 29 | $ ./script.rs 30 | A random number: 9240261453149857564 31 | ``` 32 | 33 | Rust version 1.74 or newer required. 34 | 35 | See the [documentation at rust-script.org](https://rust-script.org). 36 | 37 | ## Related projects 38 | - [cargo-script](https://github.com/DanielKeep/cargo-script) - the unmaintained project that `rust-script` was forked from. 39 | - [cargo-eval](https://github.com/reitermarkus/cargo-eval/) - maintained fork of `cargo-script`. 40 | - [cargo-play](https://github.com/fanzeyi/cargo-play) - local Rust playground. 41 | - [runner](https://github.com/stevedonovan/runner/) - tool for running Rust snippets. 42 | - [scriptisto](https://github.com/igor-petruk/scriptisto) - language-agnostic "shebang interpreter" that enables you to write scripts in compiled languages. 43 | - [official cargo-script RFC](https://github.com/rust-lang/cargo/issues/12207) - in progress integration into cargo 44 | 45 | ## License 46 | `rust-script` is primarily distributed under the terms of both the [MIT license](LICENSE-MIT) and the [Apache License (Version 2.0)](LICENSE-APACHE). 47 | -------------------------------------------------------------------------------- /src/platform.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This module is for platform-specific stuff. 3 | */ 4 | 5 | pub use self::inner::force_cargo_color; 6 | 7 | use std::fs; 8 | 9 | use std::path::PathBuf; 10 | use std::time::{SystemTime, UNIX_EPOCH}; 11 | 12 | // Last-modified time of a directory, in milliseconds since the UNIX epoch. 13 | pub fn dir_last_modified(dir: &fs::DirEntry) -> u128 { 14 | dir.metadata() 15 | .and_then(|md| { 16 | md.modified() 17 | .map(|t| t.duration_since(UNIX_EPOCH).unwrap().as_millis()) 18 | }) 19 | .unwrap_or(0) 20 | } 21 | 22 | // Current system time, in milliseconds since the UNIX epoch. 23 | pub fn current_time() -> u128 { 24 | SystemTime::now() 25 | .duration_since(UNIX_EPOCH) 26 | .unwrap() 27 | .as_millis() 28 | } 29 | 30 | pub fn cache_dir() -> PathBuf { 31 | #[cfg(not(test))] 32 | { 33 | dirs::cache_dir() 34 | .map(|dir| dir.join(crate::consts::PROGRAM_NAME)) 35 | .expect("Cannot get cache directory") 36 | } 37 | #[cfg(test)] 38 | { 39 | use lazy_static::lazy_static; 40 | lazy_static! { 41 | static ref TEMP_DIR: tempfile::TempDir = tempfile::TempDir::new().unwrap(); 42 | } 43 | TEMP_DIR.path().to_path_buf() 44 | } 45 | } 46 | 47 | pub fn generated_projects_cache_path() -> PathBuf { 48 | cache_dir().join("projects") 49 | } 50 | 51 | pub fn binary_cache_path() -> PathBuf { 52 | cache_dir().join("binaries") 53 | } 54 | 55 | #[cfg(unix)] 56 | mod inner { 57 | use std::io::IsTerminal as _; 58 | 59 | /** 60 | Returns `true` if `rust-script` should force Cargo to use coloured output. 61 | 62 | This depends on whether `rust-script`'s STDERR is connected to a TTY or not. 63 | */ 64 | pub fn force_cargo_color() -> bool { 65 | std::io::stderr().is_terminal() 66 | } 67 | } 68 | 69 | #[cfg(windows)] 70 | pub mod inner { 71 | /** 72 | Returns `true` if `rust-script` should force Cargo to use coloured output. 73 | 74 | Always returns `false` on Windows because colour is communicated over a side-channel. 75 | */ 76 | pub fn force_cargo_color() -> bool { 77 | false 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/file_assoc.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This module deals with setting up file associations on Windows 3 | */ 4 | use crate::error::MainResult; 5 | use std::env; 6 | use std::io; 7 | use winreg::{enums as wre, RegKey}; 8 | 9 | pub fn install_file_association() -> MainResult<()> { 10 | let rust_script_path = env::current_exe()?.canonicalize()?; 11 | if !rust_script_path.exists() { 12 | return Err(format!("{:?} not found", rust_script_path).into()); 13 | } 14 | 15 | // We have to remove the `\\?\` prefix because, if we don't, the shell freaks out. 16 | let rust_script_path = rust_script_path.to_string_lossy(); 17 | let rust_script_path = if let Some(stripped) = rust_script_path.strip_prefix(r"\\?\") { 18 | stripped 19 | } else { 20 | &rust_script_path[..] 21 | }; 22 | 23 | let res = (|| -> io::Result<()> { 24 | let hlcr = RegKey::predef(wre::HKEY_CLASSES_ROOT); 25 | let (dot_ers, _) = hlcr.create_subkey(".ers")?; 26 | dot_ers.set_value("", &"RustScript.Ers")?; 27 | 28 | let (cs_ers, _) = hlcr.create_subkey("RustScript.Ers")?; 29 | cs_ers.set_value("", &"Rust Script")?; 30 | 31 | let (sh_o_c, _) = cs_ers.create_subkey(r"shell\open\command")?; 32 | sh_o_c.set_value("", &format!(r#""{}" "%1" %*"#, rust_script_path))?; 33 | Ok(()) 34 | })(); 35 | 36 | match res { 37 | Ok(()) => (), 38 | Err(e) => { 39 | if e.kind() == io::ErrorKind::PermissionDenied { 40 | println!( 41 | "Access denied. Make sure you run this command from an administrator prompt." 42 | ); 43 | } 44 | return Err(e.into()); 45 | } 46 | } 47 | 48 | println!("Created rust-script registry entry."); 49 | println!("- Handler set to: {}", rust_script_path); 50 | 51 | Ok(()) 52 | } 53 | 54 | pub fn uninstall_file_association() -> MainResult<()> { 55 | let mut ignored_missing = false; 56 | { 57 | let mut notify = || ignored_missing = true; 58 | 59 | let hlcr = RegKey::predef(wre::HKEY_CLASSES_ROOT); 60 | hlcr.delete_subkey(r"RustScript.Ers\shell\open\command") 61 | .ignore_missing_and(&mut notify)?; 62 | hlcr.delete_subkey(r"RustScript.Ers\shell\open") 63 | .ignore_missing_and(&mut notify)?; 64 | hlcr.delete_subkey(r"RustScript.Ers\shell") 65 | .ignore_missing_and(&mut notify)?; 66 | hlcr.delete_subkey(r"RustScript.Ers") 67 | .ignore_missing_and(&mut notify)?; 68 | } 69 | 70 | if ignored_missing { 71 | println!("Ignored some missing registry entries."); 72 | } 73 | println!("Deleted rust-script registry entry."); 74 | 75 | Ok(()) 76 | } 77 | 78 | trait IgnoreMissing { 79 | fn ignore_missing_and(self, f: F) -> Self 80 | where 81 | F: FnOnce(); 82 | } 83 | 84 | impl IgnoreMissing for io::Result<()> { 85 | fn ignore_missing_and(self, f: F) -> Self 86 | where 87 | F: FnOnce(), 88 | { 89 | match self { 90 | Ok(()) => Ok(()), 91 | Err(e) => { 92 | if e.kind() == io::ErrorKind::NotFound { 93 | f(); 94 | Ok(()) 95 | } else { 96 | Err(e) 97 | } 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/util/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Mutex; 2 | 3 | macro_rules! rust_script { 4 | ( 5 | #[env($($env_k:ident=$env_v:expr),* $(,)*)] 6 | $($args:expr),* $(,)* 7 | ) => { 8 | { 9 | extern crate tempfile; 10 | use std::process::Command; 11 | 12 | let cargo_lock = crate::util::CARGO_MUTEX.lock().expect("Could not acquire Cargo mutex"); 13 | 14 | let cmd_str; 15 | let out = { 16 | let target_dir = ::std::env::var("CARGO_TARGET_DIR") 17 | .unwrap_or_else(|_| String::from("target")); 18 | let mut cmd = Command::new(format!("{}/debug/rust-script", target_dir)); 19 | $( 20 | cmd.arg($args); 21 | )* 22 | 23 | cmd.env_remove("CARGO_TARGET_DIR"); 24 | $(cmd.env(stringify!($env_k), $env_v);)* 25 | 26 | cmd_str = format!("{:?}", cmd); 27 | 28 | cmd.output() 29 | .map(crate::util::Output::from) 30 | }; 31 | 32 | if let Ok(out) = out.as_ref() { 33 | println!("rust-script cmd: {}", cmd_str); 34 | println!("rust-script stdout:"); 35 | println!("-----"); 36 | println!("{}", out.stdout); 37 | println!("-----"); 38 | println!("rust-script stderr:"); 39 | println!("-----"); 40 | println!("{}", out.stderr); 41 | println!("-----"); 42 | } 43 | 44 | drop(cargo_lock); 45 | 46 | out 47 | } 48 | }; 49 | 50 | ($($args:expr),* $(,)*) => { 51 | rust_script!(#[env()] $($args),*) 52 | }; 53 | } 54 | 55 | macro_rules! with_output_marker { 56 | (prelude $p:expr; $e:expr) => { 57 | format!(concat!($p, "{}", $e), crate::util::OUTPUT_MARKER_CODE) 58 | }; 59 | 60 | ($e:expr) => { 61 | format!(concat!("{}", $e), crate::util::OUTPUT_MARKER_CODE) 62 | }; 63 | } 64 | 65 | lazy_static! { 66 | #[doc(hidden)] 67 | pub static ref CARGO_MUTEX: Mutex<()> = Mutex::new(()); 68 | } 69 | 70 | pub const OUTPUT_MARKER: &str = "--output--"; 71 | pub const OUTPUT_MARKER_CODE: &str = "println!(\"--output--\");"; 72 | 73 | pub struct Output { 74 | pub status: ::std::process::ExitStatus, 75 | pub stdout: String, 76 | pub stderr: String, 77 | } 78 | 79 | impl Output { 80 | pub fn stdout_output(&self) -> &str { 81 | assert!(self.success()); 82 | for marker in self.stdout.matches(OUTPUT_MARKER) { 83 | let i = subslice_offset(&self.stdout, marker).expect("couldn't find marker in output"); 84 | let before_cp = self.stdout[..i].chars().next_back().unwrap_or('\n'); 85 | if !(before_cp == '\r' || before_cp == '\n') { 86 | continue; 87 | } 88 | let after = &self.stdout[i + OUTPUT_MARKER.len()..]; 89 | let after_cp = after.chars().next().expect("couldn't find cp after marker"); 90 | if !(after_cp == '\r' || after_cp == '\n') { 91 | continue; 92 | } 93 | return after; 94 | } 95 | panic!("could not find `{}` in script output", OUTPUT_MARKER); 96 | } 97 | 98 | pub fn success(&self) -> bool { 99 | self.status.success() 100 | } 101 | } 102 | 103 | impl From<::std::process::Output> for Output { 104 | fn from(v: ::std::process::Output) -> Self { 105 | Self { 106 | status: v.status, 107 | stdout: String::from_utf8(v.stdout).unwrap(), 108 | stderr: String::from_utf8(v.stderr).unwrap(), 109 | } 110 | } 111 | } 112 | 113 | fn subslice_offset(outer: &str, inner: &str) -> Option { 114 | let outer_beg = outer.as_ptr() as usize; 115 | let inner = inner.as_ptr() as usize; 116 | if inner < outer_beg || inner > outer_beg.wrapping_add(outer.len()) { 117 | None 118 | } else { 119 | Some(inner.wrapping_sub(outer_beg)) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This module just contains any big string literals I don't want cluttering up the rest of the code. 3 | */ 4 | 5 | pub const PROGRAM_NAME: &str = "rust-script"; 6 | 7 | /* 8 | What follows are the templates used to wrap script input. 9 | */ 10 | 11 | /// Substitution for the script body. 12 | pub const SCRIPT_BODY_SUB: &str = "script"; 13 | 14 | /// Substitution for the script prelude. 15 | pub const SCRIPT_PRELUDE_SUB: &str = "prelude"; 16 | 17 | /// The template used for script file inputs that doesn't have main function. 18 | pub const FILE_NO_MAIN_TEMPLATE: &str = r#" 19 | fn main() -> Result<(), Box> { 20 | {#{script}} 21 | Ok(()) 22 | } 23 | "#; 24 | 25 | /// The template used for `--expr` input. 26 | pub const EXPR_TEMPLATE: &str = r#" 27 | #{prelude} 28 | use std::any::{Any, TypeId}; 29 | 30 | fn main() { 31 | let exit_code = match try_main() { 32 | Ok(()) => None, 33 | Err(e) => { 34 | use std::io::{self, Write}; 35 | let _ = writeln!(io::stderr(), "Error: {}", e); 36 | Some(1) 37 | }, 38 | }; 39 | if let Some(exit_code) = exit_code { 40 | std::process::exit(exit_code); 41 | } 42 | } 43 | 44 | fn try_main() -> Result<(), Box> { 45 | fn _rust_script_is_empty_tuple(_s: &T) -> bool { 46 | TypeId::of::<()>() == TypeId::of::() 47 | } 48 | match {#{script}} { 49 | __rust_script_expr if !_rust_script_is_empty_tuple(&__rust_script_expr) => println!("{:?}", __rust_script_expr), 50 | _ => {} 51 | } 52 | Ok(()) 53 | } 54 | "#; 55 | 56 | /* 57 | Regarding the loop templates: what I *want* is for the result of the closure to be printed to standard output *only* if it's not `()`. 58 | 59 | * TODO: Merge the `LOOP_*` templates so there isn't duplicated code. It's icky. 60 | */ 61 | 62 | /// The template used for `--loop` input, assuming no `--count` flag is also given. 63 | pub const LOOP_TEMPLATE: &str = r#" 64 | #![allow(unused_imports)] 65 | #![allow(unused_braces)] 66 | #{prelude} 67 | use std::any::Any; 68 | use std::io::prelude::*; 69 | 70 | fn main() { 71 | let mut closure = enforce_closure( 72 | {#{script}} 73 | ); 74 | let mut line_buffer = String::new(); 75 | let stdin = std::io::stdin(); 76 | loop { 77 | line_buffer.clear(); 78 | let read_res = stdin.read_line(&mut line_buffer).unwrap_or(0); 79 | if read_res == 0 { break } 80 | let output = closure(&line_buffer); 81 | 82 | let display = { 83 | let output_any: &dyn Any = &output; 84 | !output_any.is::<()>() 85 | }; 86 | 87 | if display { 88 | println!("{:?}", output); 89 | } 90 | } 91 | } 92 | 93 | fn enforce_closure(closure: F) -> F 94 | where F: FnMut(&str) -> T, T: 'static { 95 | closure 96 | } 97 | "#; 98 | 99 | /// The template used for `--count --loop` input. 100 | pub const LOOP_COUNT_TEMPLATE: &str = r#" 101 | #![allow(unused_imports)] 102 | #![allow(unused_braces)] 103 | use std::any::Any; 104 | use std::io::prelude::*; 105 | 106 | fn main() { 107 | let mut closure = enforce_closure( 108 | {#{script}} 109 | ); 110 | let mut line_buffer = String::new(); 111 | let stdin = std::io::stdin(); 112 | let mut count = 0; 113 | loop { 114 | line_buffer.clear(); 115 | let read_res = stdin.read_line(&mut line_buffer).unwrap_or(0); 116 | if read_res == 0 { break } 117 | count += 1; 118 | let output = closure(&line_buffer, count); 119 | 120 | let display = { 121 | let output_any: &dyn Any = &output; 122 | !output_any.is::<()>() 123 | }; 124 | 125 | if display { 126 | println!("{:?}", output); 127 | } 128 | } 129 | } 130 | 131 | fn enforce_closure(closure: F) -> F 132 | where F: FnMut(&str, usize) -> T, T: 'static { 133 | closure 134 | } 135 | "#; 136 | 137 | /** 138 | When generating a package's unique ID, how many hex nibbles of the digest should be used *at most*? 139 | 140 | The largest meaningful value is `40`. 141 | */ 142 | pub const ID_DIGEST_LEN_MAX: usize = 24; 143 | 144 | /** 145 | How old can stuff in the cache be before we automatically clear it out? 146 | 147 | Measured in milliseconds. 148 | */ 149 | pub const MAX_CACHE_AGE_MS: u128 = 7 * 24 * 60 * 60 * 1000; 150 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | 9 | test: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | rust: ["1.74.0", "stable"] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Checkout source 17 | uses: actions/checkout@v5 18 | - name: Setup rust 19 | uses: dtolnay/rust-toolchain@stable 20 | with: 21 | toolchain: ${{ matrix.rust }} 22 | - name: Run unit tests 23 | run: rustc --version && cargo --version && cargo test --features online_tests 24 | - name: Run script tests 25 | if: runner.os != 'Windows' 26 | run: | 27 | # Run twice to test problem with expression caching 28 | ./tests/scripts/test-runner.sh 29 | ./tests/scripts/test-runner.sh 30 | 31 | test-examples: 32 | strategy: 33 | matrix: 34 | os: [ubuntu-latest, macos-latest] 35 | rust: ["1.74.0", "stable"] 36 | runs-on: ${{ matrix.os }} 37 | steps: 38 | - name: Checkout source 39 | uses: actions/checkout@v5 40 | - name: Setup rust 41 | uses: dtolnay/rust-toolchain@stable 42 | with: 43 | toolchain: ${{ matrix.rust }} 44 | - name: Setup homebrew 45 | uses: Homebrew/actions/setup-homebrew@master 46 | - name: Install hyperfine and just 47 | run: brew install hyperfine just 48 | - name: Install rust-script 49 | run: cargo install --locked --path . 50 | - name: Test examples 51 | run: ./test-examples.sh 52 | working-directory: examples 53 | 54 | check-format: 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Checkout source 58 | uses: actions/checkout@v5 59 | - name: Setup rust 60 | uses: dtolnay/rust-toolchain@stable 61 | - name: Install rustfmt 62 | run: rustup component add rustfmt 63 | - name: Check formatting with rustfmt 64 | run: cargo fmt -- --check 65 | 66 | check-clippy: 67 | strategy: 68 | matrix: 69 | os: [ubuntu-latest, macos-latest, windows-latest] 70 | runs-on: ${{ matrix.os }} 71 | steps: 72 | - name: Checkout source 73 | uses: actions/checkout@v5 74 | - name: Setup rust 75 | uses: dtolnay/rust-toolchain@stable 76 | - name: Install rustfmt 77 | run: rustup component add clippy 78 | - name: Check for clippy warnings 79 | run: cargo clippy --all-targets --all-features -- -D warnings -W clippy::cargo 80 | 81 | upload-debug-builds: 82 | strategy: 83 | matrix: 84 | os: [ubuntu-latest, macos-latest, windows-latest] 85 | runs-on: ${{ matrix.os }} 86 | steps: 87 | - name: Checkout source 88 | uses: actions/checkout@v5 89 | - name: Setup rust 90 | uses: dtolnay/rust-toolchain@stable 91 | - name: Build debug 92 | run: cargo build 93 | - name: Upload Windows debug build 94 | if: runner.os == 'Windows' 95 | uses: actions/upload-artifact@v4 96 | with: 97 | path: ./target/debug/rust-script.exe 98 | name: windows-binary 99 | - name: Upload macOS debug build 100 | if: runner.os == 'macOS' 101 | uses: actions/upload-artifact@v4 102 | with: 103 | path: ./target/debug/rust-script 104 | name: mac-binary 105 | - name: Upload Linux debug build 106 | if: runner.os == 'Linux' 107 | uses: actions/upload-artifact@v4 108 | with: 109 | path: ./target/debug/rust-script 110 | name: linux-binary 111 | 112 | test-install-file-association: 113 | runs-on: windows-latest 114 | steps: 115 | - name: Checkout source 116 | uses: actions/checkout@v5 117 | - name: Setup rust 118 | uses: dtolnay/rust-toolchain@stable 119 | - name: Build debug 120 | run: cargo build 121 | - name: Install file association 122 | run: ./target/debug/rust-script.exe --install-file-association 123 | - name: Run example script 124 | run: cmd.exe /C .\examples\hello.ers 125 | - name: Uninstall file association 126 | run: ./target/debug/rust-script.exe --uninstall-file-association 127 | - name: Run example script 128 | run: cmd.exe /C .\examples\hello.ers 129 | continue-on-error: true 130 | 131 | security-audit: 132 | runs-on: ubuntu-latest 133 | steps: 134 | - uses: actions/checkout@v5 135 | - uses: actions-rs/audit-check@v1 136 | with: 137 | token: ${{ secrets.GITHUB_TOKEN }} 138 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | - [Overview](#overview) 6 | - [News](#news) 7 | - [Installation](#installation) 8 | - [Distro Packages](#distro-packages) 9 | - [Arch Linux](#arch-linux) 10 | - [Scripts](#scripts) 11 | - [Executable Scripts](#executable-scripts) 12 | - [Expressions](#expressions) 13 | - [Filters](#filters) 14 | - [Environment Variables](#environment-variables) 15 | - [Troubleshooting](#troubleshooting) 16 | 17 | ## Overview 18 | 19 | With `rust-script` Rust files and expressions can be executed just like a shell or Python script. Features include: 20 | 21 | - Caching compiled artifacts for speed. 22 | - Reading Cargo manifests embedded in Rust scripts. 23 | - Supporting executable Rust scripts via Unix shebangs and Windows file associations. 24 | - Using expressions as stream filters (*i.e.* for use in command pipelines). 25 | - Running unit tests and benchmarks from scripts. 26 | 27 | You can get an overview of the available options using the `--help` flag. 28 | 29 | ## News 30 | See the [changelog](https://github.com/fornwall/rust-script/blob/main/CHANGELOG.md) for information about releases and changes. 31 | 32 | ## Installation 33 | 34 | Install or update `rust-script` using Cargo: 35 | 36 | ```sh 37 | cargo install rust-script 38 | ``` 39 | 40 | Rust 1.74 or later is required. 41 | 42 | ### Distro Packages 43 | 44 | #### Arch Linux 45 | 46 | `rust-script` can be installed from the [extra repository](https://archlinux.org/packages/extra/x86_64/rust-script/): 47 | 48 | ```sh 49 | pacman -S rust-script 50 | ``` 51 | 52 | #### Homebrew 53 | 54 | `rust-script` can be installed from the [Homebrew](https://brew.sh/): 55 | 56 | ```sh 57 | brew install rust-script 58 | ``` 59 | 60 | ## Scripts 61 | 62 | The primary use for `rust-script` is for running Rust source files as scripts. For example: 63 | 64 | ```sh 65 | $ echo 'println!("Hello, World!");' > hello.rs 66 | $ rust-script hello.rs 67 | Hello, World! 68 | ``` 69 | 70 | Under the hood, a Cargo project will be generated and built (with the Cargo output hidden unless compilation fails or the `-c`/`--cargo-output` option is used). The first invocation of the script will be slower as the script is compiled - subsequent invocations of unmodified scripts will be fast as the built executable is cached. 71 | 72 | As seen from the above example, using a `fn main() {}` function is not required. If not present, the script file will be wrapped in a `fn main() { ... }` block. 73 | 74 | `rust-script` will look for embedded dependency and manifest information in the script as shown by the below two equivalent `now.rs` variants: 75 | 76 | ```rust 77 | #!/usr/bin/env rust-script 78 | //! This is a regular crate doc comment, but it also contains a partial 79 | //! Cargo manifest. Note the use of a *fenced* code block, and the 80 | //! `cargo` "language". 81 | //! 82 | //! ```cargo 83 | //! [dependencies] 84 | //! time = "0.1.25" 85 | //! ``` 86 | fn main() { 87 | println!("{}", time::now().rfc822z()); 88 | } 89 | ``` 90 | 91 | ```rust 92 | // cargo-deps: time="0.1.25" 93 | // You can also leave off the version number, in which case, it's assumed 94 | // to be "*". Also, the `cargo-deps` comment *must* be a single-line 95 | // comment, and it *must* be the first thing in the file, after the 96 | // shebang. 97 | // Multiple dependencies should be separated by commas: 98 | // cargo-deps: time="0.1.25", libc="0.2.5" 99 | fn main() { 100 | println!("{}", time::now().rfc822z()); 101 | } 102 | ``` 103 | 104 | The output from running one of the above scripts may look something like: 105 | 106 | ```sh 107 | $ rust-script now 108 | Wed, 28 Oct 2020 00:38:45 +0100 109 | ``` 110 | 111 | Useful command-line arguments: 112 | 113 | - `--bench`: Compile and run benchmarks. Requires a nightly toolchain. 114 | - `--debug`: Build a debug executable, not an optimised one. 115 | - `--force`: Force the script to be rebuilt. Useful if you want to force a recompile with a different toolchain. 116 | - `--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. 117 | - `--test`: Compile and run tests. 118 | - `--wrapper`: Add a wrapper around the executable. Can be used to run debugging with e.g. `rust-script --debug --wrapper rust-lldb my-script.rs` or benchmarking with `rust-script --wrapper "hyperfine --runs 100" my-script.rs` 119 | 120 | ## Executable Scripts 121 | 122 | On Unix systems, you can use `#!/usr/bin/env rust-script` as a shebang line in a Rust script. This will allow you to execute a script files (which don't need to have the `.rs` file extension) directly. 123 | 124 | If you are using Windows, you can associate the `.ers` extension (executable Rust - a renamed `.rs` file) with `rust-script`. This allows you to execute Rust scripts simply by naming them like any other executable or script. 125 | 126 | This can be done using the `rust-script --install-file-association` command. Uninstall the file association with `rust-script --uninstall-file-association`. 127 | 128 | If you want to make a script usable across platforms, use *both* a shebang line *and* give the file a `.ers` file extension. 129 | 130 | ## Expressions 131 | 132 | Using the `-e`/`--expr` option a Rust expression can be evaluated directly, with dependencies (if any) added using `-d`/`--dep`: 133 | 134 | ```sh 135 | $ rust-script -e '1+2' 136 | 3 137 | $ rust-script --dep time --expr "time::OffsetDateTime::now_utc().format(time::Format::Rfc3339).to_string()"` 138 | "2020-10-28T11:42:10+00:00" 139 | $ # Use a specific version of the time crate (instead of default latest): 140 | $ rust-script --dep time=0.1.38 -e "time::now().rfc822z().to_string()" 141 | "2020-10-28T11:42:10+00:00" 142 | ``` 143 | 144 | The code given is embedded into a block expression, evaluated, and printed out using the `Debug` formatter (*i.e.* `{:?}`). 145 | 146 | ## Filters 147 | 148 | You can use `rust-script` to write a quick filter, by specifying a closure to be called for each line read from stdin, like so: 149 | 150 | ```sh 151 | $ cat now.ers | rust-script --loop \ 152 | "let mut n=0; move |l| {n+=1; println!(\"{:>6}: {}\",n,l.trim_end())}" 153 | 1: // cargo-deps: time="0.1.25" 154 | 3: fn main() { 155 | 4: println!("{}", time::now().rfc822z()); 156 | 5: } 157 | ``` 158 | 159 | You can achieve a similar effect to the above by using the `--count` flag, which causes the line number to be passed as a second argument to your closure: 160 | 161 | ```sh 162 | $ cat now.ers | rust-script --count --loop \ 163 | "|l,n| println!(\"{:>6}: {}\", n, l.trim_end())" 164 | 1: // cargo-deps: time="0.1.25" 165 | 2: fn main() { 166 | 3: println!("{}", time::now().rfc822z()); 167 | 4: } 168 | ``` 169 | 170 | ## Environment Variables 171 | 172 | The following environment variables are provided to scripts by `rust-script`: 173 | 174 | - `RUST_SCRIPT_BASE_PATH`: the base path used by `rust-script` to resolve relative dependency paths. Note that this is *not* necessarily the same as either the working directory, or the directory in which the script is being compiled. 175 | 176 | - `RUST_SCRIPT_PKG_NAME`: the generated package name of the script. 177 | 178 | - `RUST_SCRIPT_SAFE_NAME`: the file name of the script (sans file extension) being run. For scripts, this is derived from the script's filename. May also be `"expr"` or `"loop"` for those invocations. 179 | 180 | - `RUST_SCRIPT_PATH`: absolute path to the script being run, assuming one exists. Set to the empty string for expressions. 181 | 182 | ## Troubleshooting 183 | 184 | Please report all issues on [the GitHub issue tracker](https://github.com/fornwall/rust-script/issues). 185 | 186 | If relevant, run with the `RUST_LOG=rust_script=trace` environment variable set to see verbose log output and attach that output to an issue. 187 | -------------------------------------------------------------------------------- /tests/tests/script.rs: -------------------------------------------------------------------------------- 1 | #[cfg_attr(not(feature = "online_tests"), ignore)] 2 | #[test] 3 | fn test_script_explicit() { 4 | let out = rust_script!("-d", "boolinator", "tests/data/script-explicit.rs").unwrap(); 5 | scan!(out.stdout_output(); 6 | ("Some(1)") => () 7 | ) 8 | .unwrap() 9 | } 10 | 11 | #[cfg_attr(not(feature = "online_tests"), ignore)] 12 | #[test] 13 | fn test_script_full_block() { 14 | let out = rust_script!("tests/data/script-full-block.rs").unwrap(); 15 | scan!(out.stdout_output(); 16 | ("Some(1)") => () 17 | ) 18 | .unwrap() 19 | } 20 | 21 | #[cfg_attr(not(feature = "online_tests"), ignore)] 22 | #[test] 23 | fn test_script_full_line() { 24 | let out = rust_script!("tests/data/script-full-line.rs").unwrap(); 25 | scan!(out.stdout_output(); 26 | ("Some(1)") => () 27 | ) 28 | .unwrap() 29 | } 30 | 31 | #[cfg_attr(not(feature = "online_tests"), ignore)] 32 | #[test] 33 | fn test_script_full_line_without_main() { 34 | let out = rust_script!("tests/data/script-full-line-without-main.rs").unwrap(); 35 | scan!(out.stdout_output(); 36 | ("Some(1)") => () 37 | ) 38 | .unwrap() 39 | } 40 | 41 | #[test] 42 | fn test_script_main_with_space() { 43 | let out = rust_script!("tests/data/script-main-with-spaces.rs").unwrap(); 44 | scan!(out.stdout_output(); 45 | ("Hello, World!") => () 46 | ) 47 | .unwrap() 48 | } 49 | 50 | #[test] 51 | fn test_script_invalid_doc_comment() { 52 | let out = rust_script!("tests/data/script-invalid-doc-comment.rs").unwrap(); 53 | scan!(out.stdout_output(); 54 | ("Hello, World!") => () 55 | ) 56 | .unwrap() 57 | } 58 | 59 | #[test] 60 | fn test_script_no_deps() { 61 | let out = rust_script!("tests/data/script-no-deps.rs").unwrap(); 62 | scan!(out.stdout_output(); 63 | ("Hello, World!") => () 64 | ) 65 | .unwrap() 66 | } 67 | 68 | #[cfg_attr(not(feature = "online_tests"), ignore)] 69 | #[test] 70 | fn test_script_short() { 71 | let out = rust_script!("tests/data/script-short.rs").unwrap(); 72 | scan!(out.stdout_output(); 73 | ("Some(1)") => () 74 | ) 75 | .unwrap() 76 | } 77 | 78 | #[cfg_attr(not(feature = "online_tests"), ignore)] 79 | #[test] 80 | fn test_script_short_without_main() { 81 | let out = rust_script!("tests/data/script-short-without-main.rs").unwrap(); 82 | scan!(out.stdout_output(); 83 | ("Some(1)") => () 84 | ) 85 | .unwrap() 86 | } 87 | 88 | #[test] 89 | fn test_script_test() { 90 | let out = rust_script!("--test", "tests/data/script-test.rs").unwrap(); 91 | assert!(out.success()); 92 | assert!(out.stdout.contains("running 1 test")); 93 | } 94 | 95 | #[test] 96 | fn test_script_test_extra_args_for_cargo() { 97 | let out = rust_script!("--test", "tests/data/script-test-extra-args.rs", "--help",).unwrap(); 98 | assert!(out.success()); 99 | assert!(out 100 | .stdout 101 | .contains("Execute all unit and integration tests")); 102 | } 103 | 104 | #[test] 105 | fn test_script_test_extra_args_for_test() { 106 | let out = rust_script!( 107 | "--test", 108 | "tests/data/script-test-extra-args.rs", 109 | "--", 110 | "--nocapture" 111 | ) 112 | .unwrap(); 113 | assert!(out.success()); 114 | assert!(out.stdout.contains("Hello, world!")); 115 | } 116 | 117 | #[test] 118 | fn test_script_hyphens() { 119 | use scan_rules::scanner::QuotedString; 120 | let out = rust_script!("--", "tests/data/script-args.rs", "-NotAnArg").unwrap(); 121 | scan!(out.stdout_output(); 122 | ("[0]:", let _: QuotedString, "[1]:", let arg: QuotedString) => { 123 | assert_eq!(arg, "-NotAnArg"); 124 | } 125 | ) 126 | .unwrap() 127 | } 128 | 129 | #[test] 130 | fn test_script_hyphens_without_separator() { 131 | use scan_rules::scanner::QuotedString; 132 | let out = rust_script!("tests/data/script-args.rs", "-NotAnArg").unwrap(); 133 | scan!(out.stdout_output(); 134 | ("[0]:", let _: QuotedString, "[1]:", let arg: QuotedString) => { 135 | assert_eq!(arg, "-NotAnArg"); 136 | } 137 | ) 138 | .unwrap() 139 | } 140 | 141 | #[test] 142 | fn test_script_has_weird_chars() { 143 | let out = rust_script!("tests/data/script-has.weird§chars!.rs").unwrap(); 144 | assert!(out.success()); 145 | } 146 | 147 | #[test] 148 | fn test_script_cs_env() { 149 | let out = rust_script!("tests/data/script-cs-env.rs").unwrap(); 150 | scan!(out.stdout_output(); 151 | ("Ok") => () 152 | ) 153 | .unwrap() 154 | } 155 | 156 | #[test] 157 | fn test_script_including_relative() { 158 | let out = rust_script!("tests/data/script-including-relative.rs").unwrap(); 159 | scan!(out.stdout_output(); 160 | ("hello, including script") => () 161 | ) 162 | .unwrap() 163 | } 164 | 165 | #[cfg_attr(not(feature = "online_tests"), ignore)] 166 | #[test] 167 | fn script_with_same_name_as_dependency() { 168 | let out = rust_script!("tests/data/time.rs").unwrap(); 169 | scan!(out.stdout_output(); 170 | ("Hello") => () 171 | ) 172 | .unwrap() 173 | } 174 | 175 | #[test] 176 | fn script_without_main_question_mark() { 177 | let out = rust_script!("tests/data/question-mark").unwrap(); 178 | assert!(out 179 | .stderr 180 | .starts_with("Error: Os { code: 2, kind: NotFound, message:")); 181 | } 182 | 183 | #[cfg_attr(not(feature = "online_tests"), ignore)] 184 | #[test] 185 | fn test_script_async_main() { 186 | let out = rust_script!("tests/data/script-async-main.rs").unwrap(); 187 | scan!(out.stdout_output(); 188 | ("Some(1)") => () 189 | ) 190 | .unwrap() 191 | } 192 | 193 | #[cfg_attr(not(feature = "online_tests"), ignore)] 194 | #[test] 195 | fn test_pub_fn_main() { 196 | let out = rust_script!("tests/data/pub-fn-main.rs").unwrap(); 197 | scan!(out.stdout_output(); 198 | ("Some(1)") => () 199 | ) 200 | .unwrap() 201 | } 202 | 203 | #[test] 204 | fn test_cargo_target_dir_env() { 205 | let out = rust_script!("tests/data/cargo-target-dir-env.rs").unwrap(); 206 | scan!(out.stdout_output(); 207 | ("true") => () 208 | ) 209 | .unwrap() 210 | } 211 | 212 | #[cfg_attr(not(feature = "online_tests"), ignore)] 213 | #[test] 214 | fn test_outer_line_doc() { 215 | let out = rust_script!("tests/data/outer-line-doc.rs").unwrap(); 216 | scan!(out.stdout_output(); 217 | ("Some(1)") => () 218 | ) 219 | .unwrap() 220 | } 221 | 222 | #[test] 223 | fn test_whitespace_before_main() { 224 | let out = rust_script!("tests/data/whitespace-before-main.rs").unwrap(); 225 | scan!(out.stdout_output(); 226 | ("hello, world") => () 227 | ) 228 | .unwrap() 229 | } 230 | 231 | #[test] 232 | fn test_force_rebuild() { 233 | for option in ["-f", "--force"] { 234 | std::env::remove_var("_RUST_SCRIPT_TEST_MESSAGE"); 235 | 236 | let script_path = "tests/data/script-using-env.rs"; 237 | let out = rust_script!(option, script_path).unwrap(); 238 | scan!(out.stdout_output(); 239 | ("msg = undefined") => () 240 | ) 241 | .unwrap(); 242 | 243 | std::env::set_var("_RUST_SCRIPT_TEST_MESSAGE", "hello"); 244 | 245 | let out = rust_script!(script_path).unwrap(); 246 | scan!(out.stdout_output(); 247 | ("msg = undefined") => () 248 | ) 249 | .unwrap(); 250 | 251 | let out = rust_script!(option, script_path).unwrap(); 252 | scan!(out.stdout_output(); 253 | ("msg = hello") => () 254 | ) 255 | .unwrap(); 256 | } 257 | } 258 | 259 | #[test] 260 | #[ignore] 261 | fn test_stable_toolchain() { 262 | let out = rust_script!( 263 | "--toolchain", 264 | "stable", 265 | "tests/data/script-unstable-feature.rs" 266 | ) 267 | .unwrap(); 268 | assert!(out.stderr.contains("`#![feature]` may not be used")); 269 | assert!(!out.success()); 270 | } 271 | 272 | #[test] 273 | #[ignore] 274 | fn test_nightly_toolchain() { 275 | let out = rust_script!( 276 | "--toolchain", 277 | "nightly", 278 | "tests/data/script-unstable-feature.rs" 279 | ) 280 | .unwrap(); 281 | scan!(out.stdout_output(); 282 | ("`#![feature]` *may* be used!") => () 283 | ) 284 | .unwrap(); 285 | assert!(out.success()); 286 | } 287 | 288 | #[test] 289 | fn test_same_flags() { 290 | let out = rust_script!("tests/data/same-flags.rs", "--help").unwrap(); 291 | scan!(out.stdout_output(); 292 | ("Argument: --help") => () 293 | ) 294 | .unwrap() 295 | } 296 | 297 | #[cfg(unix)] 298 | #[cfg_attr(not(feature = "online_tests"), ignore)] 299 | #[test] 300 | fn test_extern_c_main() { 301 | let out = rust_script!("tests/data/extern-c-main.rs").unwrap(); 302 | scan!(out.stdout_output(); 303 | ("hello, world") => () 304 | ) 305 | .unwrap() 306 | } 307 | 308 | #[test] 309 | #[cfg_attr(not(feature = "online_tests"), ignore)] 310 | fn test_script_multiple_deps() { 311 | let out = rust_script!( 312 | "-d", 313 | "serde_json=1.0.96", 314 | "-d", 315 | "anyhow=1.0.71", 316 | "tests/data/script-using-anyhow-and-serde.rs" 317 | ) 318 | .unwrap(); 319 | scan!(out.stdout_output(); 320 | ("Ok") => () 321 | ) 322 | .unwrap() 323 | } 324 | -------------------------------------------------------------------------------- /src/arguments.rs: -------------------------------------------------------------------------------- 1 | use clap::ArgAction; 2 | 3 | use crate::build_kind::BuildKind; 4 | 5 | #[derive(Debug)] 6 | pub struct Args { 7 | pub script: Option, 8 | pub script_args: Vec, 9 | pub expr: bool, 10 | pub loop_: bool, 11 | pub count: bool, 12 | pub base_path: Option, 13 | pub pkg_path: Option, 14 | pub gen_pkg_only: bool, 15 | pub cargo_output: bool, 16 | pub clear_cache: bool, 17 | pub debug: bool, 18 | pub dep: Vec, 19 | pub extern_: Vec, 20 | pub force: bool, 21 | pub unstable_features: Vec, 22 | pub build_kind: BuildKind, 23 | pub toolchain_version: Option, 24 | #[cfg(windows)] 25 | pub install_file_association: bool, 26 | #[cfg(windows)] 27 | pub uninstall_file_association: bool, 28 | pub wrapper: Option, 29 | } 30 | 31 | impl Args { 32 | pub fn parse() -> Self { 33 | use clap::{Arg, ArgGroup, Command}; 34 | let version = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"); 35 | let about = r#"Compiles and runs a Rust script"#; 36 | 37 | let app = Command::new(crate::consts::PROGRAM_NAME) 38 | .version(version) 39 | .about(about) 40 | .arg(Arg::new("script") 41 | .index(1) 42 | .help("Script file or expression to execute") 43 | .required_unless_present_any(if cfg!(windows) { 44 | ["clear-cache", "install-file-association", "uninstall-file-association"].iter() 45 | } else { 46 | ["clear-cache"].iter() 47 | }) 48 | .conflicts_with_all(if cfg!(windows) { 49 | ["install-file-association", "uninstall-file-association"].iter() 50 | } else { 51 | [].iter() 52 | }) 53 | .num_args(1..) 54 | .trailing_var_arg(true) 55 | ) 56 | .arg(Arg::new("expr") 57 | .help("Execute