Skip to main content

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}