Skip to main content

usage/complete/
mod.rs

1use crate::error::UsageErr;
2use crate::Spec;
3
4mod bash;
5mod fish;
6mod nu;
7mod powershell;
8mod zsh;
9
10/// Options for generating shell completion scripts.
11pub struct CompleteOptions {
12    /// Path to the `usage` binary (e.g., "usage" or "/usr/local/bin/usage").
13    pub usage_bin: String,
14    /// Target shell: "bash", "fish", "zsh", or "powershell".
15    pub shell: String,
16    /// Name of the CLI binary to generate completions for.
17    pub bin: String,
18    /// Optional cache key (e.g., version) to avoid regenerating the spec file.
19    pub cache_key: Option<String>,
20    /// The usage spec to embed directly in the completion script.
21    pub spec: Option<Spec>,
22    /// Command to run to generate the usage spec dynamically.
23    pub usage_cmd: Option<String>,
24    /// Source file path for the `@generated` comment.
25    pub source_file: Option<String>,
26}
27
28/// Generates a shell completion script for the specified shell.
29///
30/// # Arguments
31/// * `options` - Configuration options including target shell and spec source
32///
33/// # Returns
34/// The generated completion script as a string, or an error if the shell is unsupported.
35///
36/// # Supported Shells
37/// - `bash` - Bash completion using `complete` builtin
38/// - `fish` - Fish shell completions
39/// - `zsh` - Zsh completion using `compdef`
40/// - `powershell` - PowerShell completion using `Register-ArgumentCompleter`
41pub fn complete(options: &CompleteOptions) -> Result<String, UsageErr> {
42    let viewed;
43    let effective;
44    let options = if let Some((spec, view)) = options
45        .spec
46        .as_ref()
47        .and_then(|spec| spec.view_for_program(&options.bin).map(|view| (spec, view)))
48    {
49        viewed = spec.for_view(view)?;
50        effective = CompleteOptions {
51            usage_bin: options.usage_bin.clone(),
52            shell: options.shell.clone(),
53            bin: options.bin.clone(),
54            cache_key: options.cache_key.clone(),
55            spec: Some(viewed),
56            usage_cmd: options.usage_cmd.clone(),
57            source_file: options.source_file.clone(),
58        };
59        &effective
60    } else {
61        options
62    };
63    match options.shell.as_str() {
64        "bash" => Ok(bash::complete_bash(options)),
65        "fish" => Ok(fish::complete_fish(options)),
66        "nu" => Ok(nu::complete_nu(options)),
67        "powershell" => Ok(powershell::complete_powershell(options)),
68        "zsh" => Ok(zsh::complete_zsh(options)),
69        _ => Err(UsageErr::UnsupportedShell(options.shell.clone())),
70    }
71}
72
73/// Generates a shell-specific "init" script that enables tab-completion for any
74/// command on `$PATH` whose first line is a `usage` shebang, without requiring
75/// per-script `usage g completion` generation. The user sources this once from
76/// their shell rc.
77///
78/// # Supported Shells
79/// - `bash` - registers a `complete -D` default handler
80/// - `zsh` - registers a `compdef -default-` fallback handler
81/// - `fish` - scans `$PATH` once at startup and registers per-command completers
82pub fn complete_init(shell: &str, usage_bin: &str) -> Result<String, UsageErr> {
83    match shell {
84        "bash" => Ok(bash::complete_bash_init(usage_bin)),
85        "fish" => Ok(fish::complete_fish_init(usage_bin)),
86        "zsh" => Ok(zsh::complete_zsh_init(usage_bin)),
87        _ => Err(UsageErr::UnsupportedShell(shell.to_string())),
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn complete_init_supported_shells() {
97        for shell in ["bash", "zsh", "fish"] {
98            let out = complete_init(shell, "usage").expect("supported shell");
99            assert!(!out.is_empty(), "{shell} init should not be empty");
100        }
101    }
102
103    #[test]
104    fn complete_init_rejects_unsupported_shell() {
105        let err = complete_init("nu", "usage").expect_err("nu has no init script");
106        assert!(matches!(err, UsageErr::UnsupportedShell(ref s) if s == "nu"));
107    }
108}