Loading acorn-lib/src/analyzer/mod.rs +6 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 { Loading @@ -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 { Loading Loading @@ -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!( Loading acorn-lib/src/io/mod.rs +15 −36 Original line number Diff line number Diff line Loading @@ -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}; Loading Loading @@ -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 { Loading Loading @@ -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 { Loading @@ -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 { Loading Loading @@ -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, } Loading @@ -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, } Loading acorn-lib/src/prelude.rs +37 −1 Original line number Diff line number Diff line Loading @@ -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)] Loading @@ -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 { Loading acorn-lib/src/util/macros.rs +119 −0 Original line number Diff line number Diff line Loading @@ -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() }}; } acorn-lib/src/util/tests/mod.rs +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; Loading Loading @@ -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()); } Loading
acorn-lib/src/analyzer/mod.rs +6 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 { Loading @@ -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 { Loading Loading @@ -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!( Loading
acorn-lib/src/io/mod.rs +15 −36 Original line number Diff line number Diff line Loading @@ -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}; Loading Loading @@ -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 { Loading Loading @@ -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 { Loading @@ -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 { Loading Loading @@ -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, } Loading @@ -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, } Loading
acorn-lib/src/prelude.rs +37 −1 Original line number Diff line number Diff line Loading @@ -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)] Loading @@ -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 { Loading
acorn-lib/src/util/macros.rs +119 −0 Original line number Diff line number Diff line Loading @@ -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() }}; }
acorn-lib/src/util/tests/mod.rs +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; Loading Loading @@ -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()); }