Skip to content

Commit

Permalink
cmd* macros: Add ability to specify argument collection with `@<expre…
Browse files Browse the repository at this point in the history
…ssion>`
  • Loading branch information
N3xed committed Sep 1, 2021
1 parent 0ccb600 commit f3bd5c5
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 42 deletions.
6 changes: 3 additions & 3 deletions src/cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ impl Crate {
debug!("Generating new Cargo crate in path {}", self.0.display());

cmd!(
"cargo", if init { "init" } else {"new"};
args=(options),
arg=(&self.0)
"cargo", if init { "init" } else {"new"},
@options,
&self.0
)?;
Ok(())
}
Expand Down
102 changes: 63 additions & 39 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,56 +27,74 @@ macro_rules! path_buf {

/// Spawn a command and return its handle.
///
/// This is a simple wrapper over the [`std::process::Command`] API.
/// It expects at least one argument for the program to run. Every comma seperated
/// argument thereafter is added to the command's arguments. The opional `key=value`
/// arguments after a semicolon are simply translated to calling the
/// This is a simple wrapper over the [`std::process::Command`] API. It expects at least
/// one argument for the program to run. Every comma seperated argument thereafter is
/// added to the command's arguments. Arguments after an `@`-sign specify collections of
/// arguments (specifically `impl IntoIterator<Item = impl AsRef<OsStr>`). The opional
/// `key=value` arguments after a semicolon are simply translated to calling the
/// `std::process::Command::<key>` method with `value` as its arguments.
///
/// **Note:**
/// `@`-arguments must be followed by at least one normal argument. For example
/// `cmd!("cmd", @args)` will not compile but `cmd!("cmd", @args, "other")` will. You can
/// use `key=value` arguments to work around this limitation: `cmd!("cmd"; args=(args))`.
///
/// After building the command [`std::process::Command::spawn`] is called and its return
/// value returned.
///
/// # Examples
/// ```ignore
/// cmd_spawn("git", "clone"; arg=("url.com"), env=("var", "value"));
/// let args_list = ["--foo", "--bar", "value"];
/// cmd_spawn("git", @args_list, "clone"; arg=("url.com"), env=("var", "value"));
/// ```
#[macro_export]
macro_rules! cmd_spawn {
($cmd:expr $(,$cmdarg:expr)*; $($k:ident = $v:tt),*) => {{
($cmd:expr $(, $(@$cmdargs:expr,)* $cmdarg:expr)* $(; $($k:ident = $v:tt),*)?) => {{
let cmd = &($cmd);
let mut builder = std::process::Command::new(cmd);
$(builder.arg($cmdarg);)*
$(
$(builder.args($cmdargs);)*
builder.arg($cmdarg);
)*
$(builder. $k $v;)*

builder.spawn()
}};
($cmd:expr $(,$cmdarg:expr)*) => {
cmd_spawn!($cmd, $($cmdarg),*;)
};
}

/// Run a command to completion.
///
/// This is a simple wrapper over the [`std::process::Command`] API.
/// It expects at least one argument for the program to run. Every comma seperated
/// argument thereafter is added to the command's arguments. The opional `key=value`
/// arguments after a semicolon are simply translated to calling the
/// This is a simple wrapper over the [`std::process::Command`] API. It expects at least
/// one argument for the program to run. Every comma seperated argument thereafter is
/// added to the command's arguments. Arguments after an `@`-sign specify collections of
/// arguments (specifically `impl IntoIterator<Item = impl AsRef<OsStr>`). The opional
/// `key=value` arguments after a semicolon are simply translated to calling the
/// `std::process::Command::<key>` method with `value` as its arguments.
///
/// **Note:**
/// `@`-arguments must be followed by at least one normal argument. For example
/// `cmd!("cmd", @args)` will not compile but `cmd!("cmd", @args, "other")` will. You can
/// use `key=value` arguments to work around this limitation: `cmd!("cmd"; args=(args))`.
///
/// After building the command [`std::process::Command::status`] is called and its return
/// value returned if the command was executed sucessfully otherwise an error is returned.
///
/// # Examples
/// ```ignore
/// cmd("git", "clone"; arg=("url.com"), env=("var", "value"));
/// let args_list = ["--foo", "--bar", "value"];
/// cmd("git", @args_list, "clone"; arg=("url.com"), env=("var", "value"));
/// ```
#[macro_export]
macro_rules! cmd {
($cmd:expr $(,$cmdarg:expr)*; $($k:ident = $v:tt),*) => {{
($cmd:expr $(, $(@$cmdargs:expr,)* $cmdarg:expr)* $(; $($k:ident = $v:tt),*)?) => {{
let cmd = &($cmd);
let mut builder = std::process::Command::new(cmd);
$(builder.arg($cmdarg);)*
$(builder. $k $v;)*
$(
$(builder.args($cmdargs);)*
builder.arg($cmdarg);
)*

$($(builder. $k $v;)*)?

match builder.status() {
Err(err) => Err(err.into()),
Expand All @@ -90,59 +108,68 @@ macro_rules! cmd {
}
}
}};
($cmd:expr $(,$cmdarg:expr)*) => {
cmd!($cmd, $($cmdarg),*;)
};
}

/// Run a command to completion and gets its `stdout` output.
///
/// This is a simple wrapper over the [`std::process::Command`] API.
/// It expects at least one argument for the program to run. Every comma seperated
/// argument thereafter is added to the command's arguments. The opional `key=value`
/// arguments after a semicolon are simply translated to calling the
/// This is a simple wrapper over the [`std::process::Command`] API. It expects at least
/// one argument for the program to run. Every comma seperated argument thereafter is
/// added to the command's arguments. Arguments after an `@`-sign specify collections of
/// arguments (specifically `impl IntoIterator<Item = impl AsRef<OsStr>`). The opional
/// `key=value` arguments after a semicolon are simply translated to calling the
/// `std::process::Command::<key>` method with `value` as its arguments.
///
/// **Note:**
/// `@`-arguments must be followed by at least one normal argument. For example
/// `cmd!("cmd", @args)` will not compile but `cmd!("cmd", @args, "other")` will. You can
/// use `key=value` arguments to work around this limitation: `cmd!("cmd"; args=(args))`.
///
/// After building the command [`std::process::Command::output`] is called. If the command
/// succeeded its `stdout` output is returned as a [`String`] otherwise an error is
/// returned. If `ignore_exitcode` is specified as the first `key=value` argument, the
/// returned. If `ignore_exitcode` is specified as the last argument, the
/// command's output will be returned even if it ran unsuccessfully.
///
/// # Examples
/// ```ignore
/// cmd("git", "clone"; arg=("url.com"), env=("var", "value"));
/// let args_list = ["--foo", "--bar", "value"];
/// cmd_output("git", @args_list, "clone"; arg=("url.com"), env=("var", "value"));
/// ```
#[macro_export]
macro_rules! cmd_output {
($cmd:expr $(,$cmdarg:expr)*; ignore_exitcode $(,$k:ident = $v:tt)* ) => {{
($cmd:expr $(, $(@$cmdargs:expr,)* $cmdarg:expr)*; ignore_exitcode $(,$k:ident = $v:tt)*) => {{
let cmd = &($cmd);
let mut builder = std::process::Command::new(cmd);
$(builder.arg($cmdarg);)*
$(
$(builder.args($cmdargs);)*
builder.arg($cmdarg);
)*
$(builder. $k $v;)*

let result = builder.output()?;
if log::log_enabled!(log::Level::Debug) {
use std::io::Write;
std::io::stdout().write_all(&result.stdout[..]).ok();
std::io::stderr().write_all(&result.stderr[..]).ok();
}

String::from_utf8_lossy(&result.stdout[..]).trim_end_matches(&['\n', '\r'][..]).to_string()
}};
($cmd:expr $(,$cmdarg:expr)*; $($k:ident = $v:tt),*) => {{
($cmd:expr $(, $(@$cmdargs:expr,)* $cmdarg:expr)* $(; $($k:ident = $v:tt),*)?) => {{
let cmd = &($cmd);
let mut builder = std::process::Command::new(cmd);
$(builder.arg($cmdarg);)*
$(builder. $k $v;)*
$(
$(builder.args($cmdargs);)*
builder.arg($cmdarg);
)*
$($(builder. $k $v;)*)?

match builder.output() {
Err(err) => Err(err.into()),
Ok(result) => {
if !result.status.success() {
use std::io::Write;
if log::log_enabled!(log::Level::Error) {
std::io::stdout().write_all(&result.stdout[..]).ok();
std::io::stderr().write_all(&result.stderr[..]).ok();
}
std::io::stdout().write_all(&result.stdout[..]).ok();
std::io::stderr().write_all(&result.stderr[..]).ok();

Err(anyhow::anyhow!("Command '{:?}' failed with exit code {:?}.", &builder, result.status.code()))
}
Expand All @@ -152,9 +179,6 @@ macro_rules! cmd_output {
}
}
}};
($cmd:expr $(,$cmdarg:expr)*) => {
cmd_output!($cmd, $($cmdarg),*;)
};
}

pub trait PathExt: AsRef<Path> {
Expand Down

0 comments on commit f3bd5c5

Please sign in to comment.