Commit a42c8b2c authored by Wohlgemuth, Jason's avatar Wohlgemuth, Jason
Browse files

feat: Add cmd macro

parent 37abfe4f
Loading
Loading
Loading
Loading
Loading
+6 −6
Original line number Diff line number Diff line
@@ -3,11 +3,12 @@
//! This is where we keep functions and interfaces necessary to execute ACORN's automated editorial style guide as well as content readability analyzer.
//!
use crate::analyzer::vale::{ValeOutput, ValeOutputItem};
use crate::cmd;
use crate::io::{command_exists, download_binary, extract_zip, file_checksum, make_executable, standard_project_folder};
#[cfg(feature = "std")]
use crate::io::{get, InputOutput};
use crate::prelude;
use crate::prelude::{create_dir_all, remove_file, Command, Error, File, HashMap, PathBuf, Write};
use crate::prelude::{create_dir_all, remove_file, Command, CommandOutput, Error, File, HashMap, PathBuf, Write};
use crate::schema::research_activity::ResearchActivity;
use crate::schema::{ImageObject, MediaObject, Organization, ProgrammingLanguage, VideoObject, Website};
use crate::skip;
@@ -538,8 +539,7 @@ impl StaticAnalyzer<ValeConfig> for Vale {
                };
                match result {
                    | Ok(output) if output.status.success() => {
                        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
                        let parsed = ValeOutput::parse(&stdout, path.clone());
                        let parsed = ValeOutput::parse(&output.stdout(), path.clone());
                        if parsed.is_empty() {
                            Check::init().category(CheckCategory::Prose).success(true).message(id).build()
                        } else {
@@ -554,7 +554,7 @@ impl StaticAnalyzer<ValeConfig> for Vale {
                        }
                    }
                    | Ok(output) => {
                        let why = String::from_utf8_lossy(&output.stderr).trim().to_string();
                        let why = output.stderr();
                        let message = if why.is_empty() {
                            format!("process exited with status {}", output.status)
                        } else {
@@ -775,8 +775,8 @@ impl StaticAnalyzer<ValeConfig> for Vale {
            let path = which(name.clone()).unwrap().to_path_buf();
            self.binary = Some(path.clone());
            let offset = "vale version ".len();
            let output = Command::new(name.clone()).arg("--version").output().unwrap();
            let stdout = String::from_utf8(output.stdout).unwrap();
            let output = cmd!(name.clone(), ["--version"]).unwrap();
            let stdout = output.stdout();
            let version = stdout[offset..].trim().to_string();
            self.version = Some(SemanticVersion::from(version.as_ref()));
            debug!(
+15 −36
Original line number Diff line number Diff line
@@ -19,8 +19,10 @@
//! write_file(PathBuf::from("/path/to/that/file"), contents);
//! ```
//!
use crate::cmd;
use crate::fail;
use crate::prelude::{canonicalize, create_dir_all, io, var, BufReader, Command, Cursor, Error, File, PathBuf, Read, Write};
use crate::prelude::CommandOutput;
use crate::prelude::{canonicalize, create_dir_all, io, var, BufReader, Cursor, Error, File, PathBuf, Read, Write};
#[cfg(any(unix, target_os = "wasi", target_os = "redox"))]
use crate::prelude::{set_permissions, Permissions, PermissionsExt};
use crate::util::constants::{APPLICATION, LARGE_FILE_THRESHOLD_BYTES, ORGANIZATION, QUALIFIER};
@@ -161,14 +163,8 @@ impl FromCommand for SemanticVersion {
    {
        let command = name.into();
        if command_exists(command.clone()) {
            match Command::new(&command).arg("--version").output() {
                | Ok(output) if output.status.success() => {
                    let value = String::from_utf8_lossy(&output.stdout);
                    match value.lines().next() {
                        | Some(line) => Some(SemanticVersion::from(line)),
                        | None => None,
                    }
                }
            match cmd!(&command, ["--version"]) {
                | Ok(output) if output.status.success() => output.stdout().lines().next().map(SemanticVersion::from),
                | Ok(_) | Err(_) => None,
            }
        } else {
@@ -523,11 +519,10 @@ pub fn files_from_git_branch(value: &str, extensions: Option<Vec<&str>>) -> Vec<
            | None => "main".to_string(),
        };
        let args = vec!["diff", "--name-only", &default_branch, "--merge-base", value];
        let result = Command::new("git").args(args).output();
        match result {
            | Ok(output) if output.status.success() => filter_git_command_result(String::from_utf8_lossy(&output.stdout).to_string(), extensions),
        match cmd!("git", args) {
            | Ok(output) if output.status.success() => filter_git_command_result(output.stdout(), extensions),
            | Ok(output) => {
                let why = String::from_utf8_lossy(&output.stderr).trim().to_string();
                let why = output.stderr();
                let message = if why.is_empty() {
                    format!("process exited with status {}", output.status)
                } else {
@@ -554,12 +549,12 @@ pub fn files_from_git_branch(value: &str, extensions: Option<Vec<&str>>) -> Vec<
pub fn files_from_git_commit(value: &str, extensions: Option<Vec<&str>>) -> Vec<PathBuf> {
    if command_exists("git".to_owned()) {
        let args = vec!["diff-tree", "--no-commit-id", "--name-only", "-r", value];
        let result = Command::new("git").args(args).output();
        let result = cmd!("git", args);
        debug!("=> {} Git command response - {result:?}", Label::using());
        let files = match result {
            | Ok(output) if output.status.success() => filter_git_command_result(String::from_utf8_lossy(&output.stdout).to_string(), extensions),
            | Ok(output) if output.status.success() => filter_git_command_result(output.stdout(), extensions),
            | Ok(output) => {
                let why = String::from_utf8_lossy(&output.stderr).trim().to_string();
                let why = output.stderr();
                let message = if why.is_empty() {
                    format!("process exited with status {}", output.status)
                } else {
@@ -682,16 +677,8 @@ pub fn folder_size<P: Into<PathBuf>>(path: P) -> u64 {
pub fn git_branch_name() -> Option<String> {
    if command_exists("git".to_owned()) {
        let args = vec!["symbolic-ref", "--short", "HEAD"];
        let result = Command::new("git").args(args).output();
        match result {
            | Ok(output) if output.status.success() => {
                let value = String::from_utf8_lossy(&output.stdout).to_string();
                let name = match value.split("/").last() {
                    | Some(x) => Some(x.to_string()),
                    | None => None,
                };
                name
            }
        match cmd!("git", args) {
            | Ok(output) if output.status.success() => output.stdout().split("/").last().map(|x| x.to_string()),
            | Ok(_) => None,
            | Err(_) => None,
        }
@@ -707,16 +694,8 @@ pub fn git_branch_name() -> Option<String> {
pub fn git_default_branch_name() -> Option<String> {
    if command_exists("git".to_owned()) {
        let args = vec!["symbolic-ref", "refs/remotes/origin/HEAD", "--short"];
        let result = Command::new("git").args(args).output();
        match result {
            | Ok(output) if output.status.success() => {
                let value = String::from_utf8_lossy(&output.stdout).to_string();
                let name = match value.split("/").last() {
                    | Some(x) => Some(x.to_string()),
                    | None => None,
                };
                name
            }
        match cmd!("git", args) {
            | Ok(output) if output.status.success() => output.stdout().split("/").last().map(|x| x.to_string()),
            | Ok(_) => None,
            | Err(_) => None,
        }
+37 −1
Original line number Diff line number Diff line
@@ -13,7 +13,7 @@ mod std {
    #[cfg(unix)]
    pub use std::os::unix::fs::PermissionsExt;
    pub use std::path::{absolute, Path, PathBuf};
    pub use std::process::{exit, Command};
    pub use std::process::{exit, Command, Output};
}
/// Module that provides `no-std` support
#[allow(unused_imports)]
@@ -25,6 +25,42 @@ mod no_std {
pub use no_std::*;
#[cfg(feature = "std")]
pub use std::*;

/// Extension trait for extracting strings from [`std::process::Output`]
///
/// Eliminates repeated `String::from_utf8_lossy(&output.stdout).trim().to_string()` patterns.
///
/// # Examples
///
/// ```ignore
/// use acorn::cmd;
/// use acorn::prelude::CommandOutput;
///
/// match cmd!("git", ["branch", "--show-current"]) {
///     Ok(output) if output.status.success() => {
///         let branch = output.stdout();
///         println!("{branch}");
///     }
///     Ok(output) => eprintln!("{}", output.stderr()),
///     Err(why) => eprintln!("{why}"),
/// }
/// ```
#[cfg(feature = "std")]
pub trait CommandOutput {
    /// Extract stdout as a trimmed, lossy UTF-8 string
    fn stdout(&self) -> String;
    /// Extract stderr as a trimmed, lossy UTF-8 string
    fn stderr(&self) -> String;
}
#[cfg(feature = "std")]
impl CommandOutput for Output {
    fn stdout(&self) -> String {
        String::from_utf8_lossy(&self.stdout).trim().to_string()
    }
    fn stderr(&self) -> String {
        String::from_utf8_lossy(&self.stderr).trim().to_string()
    }
}
/// Get Vale release filename for a given platform operating system (e.g., linux, windows, macos)
#[cfg(feature = "std")]
pub fn vale_release_filename() -> String {
+119 −0
Original line number Diff line number Diff line
@@ -130,3 +130,122 @@ macro_rules! param {
            .with_key($name)
    };
}
/// Execute a command and capture its output.
///
/// Simplifies `Command::new(binary).args(args).output()` patterns.
/// Thread safe — `Command` is `Send + Sync`.
///
/// Use the `status` prefix to call `.status()` instead of `.output()`.
/// Use the `sh` prefix to parse a single string as a shell command (splits on whitespace).
///
/// # Syntax
///
/// ```ignore
/// // Parse a single string (supports format! interpolation)
/// cmd!(sh "git diff-tree --no-commit-id --name-only")
/// cmd!(sh format!("git diff --name-only {}", branch))
///
/// // CLI-style with string literals (returns Result<Output, io::Error>)
/// cmd!("git" "diff-tree" "--no-commit-id" "--name-only")
///
/// // Capture output with array or variable
/// cmd!("git", ["branch", "--show-current"])
/// cmd!("git", args)
///
/// // Check exit status only (returns Result<ExitStatus, io::Error>)
/// cmd!(sh status "vale --config .vale.ini sync")
/// cmd!(status "vale" "--config" ".vale.ini" "sync")
/// cmd!(status "vale", ["--config", path, "sync"])
/// cmd!(status "vale", args)
/// ```
///
/// # Examples
///
/// ```ignore
/// use acorn::cmd;
/// use acorn::prelude::CommandOutput;
///
/// // Parse a full command string — great for interpolated values
/// let branch = "main";
/// match cmd!(sh format!("git diff --name-only {branch}")) {
///     Ok(output) if output.status.success() => {
///         println!("{}", output.stdout());
///     }
///     _ => {},
/// }
///
/// // CLI-style — space-separated string literals
/// match cmd!("git" "branch" "--show-current") {
///     Ok(output) if output.status.success() => {
///         println!("{}", output.stdout());
///     }
///     Ok(output) => eprintln!("{}", output.stderr()),
///     Err(why) => eprintln!("Error: {}", why),
/// }
///
/// // Array-style — for dynamic or mixed-type args
/// let args = vec!["diff", "--name-only", "main"];
/// match cmd!("git", args) {
///     Ok(output) if output.status.success() => {
///         println!("{}", output.stdout());
///     }
///     _ => {},
/// }
///
/// // Status-only check
/// match cmd!(sh status "vale --config .vale.ini sync") {
///     Ok(status) if status.success() => println!("synced"),
///     _ => eprintln!("sync failed"),
/// }
/// ```
#[macro_export]
macro_rules! cmd {
    // Parse string and capture output: cmd!(sh "git diff --name-only")
    (sh $cmd:expr) => {{
        let cmd_str = $cmd.to_string();
        let mut parts = cmd_str.split_whitespace();
        match parts.next() {
            | Some(binary) => {
                let args: Vec<&str> = parts.collect();
                $crate::prelude::Command::new(binary).args(args).output()
            }
            | None => Err($crate::prelude::io::Error::other("empty command string")),
        }
    }};
    // Parse string and check status: cmd!(sh status "vale --config .vale.ini sync")
    (sh status $cmd:expr) => {{
        let cmd_str = $cmd.to_string();
        let mut parts = cmd_str.split_whitespace();
        match parts.next() {
            | Some(binary) => {
                let args: Vec<&str> = parts.collect();
                $crate::prelude::Command::new(binary).args(args).status()
            }
            | None => Err($crate::prelude::io::Error::other("empty command string")),
        }
    }};
    // Status with array literal: cmd!(status binary, [arg1, arg2])
    (status $binary:expr, [ $($arg:expr),* $(,)? ]) => {{
        $crate::prelude::Command::new($binary).args([$($arg),*]).status()
    }};
    // Status with args variable: cmd!(status binary, args)
    (status $binary:expr, $args:expr) => {{
        $crate::prelude::Command::new($binary).args($args).status()
    }};
    // Status CLI-style: cmd!(status "vale" "--config" ".vale.ini" "sync")
    (status $binary:literal $($arg:literal)*) => {{
        $crate::prelude::Command::new($binary).args([$($arg),*]).status()
    }};
    // Output with array literal: cmd!(binary, [arg1, arg2])
    ($binary:expr, [ $($arg:expr),* $(,)? ]) => {{
        $crate::prelude::Command::new($binary).args([$($arg),*]).output()
    }};
    // Output with args variable: cmd!(binary, args)
    ($binary:expr, $args:expr) => {{
        $crate::prelude::Command::new($binary).args($args).output()
    }};
    // CLI-style: cmd!("git" "diff-tree" "--no-commit-id" "--name-only")
    ($binary:literal $($arg:literal)*) => {{
        $crate::prelude::Command::new($binary).args([$($arg),*]).output()
    }};
}
+67 −1
Original line number Diff line number Diff line
use crate::cmd;
use crate::io::api::ParamStyle;
use crate::io::{
    command_exists, extract_zip, git_branch_name, git_default_branch_name, parent, read_file, to_absolute_string, FromCommand, FromPath,
};
use crate::param;
use crate::prelude::{Path, PathBuf};
use crate::prelude::{CommandOutput, Path, PathBuf};
use crate::util::*;
#[cfg(test)]
use similar_asserts::assert_eq;
@@ -389,3 +390,68 @@ fn test_param_with_quoted_value() {
    assert_eq!(param.name, "ror-id");
    assert_eq!(param.values, vec![vec![Some("\"https://ror.org/01qz5mb56\"".to_string())]]);
}
#[test]
fn test_cmd_output() {
    // Array literal
    let output = cmd!("echo", ["hello"]).expect("array literal");
    assert!(output.status.success());
    assert_eq!(output.stdout(), "hello");
    // Args variable
    let args = vec!["world"];
    let output = cmd!("echo", args).expect("args variable");
    assert!(output.status.success());
    assert_eq!(output.stdout(), "world");
    // CLI-style literals
    let output = cmd!("echo" "cli-style").expect("cli-style");
    assert!(output.status.success());
    assert_eq!(output.stdout(), "cli-style");
    // Shell string
    let output = cmd!(sh "echo sh-output").expect("sh string");
    assert!(output.status.success());
    assert_eq!(output.stdout(), "sh-output");
    // Shell string with multiple args
    let output = cmd!(sh "echo one two three").expect("sh multi-arg");
    assert!(output.status.success());
    assert_eq!(output.stdout(), "one two three");
    // Shell string with format! interpolation
    let name = "world";
    let output = cmd!(sh format!("echo hello-{name}")).expect("sh format!");
    assert!(output.status.success());
    assert_eq!(output.stdout(), "hello-world");
    // Stderr capture
    let output = cmd!("sh", ["-c", "echo oops >&2"]).expect("stderr");
    assert_eq!(output.stderr(), "oops");
    // Non-zero exit code
    let output = cmd!("sh", ["-c", "exit 42"]).expect("exit code");
    assert!(!output.status.success());
}
#[test]
fn test_cmd_status() {
    // Array literal
    let status = cmd!(status "echo", ["hello"]).expect("array literal");
    assert!(status.success());
    // Args variable
    let args = vec!["hello"];
    let status = cmd!(status "echo", args).expect("args variable");
    assert!(status.success());
    // CLI-style literals
    let status = cmd!(status "echo" "status-check").expect("cli-style");
    assert!(status.success());
    // Shell string
    let status = cmd!(sh status "echo sh-status").expect("sh string");
    assert!(status.success());
    // Shell string with format! interpolation
    let arg = "check";
    let status = cmd!(sh status format!("echo {arg}")).expect("sh format!");
    assert!(status.success());
    // Non-zero exit code
    let status = cmd!(status "sh", ["-c", "exit 42"]).expect("exit code");
    assert!(!status.success());
}
#[test]
fn test_cmd_error() {
    // Nonexistent binary — array style
    assert!(cmd!("not-a-real-command-xyz", ["arg"]).is_err());
    // Nonexistent binary — sh style
    assert!(cmd!(sh "not-a-real-command-xyz arg").is_err());
}