Skip to main content

asimov_cli/
aliases.rs

1// This is free and unencumbered software released into the public domain.
2
3//! Command aliases.
4//!
5//! Aliases are resolved by rewriting the command-line arguments before they
6//! are parsed: the first subcommand token is looked up in the alias table
7//! and, if found, replaced by its expansion.
8//!
9//! For now the alias table is hardcoded; in the future it is anticipated to
10//! be user-configurable via a config file.
11
12use std::ffi::OsString;
13
14/// The table of command aliases, mapping an alias name to its expansion.
15pub static ALIASES: &[(&str, &[&str])] = &[
16    ("fetch", &["source", "fetch"]),
17    ("install", &["module", "install"]),
18    ("list", &["source", "list"]),
19    ("resolve", &["module", "resolve"]),
20    ("snap", &["source", "snap"]),
21    ("uninstall", &["module", "uninstall"]),
22    ("upgrade", &["module", "upgrade"]),
23];
24
25/// Global options that consume a value in a separate argument
26/// (e.g. `--color auto`), which must be skipped over when locating
27/// the subcommand token.
28const OPTIONS_WITH_VALUES: &[&str] = &["--color"];
29
30/// Resolves command aliases by rewriting `args` in place.
31///
32/// Locates the first subcommand token (the first argument after the program
33/// name that isn't an option or an option's value) and, if it names an alias,
34/// splices in the expansion. Any arguments following the alias are preserved.
35///
36/// The `help` subcommand is treated as transparent, so `asimov help fetch`
37/// expands to `asimov help source fetch`.
38pub fn resolve(args: &mut Vec<OsString>) {
39    let mut i = 1; // skip the program name
40    while i < args.len() {
41        let Some(arg) = args[i].to_str() else {
42            return; // non-UTF-8 argument: leave the command line untouched
43        };
44
45        // stop at `--`: everything after it is positional
46        if arg == "--" {
47            return;
48        }
49
50        // skip options (and their values), but treat a lone `-` as positional
51        if arg.starts_with('-') && arg.len() > 1 {
52            i += if OPTIONS_WITH_VALUES.contains(&arg) {
53                2
54            } else {
55                1
56            };
57            continue;
58        }
59
60        // `help` is transparent: expand the alias it's asking about instead
61        if arg == "help" {
62            i += 1;
63            continue;
64        }
65
66        // found the subcommand token: expand it if it's an alias
67        if let Some((_, expansion)) = ALIASES.iter().find(|(name, _)| *name == arg) {
68            args.splice(i..=i, expansion.iter().map(OsString::from));
69        }
70        return;
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    fn resolved(args: &[&str]) -> Vec<String> {
79        let mut args: Vec<OsString> = args.iter().map(OsString::from).collect();
80        resolve(&mut args);
81        args.into_iter()
82            .map(|arg| arg.into_string().unwrap())
83            .collect()
84    }
85
86    #[test]
87    fn expands_fetch() {
88        assert_eq!(
89            resolved(&["asimov", "fetch", "http://example.org"]),
90            ["asimov", "source", "fetch", "http://example.org"]
91        );
92    }
93
94    #[test]
95    fn expands_snap() {
96        assert_eq!(resolved(&["asimov", "snap"]), ["asimov", "source", "snap"]);
97    }
98
99    #[test]
100    fn expands_install() {
101        assert_eq!(
102            resolved(&["asimov", "install", "serpapi"]),
103            ["asimov", "module", "install", "serpapi"]
104        );
105    }
106
107    #[test]
108    fn skips_leading_flags() {
109        assert_eq!(
110            resolved(&["asimov", "-d", "--color", "auto", "fetch", "url"]),
111            ["asimov", "-d", "--color", "auto", "source", "fetch", "url"]
112        );
113    }
114
115    #[test]
116    fn leaves_non_aliases_untouched() {
117        assert_eq!(
118            resolved(&["asimov", "module", "list"]),
119            ["asimov", "module", "list"]
120        );
121    }
122
123    #[test]
124    fn only_expands_the_subcommand_position() {
125        assert_eq!(
126            resolved(&["asimov", "module", "install", "fetch"]),
127            ["asimov", "module", "install", "fetch"]
128        );
129    }
130
131    #[test]
132    fn expands_alias_after_help() {
133        assert_eq!(
134            resolved(&["asimov", "help", "fetch"]),
135            ["asimov", "help", "source", "fetch"]
136        );
137        assert_eq!(
138            resolved(&["asimov", "help", "install"]),
139            ["asimov", "help", "module", "install"]
140        );
141    }
142
143    #[test]
144    fn leaves_help_for_non_aliases_untouched() {
145        assert_eq!(resolved(&["asimov", "help"]), ["asimov", "help"]);
146        assert_eq!(
147            resolved(&["asimov", "help", "module"]),
148            ["asimov", "help", "module"]
149        );
150    }
151
152    #[test]
153    fn ignores_args_after_double_dash() {
154        assert_eq!(
155            resolved(&["asimov", "--", "fetch"]),
156            ["asimov", "--", "fetch"]
157        );
158    }
159}