asimov_runner/command_ext.rs
1// This is free and unencumbered software released into the public domain.
2
3//! Fluent construction of optional long-form command-line arguments.
4
5use crate::Command;
6use alloc::format;
7use core::fmt::Display;
8
9/// Additional argument-building methods for Tokio [`Command`].
10///
11/// Import this trait to chain optional named arguments with the command's
12/// existing `arg`, `args`, and standard-stream configuration methods.
13///
14/// ```
15/// use asimov_runner::{Command, CommandExt};
16///
17/// let mut command = Command::new("asimov-example-lister");
18/// let before = Some(String::from("urn:example:entry:123"));
19/// command
20/// .option("sort", Some("name"))
21/// .option("offset", None::<usize>)
22/// .option("before", before.as_ref())
23/// .option("limit", Some(25))
24/// .arg("https://example.com/collection");
25///
26/// let args: Vec<_> = command.as_std().get_args().collect();
27/// assert_eq!(args, [
28/// "--sort=name",
29/// "--before=urn:example:entry:123",
30/// "--limit=25",
31/// "https://example.com/collection",
32/// ]);
33/// ```
34pub trait CommandExt {
35 /// Appends `--name=value` for `Some(value)` and nothing for `None`.
36 ///
37 /// `name` is the long option name without leading dashes. Values use
38 /// [`Display`], accepting numbers, strings, and domain types such as sort
39 /// keys. Use `as_ref()` to borrow a non-`Copy` optional value. `Some(0)`,
40 /// `Some(false)`, and `Some("")` are explicit values and are not omitted.
41 ///
42 /// Each present option is one literal argument, preserving invocation order,
43 /// spaces, and punctuation without shell escaping or expansion. Names and
44 /// values are not validated. Capability decisions remain with the caller:
45 /// use [`Option::filter`] to conditionally omit an otherwise present option.
46 fn option(&mut self, name: &str, value: Option<impl Display>) -> &mut Self;
47}
48
49impl CommandExt for Command {
50 fn option(&mut self, name: &str, value: Option<impl Display>) -> &mut Self {
51 if let Some(value) = value {
52 self.arg(format!("--{name}={value}"));
53 }
54 self
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use alloc::vec::Vec;
62
63 #[test]
64 fn preserves_explicit_values_argument_boundaries_and_order() {
65 let mut command = Command::new("asimov-test-program");
66 command
67 .arg("first")
68 .option("missing", None::<&str>)
69 .option("offset", Some(0))
70 .option("enabled", Some(false))
71 .option("empty", Some(""))
72 .option("literal", Some("café a=b; '$HOME'\nnext"))
73 .option("repeat", Some(1))
74 .option("repeat", Some(2))
75 .arg("last");
76 let args: Vec<_> = command.as_std().get_args().collect();
77 assert_eq!(
78 args,
79 [
80 "first",
81 "--offset=0",
82 "--enabled=false",
83 "--empty=",
84 "--literal=café a=b; '$HOME'\nnext",
85 "--repeat=1",
86 "--repeat=2",
87 "last",
88 ]
89 );
90 }
91}