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
//! # 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'
//! ```
#[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_owned($cwd.try_clone().expect("cloning dir"));
$crate::internals::execute(cmd)
}
};
($cwd: expr, $s:expr, $( $id:ident ),*) => { $crate::bash_in!($cwd, $s, $($id = $id),*) };
}