Skip to main content

getargs_derive/
completions.rs

1//! Shell completion scripts and man pages generated from the [`Command`] metadata.
2//!
3//! Requires the `std` feature. See [Shell Completions and Man Pages][crate::guide::completions].
4
5mod bash;
6mod fish;
7mod man;
8mod zsh;
9
10use crate::{ArgInfo, Command, CommandInfo, ParseValueEnumError};
11use core::fmt;
12use core::str::FromStr;
13use std::io::{self, Write};
14use std::string::String;
15use std::vec::Vec;
16
17/// A shell for which a completion script can be generated.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum Shell {
21    /// Bash.
22    Bash,
23    /// Zsh.
24    Zsh,
25    /// Fish.
26    Fish,
27}
28
29impl Shell {
30    /// Every shell, in the order of [`NAMES`](Self::NAMES).
31    pub const ALL: &'static [Shell] = &[Shell::Bash, Shell::Zsh, Shell::Fish];
32
33    /// The name of every shell, as accepted by [`FromStr`] and written by [`Display`](fmt::Display).
34    pub const NAMES: &'static [&'static str] = &["bash", "zsh", "fish"];
35
36    /// The name of the shell: `bash`, `zsh` or `fish`.
37    pub const fn name(self) -> &'static str {
38        match self {
39            Shell::Bash => "bash",
40            Shell::Zsh => "zsh",
41            Shell::Fish => "fish",
42        }
43    }
44
45    /// The usual file name of the completion script of the command `name`: `name.bash`, `_name`
46    /// or `name.fish`.
47    pub fn file_name(self, name: &str) -> String {
48        match self {
49            Shell::Bash => std::format!("{name}.bash"),
50            Shell::Zsh => std::format!("_{name}"),
51            Shell::Fish => std::format!("{name}.fish"),
52        }
53    }
54
55    /// Renders the completion script of `command`.
56    pub fn render(self, command: &CommandInfo) -> String {
57        match self {
58            Shell::Bash => bash::render(command),
59            Shell::Zsh => zsh::render(command),
60            Shell::Fish => fish::render(command),
61        }
62    }
63}
64
65impl FromStr for Shell {
66    type Err = ParseValueEnumError;
67
68    fn from_str(value: &str) -> Result<Self, Self::Err> {
69        Self::ALL
70            .iter()
71            .copied()
72            .find(|shell| shell.name() == value)
73            .ok_or(ParseValueEnumError)
74    }
75}
76
77impl fmt::Display for Shell {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        f.write_str(self.name())
80    }
81}
82
83/// Writes the completion script of `C` for `shell` to `output`.
84pub fn generate<C: Command>(shell: Shell, output: &mut impl Write) -> io::Result<()> {
85    output.write_all(shell.render(&C::COMMAND).as_bytes())
86}
87
88/// Writes the man page of `C`, in roff, to `output`.
89pub fn man_page<C: Command>(output: &mut impl Write) -> io::Result<()> {
90    output.write_all(render_man_page(&C::COMMAND).as_bytes())
91}
92
93/// Renders the man page of `command`, in roff.
94pub fn render_man_page(command: &CommandInfo) -> String {
95    man::render(command)
96}
97
98struct Node<'c> {
99    path: Vec<&'c CommandInfo>,
100    command: &'c CommandInfo,
101    args: Vec<&'c ArgInfo>,
102}
103
104impl<'c> Node<'c> {
105    fn root(command: &'c CommandInfo) -> Self {
106        Node {
107            path: std::vec![command],
108            command,
109            args: command.args.iter().collect(),
110        }
111    }
112
113    fn id(&self) -> String {
114        let names: Vec<String> = self
115            .path
116            .iter()
117            .map(|command| identifier(command.name))
118            .collect();
119        names.join("__")
120    }
121}
122
123fn tree(command: &CommandInfo) -> Vec<Node<'_>> {
124    fn visit<'c>(node: Node<'c>, nodes: &mut Vec<Node<'c>>) {
125        let globals: Vec<&ArgInfo> = node.args.iter().copied().filter(|arg| arg.global).collect();
126        let children: Vec<Node<'c>> = node
127            .command
128            .commands
129            .iter()
130            .map(|child| {
131                let mut path = node.path.clone();
132                path.push(child);
133                let mut args: Vec<&ArgInfo> = child.args.iter().collect();
134                let inherited: Vec<&ArgInfo> = globals
135                    .iter()
136                    .copied()
137                    .filter(|global| !args.iter().any(|arg| arg.id == global.id))
138                    .collect();
139                args.extend(inherited);
140                Node {
141                    path,
142                    command: child,
143                    args,
144                }
145            })
146            .collect();
147        nodes.push(node);
148        for child in children {
149            visit(child, nodes);
150        }
151    }
152    let mut nodes = Vec::new();
153    visit(Node::root(command), &mut nodes);
154    nodes
155}
156
157fn visible<'n>(node: &'n Node<'_>) -> impl Iterator<Item = &'n ArgInfo> {
158    node.args.iter().copied().filter(|arg| !arg.hidden)
159}
160
161fn options<'n>(node: &'n Node<'_>) -> impl Iterator<Item = &'n ArgInfo> {
162    visible(node).filter(|arg| !arg.is_positional())
163}
164
165fn positionals<'n>(node: &'n Node<'_>) -> impl Iterator<Item = &'n ArgInfo> {
166    visible(node).filter(|arg| arg.is_positional())
167}
168
169fn subcommands(command: &CommandInfo) -> impl Iterator<Item = &CommandInfo> {
170    command.commands.iter().filter(|command| !command.hidden)
171}
172
173fn command_names(command: &CommandInfo) -> Vec<&'static str> {
174    core::iter::once(command.name)
175        .chain(command.visible_aliases.iter().copied())
176        .collect()
177}
178
179fn all_command_names(command: &CommandInfo) -> Vec<&'static str> {
180    core::iter::once(command.name)
181        .chain(command.visible_aliases.iter().copied())
182        .chain(command.aliases.iter().copied())
183        .collect()
184}
185
186fn shorts(arg: &ArgInfo) -> Vec<char> {
187    arg.short
188        .into_iter()
189        .chain(arg.visible_short_aliases.iter().copied())
190        .collect()
191}
192
193fn longs(arg: &ArgInfo) -> Vec<&'static str> {
194    arg.long
195        .into_iter()
196        .chain(arg.visible_aliases.iter().copied())
197        .collect()
198}
199
200fn flags(arg: &ArgInfo) -> Vec<String> {
201    let mut flags: Vec<String> = shorts(arg)
202        .into_iter()
203        .map(|short| std::format!("-{short}"))
204        .collect();
205    flags.extend(longs(arg).into_iter().map(|long| std::format!("--{long}")));
206    flags
207}
208
209fn summary(text: &str) -> &str {
210    text.lines().next().unwrap_or_default().trim()
211}
212
213fn identifier(name: &str) -> String {
214    name.chars()
215        .map(|character| {
216            if character.is_ascii_alphanumeric() {
217                character
218            } else {
219                '_'
220            }
221        })
222        .collect()
223}