Skip to main content

acorn/util/
macros.rs

1//! Macros
2
3/// Execute a command and capture its output.
4///
5/// Simplifies `Command::new(binary).args(args).output()` patterns.
6/// Thread safe — `Command` is `Send + Sync`.
7///
8/// Supports several calling conventions:
9///
10/// | Prefix         | Returns                        | Description                              |
11/// |----------------|--------------------------------|------------------------------------------|
12/// | `"literal"`    | `io::Result<Output>`           | Interpolated literal, capture output     |
13/// | `status "lit"` | `io::Result<ExitStatus>`       | Interpolated literal, exit status only   |
14/// | `try "lit"`    | `Result<String, String>`       | Interpolated literal, stdout or error    |
15/// | *(none)*       | `io::Result<Output>`           | Run and capture output                   |
16/// | `status`       | `io::Result<ExitStatus>`       | Exit status only (no output)             |
17/// | `sh`           | `io::Result<Output>`           | Parse string, capture output             |
18/// | `sh status`    | `io::Result<ExitStatus>`       | Parse string, exit status only           |
19/// | `try sh`       | `Result<String, String>`       | Parse string, stdout or error            |
20/// | `bash`         | `io::Result<Output>`           | Run via `bash -c <command>`              |
21/// | `pwsh`         | `io::Result<Output>`           | Run via `pwsh -NoProfile -Command ...`   |
22/// | `try`          | `Result<String, String>`       | Run, return stdout or error message      |
23///
24/// All forms support an optional `; dir: path` suffix to set the working directory.
25///
26/// # When to use each form
27///
28/// | Use case                            | Recommended form                        |
29/// |-------------------------------------|-----------------------------------------|
30/// | Known command, no shell features    | `cmd!("git rev-parse {branch}")`        |
31/// | Known command, with interpolation   | `cmd!("echo {name}")`                   |
32/// | Simple static command               | `cmd!("git" "status")`                  |
33/// | Dynamic command string              | `cmd!(sh format!("diff {file}"))`       |
34/// | Need shell features (pipes, globs)  | `cmd!(bash "echo * \| wc -l")`          |
35/// | Need PowerShell                     | `cmd!(pwsh "Get-ChildItem \| Select-Object Name")` |
36///
37/// On the surface `cmd!("...")` and `cmd!(sh "...")` both accept a literal string, but
38/// the bare literal form is the preferred entry point — it's the simplest, avoids the
39/// unnecessary `sh` prefix, and supports the full `{var}` / `{args...}` interpolation.
40/// The `sh` prefix is useful when the command string is a *runtime expression* (e.g.
41/// `format!(...)`), since `cmd!(sh $expr)` parses it with shell-aware word splitting.
42///
43/// # Literal interpolation
44///
45/// A literal string (a bare `"..."` token without `sh`) is parsed at compile time
46/// and supports both interpolation forms:
47///
48/// - **`{name}`** — expands a single value implementing `AsRef<OsStr>` (works with
49///   `String`, `&str`, `OsString`, `PathBuf`, etc.) into one command argument
50/// - **`{name...}`** — expands an iterable (splat) into zero or more command
51///   arguments
52///
53/// Both forms must occupy a full unquoted shell word. Embedded or double-quoted
54/// interpolation like `--flag={name}` and `"{name}"` is rejected at compile time.
55/// Single quotes keep the placeholder literal:
56///
57/// ```ignore
58/// cmd!("echo '{name}'")  // prints {name}
59/// cmd!("echo '{args...}'") // prints {args...}
60/// ```
61///
62/// ## Single-value `{var}` interpolation
63///
64/// ```ignore
65/// let msg = "hello world";
66/// let output = cmd!("echo {msg}")?;
67/// assert_eq!(output.stdout(), "hello world");
68///
69/// let branch = "main".to_string();
70/// let output = cmd!("git rev-parse {branch}")?;
71/// ```
72///
73/// `{var}` accepts any type implementing [`AsRef<OsStr>`] — this includes `str`,
74/// `String`, `OsStr`, `OsString`, `Path`, and `PathBuf`. For types like integers
75/// that don't implement `AsRef<OsStr>`, convert first:
76///
77/// ```ignore
78/// let port = 8080;
79/// cmd!("curl http://localhost:{port}");              // compile error
80/// cmd!("curl http://localhost:{}", port.to_string()); // use .to_string()
81/// let p = format!("localhost:{}", port);
82/// cmd!("curl http://{p}");                           // works via String
83/// ```
84///
85/// ## Splat `{args...}` interpolation
86///
87/// Splat placeholders expand a local variable that implements borrowed
88/// iteration (arrays, `Vec`, `Option`, slices, etc.):
89///
90/// ```ignore
91/// let args = ["hello", "world"];
92/// let output = cmd!("echo {args...}")?;
93/// assert_eq!(output.stdout(), "hello world");
94///
95/// let arg1: Option<&str> = Some("hello");
96/// let arg2: Option<&str> = None;
97/// let output = cmd!("echo {arg1...} {arg2...}")?;
98/// assert_eq!(output.stdout(), "hello");
99/// ```
100///
101/// ## In `sh` literals
102///
103/// The same `{var}` and `{args...}` interpolation also works in literal `sh`
104/// command strings:
105///
106/// ```ignore
107/// cmd!(sh "echo {msg}")
108/// cmd!(sh "echo {args...}")
109/// cmd!(sh status "echo {msg}")
110/// cmd!(try sh "echo {args...}")
111/// cmd!(sh "echo {msg}"; dir: project_dir)
112/// ```
113///
114/// Runtime `sh` strings (non-literal `$cmd:expr`) are split via `shell-words`
115/// at runtime and do not support interpolation — use `format!` or build an
116/// argument list explicitly.
117///
118/// # Syntax
119///
120/// ```ignore
121/// // Interpolated literal (compile-time parsed, fastest)
122/// cmd!("git rev-parse {branch}")
123/// cmd!("echo {args...}")
124/// cmd!(status "git status")
125/// cmd!(try "git rev-parse HEAD")
126/// cmd!("echo {msg}"; dir: project_dir)
127///
128/// // String form via sh (shell-aware quoting)
129/// cmd!(sh "git diff --name-only")
130/// cmd!(sh format!("git diff --name-only {branch}"))
131/// cmd!(sh "echo 'hello world'")   // handles quotes
132/// cmd!(sh "echo {args...}")       // literal-only interpolation
133/// cmd!(sh status "echo {args...}")
134/// cmd!(try sh "echo {args...}")
135/// cmd!(bash "echo 'hello world'")
136/// cmd!(pwsh "Write-Output 'hello world'")
137///
138/// // CLI-style literals
139/// cmd!("git" "diff-tree" "--no-commit-id" "--name-only")
140/// cmd!("git" "log" "--oneline"; dir: repo_path)
141///
142/// // Array literal (dynamic types)
143/// cmd!("git", ["branch", "--show-current"])
144///
145/// // Variable args
146/// cmd!("git", args)
147///
148/// // Try form — returns Result<String, String>
149/// cmd!(try "git" "rev-parse" "HEAD")
150/// cmd!(try "git", args)
151///
152/// // With working directory
153/// cmd!("git" "status"; dir: project_dir)
154/// cmd!(try sh "npm test"; dir: project_dir)
155/// cmd!(status bash "echo hello"; dir: project_dir)
156/// cmd!(try pwsh "Write-Output 'hello'"; dir: project_dir)
157/// ```
158///
159/// # Examples
160///
161/// ```ignore
162/// use acorn::cmd;
163/// use acorn::prelude::CommandOutput;
164///
165/// // Interpolated literal (no shell overhead)
166/// let branch = "main";
167/// let output = cmd!("git rev-parse {branch}")?;
168///
169/// // String form with shell-aware quoting
170/// match cmd!(sh format!("git diff --name-only {branch}")) {
171///     Ok(output) if output.status.success() => {
172///         println!("{}", output.stdout());
173///     }
174///     _ => {},
175/// }
176///
177/// // CLI-style
178/// match cmd!("git" "branch" "--show-current") {
179///     Ok(output) if output.status.success() => {
180///         println!("{}", output.stdout());
181///     }
182///     Ok(output) => eprintln!("{}", output.stderr()),
183///     Err(why) => eprintln!("Error: {}", why),
184/// }
185///
186/// // Try form — simplified error handling
187/// match cmd!(try "git" "rev-parse" "HEAD") {
188///     Ok(hash) => println!("{hash}"),
189///     Err(msg) => eprintln!("failed: {msg}"),
190/// }
191///
192/// // Explicit shell selection
193/// let output = cmd!(bash "echo bash-mode")?;
194/// let output = cmd!(pwsh "Write-Output 'pwsh-mode'")?;
195///
196/// // Splat interpolation
197/// let args = ["hello", "world"];
198/// let output = cmd!("echo {args...}")?;
199/// assert_eq!(output.stdout(), "hello world");
200///
201/// let dry_run = Some("--dry-run");
202/// let extra: Option<&str> = None;
203/// let output = cmd!(try "cargo publish {dry_run...} {extra...}")?;
204/// ```
205#[cfg(feature = "cmd")]
206#[macro_export]
207macro_rules! cmd {
208    // ── sh status ──────────────────────────────────────────────
209    // sh status literal + dir
210    (sh status $cmd:literal; dir: $dir:expr) => {{
211        let mut __parts = $crate::cmd_sh_words!($cmd).into_iter();
212        match __parts.next() {
213            | Some(__binary) => {
214                let __binary = __binary.to_string_lossy().to_string();
215                let __args = __parts.collect::<$crate::prelude::Vec<_>>();
216                $crate::util::cmd::run_status(&__binary, __args, Some($dir.as_ref()))
217            }
218            | None => Err($crate::prelude::io::Error::other("cmd!(sh ...): empty command string")),
219        }
220    }};
221    // sh status string + dir
222    (sh status $cmd:expr; dir: $dir:expr) => {{
223        match $crate::util::cmd::parse_sh(&$cmd.to_string()) {
224            Ok((binary, args)) => $crate::util::cmd::run_status(&binary, &args, Some($dir.as_ref())),
225            Err(e) => Err(e),
226        }
227    }};
228    // sh status literal (no dir)
229    (sh status $cmd:literal) => {{
230        let mut __parts = $crate::cmd_sh_words!($cmd).into_iter();
231        match __parts.next() {
232            | Some(__binary) => {
233                let __binary = __binary.to_string_lossy().to_string();
234                let __args = __parts.collect::<$crate::prelude::Vec<_>>();
235                $crate::util::cmd::run_status(&__binary, __args, None)
236            }
237            | None => Err($crate::prelude::io::Error::other("cmd!(sh ...): empty command string")),
238        }
239    }};
240    // sh status string (no dir)
241    (sh status $cmd:expr) => {{
242        match $crate::util::cmd::parse_sh(&$cmd.to_string()) {
243            Ok((binary, args)) => $crate::util::cmd::run_status(&binary, &args, None),
244            Err(e) => Err(e),
245        }
246    }};
247    // ── sh output ──────────────────────────────────────────────
248    // sh literal + dir
249    (sh $cmd:literal; dir: $dir:expr) => {{
250        let mut __parts = $crate::cmd_sh_words!($cmd).into_iter();
251        match __parts.next() {
252            | Some(__binary) => {
253                let __binary = __binary.to_string_lossy().to_string();
254                let __args = __parts.collect::<$crate::prelude::Vec<_>>();
255                $crate::util::cmd::run_output(&__binary, __args, Some($dir.as_ref()))
256            }
257            | None => Err($crate::prelude::io::Error::other("cmd!(sh ...): empty command string")),
258        }
259    }};
260    // sh string + dir
261    (sh $cmd:expr; dir: $dir:expr) => {{
262        match $crate::util::cmd::parse_sh(&$cmd.to_string()) {
263            Ok((binary, args)) => $crate::util::cmd::run_output(&binary, &args, Some($dir.as_ref())),
264            Err(e) => Err(e),
265        }
266    }};
267    // sh literal (no dir)
268    (sh $cmd:literal) => {{
269        let mut __parts = $crate::cmd_sh_words!($cmd).into_iter();
270        match __parts.next() {
271            | Some(__binary) => {
272                let __binary = __binary.to_string_lossy().to_string();
273                let __args = __parts.collect::<$crate::prelude::Vec<_>>();
274                $crate::util::cmd::run_output(&__binary, __args, None)
275            }
276            | None => Err($crate::prelude::io::Error::other("cmd!(sh ...): empty command string")),
277        }
278    }};
279    // sh string (no dir)
280    (sh $cmd:expr) => {{
281        match $crate::util::cmd::parse_sh(&$cmd.to_string()) {
282            Ok((binary, args)) => $crate::util::cmd::run_output(&binary, &args, None),
283            Err(e) => Err(e),
284        }
285    }};
286    // ── bash status ────────────────────────────────────────────
287    // status bash string + dir
288    (status bash $cmd:expr; dir: $dir:expr) => {{
289        $crate::util::cmd::run_shell_status("bash", &["-c"], &$cmd.to_string(), Some($dir.as_ref()))
290    }};
291    // status bash string (no dir)
292    (status bash $cmd:expr) => {{
293        $crate::util::cmd::run_shell_status("bash", &["-c"], &$cmd.to_string(), None)
294    }};
295    // bash status string + dir
296    (bash status $cmd:expr; dir: $dir:expr) => {{
297        $crate::util::cmd::run_shell_status("bash", &["-c"], &$cmd.to_string(), Some($dir.as_ref()))
298    }};
299    // bash status string (no dir)
300    (bash status $cmd:expr) => {{
301        $crate::util::cmd::run_shell_status("bash", &["-c"], &$cmd.to_string(), None)
302    }};
303    // ── bash output ────────────────────────────────────────────
304    // bash string + dir
305    (bash $cmd:expr; dir: $dir:expr) => {{
306        $crate::util::cmd::run_shell_output("bash", &["-c"], &$cmd.to_string(), Some($dir.as_ref()))
307    }};
308    // bash string (no dir)
309    (bash $cmd:expr) => {{
310        $crate::util::cmd::run_shell_output("bash", &["-c"], &$cmd.to_string(), None)
311    }};
312    // ── pwsh status ────────────────────────────────────────────
313    // status pwsh string + dir
314    (status pwsh $cmd:expr; dir: $dir:expr) => {{
315        $crate::util::cmd::run_shell_status("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), Some($dir.as_ref()))
316    }};
317    // status pwsh string (no dir)
318    (status pwsh $cmd:expr) => {{
319        $crate::util::cmd::run_shell_status("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), None)
320    }};
321    // pwsh status string + dir
322    (pwsh status $cmd:expr; dir: $dir:expr) => {{
323        $crate::util::cmd::run_shell_status("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), Some($dir.as_ref()))
324    }};
325    // pwsh status string (no dir)
326    (pwsh status $cmd:expr) => {{
327        $crate::util::cmd::run_shell_status("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), None)
328    }};
329    // ── pwsh output ────────────────────────────────────────────
330    // pwsh string + dir
331    (pwsh $cmd:expr; dir: $dir:expr) => {{
332        $crate::util::cmd::run_shell_output("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), Some($dir.as_ref()))
333    }};
334    // pwsh string (no dir)
335    (pwsh $cmd:expr) => {{
336        $crate::util::cmd::run_shell_output("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), None)
337    }};
338    // ── try sh ─────────────────────────────────────────────────
339    // try sh literal + dir
340    (try sh $cmd:literal; dir: $dir:expr) => {{
341        let mut __parts = $crate::cmd_sh_words!($cmd).into_iter();
342        match __parts.next() {
343            | Some(__binary) => {
344                let __binary = __binary.to_string_lossy().to_string();
345                let __args = __parts.collect::<$crate::prelude::Vec<_>>();
346                $crate::util::cmd::try_from_output(
347                    $crate::util::cmd::run_output(&__binary, __args, Some($dir.as_ref())),
348                )
349            }
350            | None => Err("cmd!(sh ...): empty command string".to_string()),
351        }
352    }};
353    // try sh string + dir
354    (try sh $cmd:expr; dir: $dir:expr) => {{
355        match $crate::util::cmd::parse_sh(&$cmd.to_string()) {
356            Ok((binary, args)) => $crate::util::cmd::try_from_output(
357                $crate::util::cmd::run_output(&binary, &args, Some($dir.as_ref())),
358            ),
359            Err(e) => Err(format!("{e}")),
360        }
361    }};
362    // try sh literal (no dir)
363    (try sh $cmd:literal) => {{
364        let mut __parts = $crate::cmd_sh_words!($cmd).into_iter();
365        match __parts.next() {
366            | Some(__binary) => {
367                let __binary = __binary.to_string_lossy().to_string();
368                let __args = __parts.collect::<$crate::prelude::Vec<_>>();
369                $crate::util::cmd::try_from_output(
370                    $crate::util::cmd::run_output(&__binary, __args, None),
371                )
372            }
373            | None => Err("cmd!(sh ...): empty command string".to_string()),
374        }
375    }};
376    // try sh string (no dir)
377    (try sh $cmd:expr) => {{
378        match $crate::util::cmd::parse_sh(&$cmd.to_string()) {
379            Ok((binary, args)) => $crate::util::cmd::try_from_output(
380                $crate::util::cmd::run_output(&binary, &args, None),
381            ),
382            Err(e) => Err(format!("{e}")),
383        }
384    }};
385    // ── try bash ───────────────────────────────────────────────
386    // try bash string + dir
387    (try bash $cmd:expr; dir: $dir:expr) => {{
388        $crate::util::cmd::try_from_output(
389            $crate::util::cmd::run_shell_output("bash", &["-c"], &$cmd.to_string(), Some($dir.as_ref())),
390        )
391    }};
392    // try bash string (no dir)
393    (try bash $cmd:expr) => {{
394        $crate::util::cmd::try_from_output(
395            $crate::util::cmd::run_shell_output("bash", &["-c"], &$cmd.to_string(), None),
396        )
397    }};
398    // ── try pwsh ───────────────────────────────────────────────
399    // try pwsh string + dir
400    (try pwsh $cmd:expr; dir: $dir:expr) => {{
401        $crate::util::cmd::try_from_output(
402            $crate::util::cmd::run_shell_output("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), Some($dir.as_ref())),
403        )
404    }};
405    // try pwsh string (no dir)
406    (try pwsh $cmd:expr) => {{
407        $crate::util::cmd::try_from_output(
408            $crate::util::cmd::run_shell_output("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), None),
409        )
410    }};
411    // ── status literal interpolation ─────────────────────────
412    // status literal + dir
413    (status $cmd:literal; dir: $dir:expr) => {{
414        let mut __parts = $crate::cmd_sh_words!($cmd).into_iter();
415        match __parts.next() {
416            | Some(__binary) => {
417                let __binary = __binary.to_string_lossy().to_string();
418                let __args = __parts.collect::<$crate::prelude::Vec<_>>();
419                $crate::util::cmd::run_status(&__binary, __args, Some($dir.as_ref()))
420            }
421            | None => Err($crate::prelude::io::Error::other("cmd!(...): empty command string")),
422        }
423    }};
424    // status literal (no dir)
425    (status $cmd:literal) => {{
426        let mut __parts = $crate::cmd_sh_words!($cmd).into_iter();
427        match __parts.next() {
428            | Some(__binary) => {
429                let __binary = __binary.to_string_lossy().to_string();
430                let __args = __parts.collect::<$crate::prelude::Vec<_>>();
431                $crate::util::cmd::run_status(&__binary, __args, None)
432            }
433            | None => Err($crate::prelude::io::Error::other("cmd!(...): empty command string")),
434        }
435    }};
436    // ── try literal interpolation ──────────────────────────
437    // try literal + dir
438    (try $cmd:literal; dir: $dir:expr) => {{
439        let mut __parts = $crate::cmd_sh_words!($cmd).into_iter();
440        match __parts.next() {
441            | Some(__binary) => {
442                let __binary = __binary.to_string_lossy().to_string();
443                let __args = __parts.collect::<$crate::prelude::Vec<_>>();
444                $crate::util::cmd::try_from_output(
445                    $crate::util::cmd::run_output(&__binary, __args, Some($dir.as_ref())),
446                )
447            }
448            | None => Err("cmd!(...): empty command string".to_string()),
449        }
450    }};
451    // try literal (no dir)
452    (try $cmd:literal) => {{
453        let mut __parts = $crate::cmd_sh_words!($cmd).into_iter();
454        match __parts.next() {
455            | Some(__binary) => {
456                let __binary = __binary.to_string_lossy().to_string();
457                let __args = __parts.collect::<$crate::prelude::Vec<_>>();
458                $crate::util::cmd::try_from_output(
459                    $crate::util::cmd::run_output(&__binary, __args, None),
460                )
461            }
462            | None => Err("cmd!(...): empty command string".to_string()),
463        }
464    }};
465    // ── default output literal interpolation ──────────────
466    // literal + dir
467    ($cmd:literal; dir: $dir:expr) => {{
468        let mut __parts = $crate::cmd_sh_words!($cmd).into_iter();
469        match __parts.next() {
470            | Some(__binary) => {
471                let __binary = __binary.to_string_lossy().to_string();
472                let __args = __parts.collect::<$crate::prelude::Vec<_>>();
473                $crate::util::cmd::run_output(&__binary, __args, Some($dir.as_ref()))
474            }
475            | None => Err($crate::prelude::io::Error::other("cmd!(...): empty command string")),
476        }
477    }};
478    // literal (no dir)
479    ($cmd:literal) => {{
480        let mut __parts = $crate::cmd_sh_words!($cmd).into_iter();
481        match __parts.next() {
482            | Some(__binary) => {
483                let __binary = __binary.to_string_lossy().to_string();
484                let __args = __parts.collect::<$crate::prelude::Vec<_>>();
485                $crate::util::cmd::run_output(&__binary, __args, None)
486            }
487            | None => Err($crate::prelude::io::Error::other("cmd!(...): empty command string")),
488        }
489    }};
490    // ── status CLI-style ───────────────────────────────────────
491    // status CLI-style + dir
492    (status $binary:literal $($arg:literal)*; dir: $dir:expr) => {{
493        $crate::util::cmd::run_status($binary, [$($arg),*], Some($dir.as_ref()))
494    }};
495    // status CLI-style (no dir)
496    (status $binary:literal $($arg:literal)*) => {{
497        $crate::util::cmd::run_status($binary, [$($arg),*], None)
498    }};
499    // ── status array ───────────────────────────────────────────
500    // status array + dir
501    (status $binary:expr, [ $($arg:expr),* $(,)? ]; dir: $dir:expr) => {{
502        let mut __cmd = $crate::prelude::Command::new($binary);
503        __cmd.args([$($arg),*]);
504        __cmd.current_dir($dir);
505        __cmd.status()
506    }};
507    // status array (no dir)
508    (status $binary:expr, [ $($arg:expr),* $(,)? ]) => {{
509        $crate::prelude::Command::new($binary).args([$($arg),*]).status()
510    }};
511    // ── status variable ───────────────────────────────────────
512    // status variable + dir
513    (status $binary:expr, $args:expr; dir: $dir:expr) => {{
514        let mut __cmd = $crate::prelude::Command::new($binary);
515        __cmd.args($args);
516        __cmd.current_dir($dir);
517        __cmd.status()
518    }};
519    // status variable (no dir)
520    (status $binary:expr, $args:expr) => {{
521        $crate::prelude::Command::new($binary).args($args).status()
522    }};
523    // ── try CLI-style ──────────────────────────────────────────
524    // try CLI-style + dir
525    (try $binary:literal $($arg:literal)*; dir: $dir:expr) => {{
526        $crate::util::cmd::try_from_output(
527            $crate::util::cmd::run_output($binary, [$($arg),*], Some($dir.as_ref())),
528        )
529    }};
530    // try CLI-style (no dir)
531    (try $binary:literal $($arg:literal)*) => {{
532        $crate::util::cmd::try_from_output(
533            $crate::util::cmd::run_output($binary, [$($arg),*], None),
534        )
535    }};
536    // ── try array ──────────────────────────────────────────────
537    // try array + dir
538    (try $binary:expr, [ $($arg:expr),* $(,)? ]; dir: $dir:expr) => {{
539        let mut __cmd = $crate::prelude::Command::new($binary);
540        __cmd.args([$($arg),*]);
541        __cmd.current_dir($dir);
542        $crate::util::cmd::try_from_output(__cmd.output())
543    }};
544    // try array (no dir)
545    (try $binary:expr, [ $($arg:expr),* $(,)? ]) => {{
546        $crate::util::cmd::try_from_output(
547            $crate::prelude::Command::new($binary).args([$($arg),*]).output(),
548        )
549    }};
550    // ── try variable ───────────────────────────────────────────
551    // try variable + dir
552    (try $binary:expr, $args:expr; dir: $dir:expr) => {{
553        let mut __cmd = $crate::prelude::Command::new($binary);
554        __cmd.args($args);
555        __cmd.current_dir($dir);
556        $crate::util::cmd::try_from_output(__cmd.output())
557    }};
558    // try variable (no dir)
559    (try $binary:expr, $args:expr) => {{
560        $crate::util::cmd::try_from_output(
561            $crate::prelude::Command::new($binary).args($args).output(),
562        )
563    }};
564    // ── default output CLI-style ──────────────────────────────
565    // CLI-style + dir
566    ($binary:literal $($arg:literal)*; dir: $dir:expr) => {{
567        $crate::util::cmd::run_output($binary, [$($arg),*], Some($dir.as_ref()))
568    }};
569    // CLI-style (no dir)
570    ($binary:literal $($arg:literal)*) => {{
571        $crate::util::cmd::run_output($binary, [$($arg),*], None)
572    }};
573    // ── default output array ──────────────────────────────────
574    // array + dir
575    ($binary:expr, [ $($arg:expr),* $(,)? ]; dir: $dir:expr) => {{
576        $crate::util::cmd::run_output($binary, [$($arg),*], Some($dir.as_ref()))
577    }};
578    // array (no dir)
579    ($binary:expr, [ $($arg:expr),* $(,)? ]) => {{
580        $crate::util::cmd::run_output($binary, [$($arg),*], None)
581    }};
582    // ── default output variable ───────────────────────────────
583    // variable + dir
584    ($binary:expr, $args:expr; dir: $dir:expr) => {{
585        $crate::util::cmd::run_output($binary, $args, Some($dir.as_ref()))
586    }};
587    // variable (no dir)
588    ($binary:expr, $args:expr) => {{
589        $crate::util::cmd::run_output($binary, $args, None)
590    }};
591}
592/// Build a [`Vec<OsString>`] of command arguments with automatic `.into()` conversion
593/// and `..` spread for conditional sub-vectors.
594///
595/// Eliminates the `[vec!["x".to_string()], vec![y], ..].concat()` pattern by
596/// accepting bare string literals, expressions, and spread iterables in a flat list.
597///
598/// # Syntax
599///
600/// ```ignore
601/// // Literals and variables — auto-converted via Into<OsString>
602/// let args = args!["--name", container_name, "--url", url];
603///
604/// // Spread — injects all items from an IntoIterator
605/// let extra = vec!["--gpus", "all"];
606/// let args = args!["run", "--detach", ..extra, "image"];
607///
608/// // Empty
609/// let args: Vec<OsString> = args![];
610/// ```
611///
612/// Internally composes iterator chains and collects — no mutable state.
613#[cfg(feature = "cmd")]
614#[macro_export]
615macro_rules! args {
616    // Spread with trailing items
617    (@build $acc:expr; .. $item:expr, $($rest:tt)*) => {
618        $crate::args!(@build ($acc.chain($item.into_iter().map(|s| s.into()))); $($rest)*)
619    };
620    // Spread, last item
621    (@build $acc:expr; .. $item:expr) => {
622        $acc.chain($item.into_iter().map(|s| s.into())).collect::<Vec<std::ffi::OsString>>()
623    };
624    // 2-tuple (key, value) — trailing items follow
625    (@build $acc:expr; ($k:expr, $v:expr), $($rest:tt)*) => {
626        $crate::args!(@build ($acc.chain(core::iter::once($k.into())).chain(core::iter::once($v.into()))); $($rest)*)
627    };
628    // 2-tuple (key, value) — last item
629    (@build $acc:expr; ($k:expr, $v:expr)) => {
630        $acc.chain(core::iter::once($k.into())).chain(core::iter::once($v.into())).collect::<Vec<std::ffi::OsString>>()
631    };
632    // Regular expression, trailing items follow
633    (@build $acc:expr; $item:expr, $($rest:tt)*) => {
634        $crate::args!(@build ($acc.chain(core::iter::once($item.into()))); $($rest)*)
635    };
636    // Regular expression, last item
637    (@build $acc:expr; $item:expr) => {
638        $acc.chain(core::iter::once($item.into())).collect::<Vec<std::ffi::OsString>>()
639    };
640    // Base case — no more tokens
641    (@build $acc:expr;) => {
642        $acc.collect::<Vec<std::ffi::OsString>>()
643    };
644    // Entry point
645    ($($tt:tt)*) => {
646        $crate::args!(@build (core::iter::empty::<std::ffi::OsString>()); $($tt)*)
647    };
648}
649/// Build an analyzer [`Check`] with required category/success and optional fields.
650///
651/// This macro wraps the existing builder API and keeps field assignment explicit.
652/// It accepts any builder method name as a field key (e.g. `severity`, `message`,
653/// `context`, `uri`, `status_code`, `errors`).
654///
655/// # Examples
656///
657/// ```ignore
658/// use acorn::{check, analyzer::{CheckCategory, CheckSeverity}};
659///
660/// let ok = check!(CheckCategory::Prose, true, message: "doc-1");
661/// let err = check!(
662///     CheckCategory::Readability,
663///     false,
664///     severity: CheckSeverity::Warning,
665///     message: "index.json",
666///     context: "12.1",
667/// );
668/// ```
669#[macro_export]
670macro_rules! check {
671    ($category:expr, $success:expr $(, $field:ident : $value:expr )* $(,)?) => {
672        $crate::check!(@apply $crate::analyzer::Check::init().category($category).success($success) $(, $field : $value )*)
673    };
674    (@apply $builder:expr) => {
675        $builder.build()
676    };
677    (@apply $builder:expr, $field:ident : $value:expr $(, $rest_field:ident : $rest_value:expr )* ) => {
678        $crate::check!(@apply $builder.$field($value) $(, $rest_field : $rest_value )*)
679    };
680}
681
682/// Build a successful [`Check`] with optional fields.
683///
684/// # Examples
685///
686/// ```ignore
687/// use acorn::{check_ok, analyzer::CheckCategory};
688///
689/// let check = check_ok!(CheckCategory::Quality, message: "input.json");
690/// ```
691#[macro_export]
692macro_rules! check_ok {
693    ($category:expr $(, $field:ident : $value:expr )* $(,)?) => {
694        $crate::check!($category, true $(, $field : $value )*)
695    };
696}
697
698/// Build a failing [`Check`] with `Error` severity and optional fields.
699///
700/// # Examples
701///
702/// ```ignore
703/// use acorn::{check_err, analyzer::CheckCategory};
704///
705/// let check = check_err!(CheckCategory::Schema, message: "invalid document");
706/// ```
707#[macro_export]
708macro_rules! check_err {
709    ($category:expr $(, $field:ident : $value:expr )* $(,)?) => {
710        $crate::check!($category, false, severity: $crate::analyzer::CheckSeverity::Error $(, $field : $value )*)
711    };
712}
713/// Logging macro for failures
714#[macro_export]
715macro_rules! fail {
716    ($msg:literal, $($rest:tt)*) => {
717        tracing::error!(
718            "{}",
719            format!(
720                "=> {} {}",
721                $crate::util::Label::fail(),
722                format!($msg, $($rest)*)
723            )
724        );
725    };
726    ($msg:literal) => {
727        tracing::error!("{}", format!("=> {} {}", $crate::util::Label::fail(), $msg));
728    };
729    ($($args:tt)*) => {
730        tracing::error!($($args)*);
731    };
732}
733/// Logging macro for skipped operations
734#[macro_export]
735macro_rules! skip {
736    ($msg:literal, $($rest:tt)*) => {
737        tracing::warn!(
738            "{}",
739            format!(
740                "=> {}{}",
741                $crate::util::Label::skip(),
742                format!($msg, $($rest)*)
743            )
744        );
745    };
746    ($msg:literal) => {
747        tracing::warn!("{}", format!("=> {}{}", $crate::util::Label::skip(), $msg));
748    };
749    ($($args:tt)*) => {
750        tracing::warn!($($args)*);
751    };
752}
753/// Creates a `Param` with the given style, name, and values.
754///
755/// The macro supports two calling styles:
756/// - **Ident style** (shorthand): `param!(QueryPair, "q", ...)`  
757///   The style is automatically qualified to `ParamStyle::QueryPair`
758/// - **Path style** (explicit): `param!(ParamStyle::QueryPair, "q", ...)`  
759///   Use when the full path is preferred or when `ParamStyle` is in scope
760///
761/// # Parameter Styles
762///
763/// Common body type parameters and request styles:
764/// - `Header`: HTTP header (e.g., `"PRIVATE-TOKEN"`)
765/// - `Body`: Request body payload (e.g., JSON or form data)
766/// - `QueryPair`: URL query string parameter
767/// - `FieldList`: Comma-separated field list
768/// - `TemplateValue`: URI template substitution
769/// - `KeyValuePair`: Key-value pair in query or body
770///
771/// # Value Syntaxes
772///
773/// The macro supports four different value syntaxes:
774/// 1. Single value: `param!(FieldList, "fl", "family-name")`
775/// 2. Single tuple: `param!(QueryPair, "filter", ("status", "inactive"))`
776/// 3. Multiple tuples: `param!(QueryPair, "q", (("key1", "val1"), ("key2", "val2")))`
777/// 4. Flat list: `param!(FieldList, "fields", vec!["field1", "field2"])`
778/// 5. Grouped list: `param!(FieldList, "fields", vec![vec!["field1"], vec!["field2"]])`
779/// 6. Body shorthand: `param!(Body &payload)` (uses an empty key for raw curl `-d` semantics)
780///
781/// # Examples
782///
783/// ```ignore
784/// // Shorthand with single value (ident style)
785/// param!(FieldList, "fl", "family-name")
786///
787/// // Shorthand with single tuple (ident style)
788/// param!(QueryPair, "filter", ("status", "inactive"))
789///
790/// // Shorthand with multiple tuples (ident style)
791/// param!(QueryPair, "q", (
792///     ("affiliation-org-name", "Lyrasis"),
793///     ("ror-org-id", "\"https://ror.org/01qz5mb56\""),
794/// ))
795///
796/// // Path style (explicit ParamStyle reference)
797/// param!(ParamStyle::FieldList, "fl", "family-name")
798///
799/// // KeyValuePair (query string key-value pair)
800/// param!(KeyValuePair, "per_page", "100")
801///
802/// param!(ParamStyle::KeyValuePair, "page", "2")
803///
804/// // Header parameter
805/// param!(Header, "PRIVATE-TOKEN", &token)
806///
807/// // Body shorthand (raw body, no key)
808/// param!(Body &payload)
809///
810/// // Body parameter (with key name)
811/// param!(Body, "body", &payload)
812///
813/// // Vec notation
814/// param!(QueryPair, "q", vec![
815///     vec!["affiliation-org-name", "Lyrasis"],
816/// ])
817/// ```
818#[macro_export]
819macro_rules! param {
820    // Body shorthand without key: param!(Body &payload)
821    (Body $val:expr) => {
822        $crate::io::api::Param::of_type($crate::io::api::ParamStyle::Body)
823            .values(vec![vec![Some($val)]])
824            .with_key("")
825    };
826    // Body shorthand without key (comma form): param!(Body, &payload)
827    (Body, $val:expr) => {
828        $crate::io::api::Param::of_type($crate::io::api::ParamStyle::Body)
829            .values(vec![vec![Some($val)]])
830            .with_key("")
831    };
832    // Ident with multiple tuples: param!(QueryPair, "q", (("a", "b"), ("c", "d")))
833    ($style:ident, $name:expr, ( $( ($($val:expr),* $(,)?) ),+ $(,)? )) => {
834        $crate::io::api::Param::of_type($crate::io::api::ParamStyle::$style)
835            .values(vec![ $( vec![ $( Some($val) ),* ] ),* ])
836            .with_key($name)
837    };
838    // Ident with single tuple: param!(QueryPair, "filter", ("status", "inactive"))
839    ($style:ident, $name:expr, ($($val:expr),+ $(,)?)) => {
840        $crate::io::api::Param::of_type($crate::io::api::ParamStyle::$style)
841            .values(vec![vec![ $( Some($val) ),* ]])
842            .with_key($name)
843    };
844    // Ident with vec notation: param!(FieldList, "fields", vec![vec!["f1"], vec!["f2"]])
845    ($style:ident, $name:expr, vec![ $( vec![ $($val:expr),* $(,)? ] ),* $(,)? ]) => {
846        $crate::io::api::Param::of_type($crate::io::api::ParamStyle::$style)
847            .values(vec![ $( vec![ $( Some($val) ),* ] ),* ])
848            .with_key($name)
849    };
850    // Ident with flat vec notation: param!(FieldList, "fields", vec!["f1", "f2"])
851    ($style:ident, $name:expr, vec![ $($val:expr),* $(,)? ]) => {
852        $crate::io::api::Param::of_type($crate::io::api::ParamStyle::$style)
853            .values(vec![ $( vec![Some($val)] ),* ])
854            .with_key($name)
855    };
856    // Ident with single value: param!(FieldList, "fl", "family-name")
857    ($style:ident, $name:expr, $val:expr) => {
858        $crate::io::api::Param::of_type($crate::io::api::ParamStyle::$style)
859            .values(vec![vec![Some($val)]])
860            .with_key($name)
861    };
862    // Path with multiple tuples: param!(ParamStyle::QueryPair, "q", (("a", "b"), ("c", "d")))
863    ($style:path, $name:expr, ( $( ($($val:expr),* $(,)?) ),+ $(,)? )) => {
864        $crate::io::api::Param::of_type($style)
865            .values(vec![ $( vec![ $( Some($val) ),* ] ),* ])
866            .with_key($name)
867    };
868    // Path with single tuple: param!(ParamStyle::QueryPair, "filter", ("status", "inactive"))
869    ($style:path, $name:expr, ($($val:expr),+ $(,)?)) => {
870        $crate::io::api::Param::of_type($style)
871            .values(vec![vec![ $( Some($val) ),* ]])
872            .with_key($name)
873    };
874    // Path with vec notation: param!(ParamStyle::FieldList, "fields", vec![vec!["f1"]])
875    ($style:path, $name:expr, vec![ $( vec![ $($val:expr),* $(,)? ] ),* $(,)? ]) => {
876        $crate::io::api::Param::of_type($style)
877            .values(vec![ $( vec![ $( Some($val) ),* ] ),* ])
878            .with_key($name)
879    };
880    // Path with flat vec notation: param!(ParamStyle::FieldList, "fields", vec!["f1", "f2"])
881    ($style:path, $name:expr, vec![ $($val:expr),* $(,)? ]) => {
882        $crate::io::api::Param::of_type($style)
883            .values(vec![ $( vec![Some($val)] ),* ])
884            .with_key($name)
885    };
886    // Path with single value: param!(ParamStyle::FieldList, "fl", "family-name")
887    ($style:path, $name:expr, $val:expr) => {
888        $crate::io::api::Param::of_type($style)
889            .values(vec![vec![Some($val)]])
890            .with_key($name)
891    };
892}
893/// Generate a validator function that delegates to a method on the input value
894///
895/// Creates a public function `fn(value: &str) -> Result<(), ValidationError>` that
896/// calls the given method on the input and validates the boolean result.
897///
898/// # Syntax
899///
900/// ```ignore
901/// // Full form: separate function name, method name, error code, and message
902/// method_validator!(
903///     /// Doc comment
904///     function_name,
905///     method_name,
906///     "error_code",
907///     "Error message"
908/// );
909///
910/// // Without message: generates "Provide a valid {code}"
911/// method_validator!(
912///     /// Doc comment
913///     function_name,
914///     method_name,
915///     "error_code"
916/// );
917///
918/// // Shorthand with message: uses function name as method name
919/// method_validator!(
920///     /// Doc comment
921///     function_name,
922///     "error_code",
923///     "Error message"
924/// );
925///
926/// // Minimal: uses function name as method name, generates default message
927/// method_validator!(
928///     /// Doc comment
929///     function_name,
930///     "error_code"
931/// );
932/// ```
933#[macro_export]
934macro_rules! method_validator {
935    ($(#[$meta:meta])* $fn_name:ident, $method:ident, $code:literal, $message:literal) => {
936        #[doc = concat!("Check if value is a valid ", $code)]
937        $(#[$meta])*
938        pub fn $fn_name(value: &str) -> Result<(), ::validator::ValidationError> {
939            match value.$method() {
940                | true => Ok(()),
941                | _ => Err(::validator::ValidationError::new($code).with_message($message.into())),
942            }
943        }
944    };
945    ($(#[$meta:meta])* $fn_name:ident, $method:ident, $code:literal) => {
946        #[doc = concat!("Check if value is a valid ", $code)]
947        $(#[$meta])*
948        pub fn $fn_name(value: &str) -> Result<(), ::validator::ValidationError> {
949            match value.$method() {
950                | true => Ok(()),
951                | _ => Err(::validator::ValidationError::new($code)
952                    .with_message(concat!("Provide valid ", $code).into())),
953            }
954        }
955    };
956    ($(#[$meta:meta])* $fn_name:ident, $code:literal, $message:literal) => {
957        #[doc = concat!("Check if value is a valid ", $code)]
958        $(#[$meta])*
959        pub fn $fn_name(value: &str) -> Result<(), ::validator::ValidationError> {
960            match value.$fn_name() {
961                | true => Ok(()),
962                | _ => Err(::validator::ValidationError::new($code).with_message($message.into())),
963            }
964        }
965    };
966    ($(#[$meta:meta])* $fn_name:ident, $code:literal) => {
967        #[doc = concat!("Check if value is a valid ", $code)]
968        $(#[$meta])*
969        pub fn $fn_name(value: &str) -> Result<(), ::validator::ValidationError> {
970            match value.$fn_name() {
971                | true => Ok(()),
972                | _ => Err(::validator::ValidationError::new($code)
973                    .with_message(concat!("Provide valid ", $code, " value").into())),
974            }
975        }
976    };
977}
978/// Generate a validator function that matches a value against a regex
979///
980/// Creates a public function `fn(value: &str) -> Result<(), ValidationError>` that
981/// matches the input against the given regex expression.
982///
983/// # Syntax
984/// ```ignore
985/// regex_validator!(
986///     /// Doc comment
987///     function_name,
988///     REGEX_CONSTANT,
989///     "error_code",
990///     "Error message"
991/// );
992/// ```
993#[macro_export]
994macro_rules! regex_validator {
995    ($(#[$meta:meta])* $fn_name:ident, $regex:expr, $code:literal, $message:literal) => {
996        #[doc = concat!("Check if value is a valid ", $code)]
997        $(#[$meta])*
998        pub fn $fn_name(value: &str) -> Result<(), ::validator::ValidationError> {
999            match $regex.is_match(value) {
1000                | Ok(value) if value => Ok(()),
1001                | _ => Err(::validator::ValidationError::new($code).with_message($message.into())),
1002            }
1003        }
1004    };
1005    ($(#[$meta:meta])* $fn_name:ident, $regex:expr, $code:literal) => {
1006        #[doc = concat!("Check if value is a valid ", $code)]
1007        $(#[$meta])*
1008        pub fn $fn_name(value: &str) -> Result<(), ::validator::ValidationError> {
1009            match $regex.is_match(value) {
1010                | Ok(value) if value => Ok(()),
1011                | _ => Err(::validator::ValidationError::new($code)
1012                    .with_message(concat!("Provide valid ", $code).into())),
1013            }
1014        }
1015    };
1016}
1017/// Generate a list validator function from an existing scalar validator function
1018///
1019/// Creates a public function `fn(value: &[String]) -> Result<(), ValidationError>` that
1020/// validates each value with the provided scalar validator and returns the first indexed error.
1021///
1022/// # Syntax
1023/// ```ignore
1024/// list_validator!(
1025///     /// Doc comment
1026///     list_function_name,
1027///     scalar_function_name,
1028///     "error_code",
1029///     "Error message"
1030/// );
1031///
1032/// list_validator!(
1033///     /// Doc comment
1034///     list_function_name,
1035///     scalar_function_name,
1036///     "error_code"
1037/// );
1038/// ```
1039#[macro_export]
1040macro_rules! list_validator {
1041    ($(#[$meta:meta])* $fn_name:ident, $validator:ident, $code:literal, $message:literal) => {
1042        #[doc = concat!("Check if all values are valid ", $code, " entries")]
1043        $(#[$meta])*
1044        pub fn $fn_name(value: &[String]) -> Result<(), ::validator::ValidationError> {
1045            value
1046                .iter()
1047                .position(|x| $validator(x).is_err())
1048                .map(|index| {
1049                    let mut err = ::validator::ValidationError::new($code).with_message($message.to_string().into());
1050                    err.add_param("index".into(), &index);
1051                    err
1052                })
1053                .map_or(Ok(()), Err)
1054        }
1055    };
1056    ($(#[$meta:meta])* $fn_name:ident, $validator:ident, $code:literal) => {
1057        #[doc = concat!("Check if all values are valid ", $code, " entries")]
1058        $(#[$meta])*
1059        pub fn $fn_name(value: &[String]) -> Result<(), ::validator::ValidationError> {
1060            value
1061                .iter()
1062                .position(|x| $validator(x).is_err())
1063                .map(|index| {
1064                    let mut err = ::validator::ValidationError::new($code)
1065                        .with_message(concat!("Every ", $code, " should be valid").to_string().into());
1066                    err.add_param("index".into(), &index);
1067                    err
1068                })
1069                .map_or(Ok(()), Err)
1070        }
1071    };
1072}
1073/// Implement [`MarkdownSupport`](crate::util::MarkdownSupport) through [`Display`](core::fmt::Display).
1074#[macro_export]
1075macro_rules! impl_to_markdown {
1076    ($($type:ty),+ $(,)?) => {
1077        $(impl $crate::util::MarkdownSupport for $type {
1078            fn to_markdown(&self) -> String {
1079                self.to_string()
1080            }
1081        })+
1082    };
1083}