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