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}