sh_inline/lib.rs
1//! # Macros to run bash scripts inline in Rust
2//!
3//! In many cases it's convenient to run child processes,
4//! particularly via Unix shell script. Writing the
5//! Rust code to use `std::process::Command` directly
6//! will get very verbose quickly. You can generate
7//! a script "manually" by using e.g. `format!()` but
8//! there are some important yet subtle things to get right,
9//! such as dealing with quoting issues.
10//!
11//! This macro takes Rust variable names at the start
12//! that are converted to a string (quoting as necessary)
13//! and bound into the script as bash variables.
14//!
15//! Further, the generated scripts use "bash strict mode"
16//! by default, i.e. `set -euo pipefail`.
17//!
18//! ```
19//! use sh_inline::*;
20//! let foo = "variable with spaces";
21//! bash!(r#"test "${foo}" = 'variable with spaces'"#, foo)?;
22//! # Ok::<(), Box<dyn std::error::Error>>(())
23//! ```
24//!
25//! This generates and executes bash script as follows:
26//! ```sh
27//! set -euo pipefail
28//! foo="variable with spaces"
29//! test ${foo} = 'variable with spaces'
30//! ```
31//!
32//! # Related crates
33//!
34//! [`xshell`] is a crate that does not depend on bash, and also supports
35//! inline formatting.
36//!
37//! [`xshell`]: https://docs.rs/xshell/latest/xshell/
38
39#[doc(hidden)]
40pub mod internals;
41
42#[cfg(feature = "cap-std-ext")]
43pub use cap_std_ext;
44#[cfg(feature = "cap-std-ext")]
45pub use cap_std_ext::cap_std;
46
47/// Create a [`Command`] object that will execute a fragment of (Bash) shell script
48/// in "strict mode", i.e. with `set -euo pipefail`. The first argument is the
49/// script, and additional arguments should be Rust variable identifiers. The
50/// provided Rust variables will become shell script variables with their values
51/// quoted.
52///
53/// This macro will allocate a temporary file for the script; this can (in very
54/// unusual cases such as file descriptior exhaustion) fail.
55///
56/// ```
57/// use sh_inline::*;
58/// let a = "foo";
59/// let b = std::path::Path::new("bar");
60/// let c = 42;
61/// let d: String = "baz".into();
62/// let r = bash_command!(r#"test "${a} ${b} ${c}" = "foo bar 42""#, a, b, c).expect("creating script").status()?;
63/// assert!(r.success());
64/// let r = bash_command!(r#"test "${a}" = "2""#, a = 1 + 1).expect("creating script").status()?;
65/// assert!(r.success());
66/// # Ok::<(), Box<dyn std::error::Error>>(())
67/// ```
68///
69/// [`Command`]: https://doc.rust-lang.org/std/process/struct.Command.html
70#[macro_export]
71macro_rules! bash_command {
72 ($s:expr) => { $crate::bash_command!($s,) };
73 ($s:expr, $( $id:ident = $v:expr),*) => {
74 {
75 use std::fmt::Write;
76 let mut script: String = "set -euo pipefail\n".into();
77 $(
78 write!(&mut script, "{}={}\n", stringify!($id), $crate::internals::CommandArg::from(&$v)).unwrap();
79 )*
80 $crate::internals::render(&$s, script)
81 }
82 };
83 ($s:expr, $( $id:ident ),*) => { $crate::bash_command!($s, $($id = $id),*) };
84}
85
86/// Execute a fragment of Bash shell script, returning an error if the subprocess exits unsuccessfully.
87/// This is intended as a convenience macro for the common case of wanting to just propagate
88/// errors. The returned error type is [std::io::Error](https://doc.rust-lang.org/std/io/struct.Error.html).
89///
90/// For more details on usage, see the [`bash_command`](./macro.bash_command.html) macro.
91///
92/// ```
93/// use sh_inline::*;
94/// let a = "foo";
95/// let b = std::path::Path::new("bar");
96/// bash!(r#"test "${a} ${b} ${c}" = "foo bar 42""#, a = a, b = b, c = 42)?;
97/// # Ok::<(), Box<dyn std::error::Error>>(())
98/// ```
99#[macro_export]
100macro_rules! bash {
101 ($s:expr) => { $crate::bash!($s,) };
102 ($s:expr, $( $id:ident = $v:expr ),*) => {
103 $crate::internals::execute($crate::bash_command!($s, $( $id = $v ),*).expect("failed to create temporary script")
104 )
105 };
106 ($s:expr, $( $id:ident ),*) => {
107 $crate::internals::execute($crate::bash_command!($s, $( $id ),*).expect("failed to create temporary script")
108 )
109 };
110}
111
112/// Execute a fragment of Bash shell script with the specified working directory.
113///
114/// Otherwise this is equivalent to the [`bash`] macro.
115///
116/// ```
117/// use sh_inline::*;
118/// use std::sync::Arc;
119/// let td = Arc::new(cap_tempfile::tempdir(cap_std::ambient_authority())?);
120/// td.write("sometestfile", "test file contents")?;
121/// bash_in!(td, r#"grep -qF "test file contents" sometestfile"#)?;
122/// # Ok::<(), Box<dyn std::error::Error>>(())
123/// ```
124#[macro_export]
125#[cfg(feature = "cap-std-ext")]
126macro_rules! bash_in {
127 ($cwd:expr, $s:expr) => { $crate::bash_in!($cwd, $s,) };
128 ($cwd:expr, $s:expr, $( $id:ident = $v:expr ),*) => {
129 { use cap_std_ext::cmdext::CapStdExtCommandExt;
130 let mut cmd = $crate::bash_command!($s, $( $id = $v ),*).expect("failed to create temporary script");
131 cmd.cwd_dir($cwd.try_clone().expect("cloning dir"));
132 $crate::internals::execute(cmd)
133 }
134 };
135 ($cwd: expr, $s:expr, $( $id:ident ),*) => { $crate::bash_in!($cwd, $s, $($id = $id),*) };
136}