Skip to main content

dev_prune/commands/
completions.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Handler for `dev-prune completions`.
5//
6// The script is generated from the same clap definition the binary parses arguments
7// with, so a flag cannot exist in one and be missing from the other. That is the whole
8// reason this is a subcommand rather than five checked-in files that go stale.
9
10use std::path::PathBuf;
11
12use anyhow::Result;
13use clap::CommandFactory;
14use clap_complete::Shell;
15
16use crate::Cli;
17use crate::constants;
18
19/// Print a completion script for `shell` to stdout.
20///
21/// The script is written for whichever of the two names invoked it — `devp completions`
22/// completes `devp`, `dev-prune completions` completes `dev-prune`. They are the same
23/// executable, but a completion script is registered against a command *name*, so one
24/// script cannot serve both and guessing would leave half the users without completion.
25///
26/// Nothing else is printed. Not a header, not the credit line, not a "now add this to
27/// your profile" hint — the output is piped into a file or `eval`'d, and anything extra
28/// in it is a shell error on every new terminal.
29pub fn run(shell: Shell) -> Result<()> {
30    let mut command = Cli::command();
31    let bin_name = invoked_as();
32    clap_complete::generate(shell, &mut command, bin_name, &mut std::io::stdout());
33    Ok(())
34}
35
36/// The name this process was launched under, without any `.exe`.
37///
38/// Falls back to the canonical name when `argv[0]` is missing or empty, which is not a
39/// thing a shell does but is a thing an embedder can do.
40fn invoked_as() -> String {
41    std::env::args_os()
42        .next()
43        .map(PathBuf::from)
44        .and_then(|path| {
45            path.file_stem()
46                .map(|stem| stem.to_string_lossy().into_owned())
47        })
48        .filter(|name| !name.is_empty())
49        .unwrap_or_else(|| constants::APP_NAME.to_string())
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn every_shell_produces_a_script() {
58        // A generator that panics or emits nothing would only be discovered by a user
59        // sourcing the output, at which point their shell is the error message.
60        for shell in [
61            Shell::Bash,
62            Shell::Zsh,
63            Shell::Fish,
64            Shell::PowerShell,
65            Shell::Elvish,
66        ] {
67            let mut command = Cli::command();
68            let mut buffer: Vec<u8> = Vec::new();
69            clap_complete::generate(shell, &mut command, "devp", &mut buffer);
70            let script = String::from_utf8(buffer).expect("completion script is UTF-8");
71
72            assert!(!script.is_empty(), "{shell} produced nothing");
73            assert!(
74                script.contains("devp"),
75                "{shell} script does not name the binary"
76            );
77            assert!(
78                script.contains("stats"),
79                "{shell} script is missing a subcommand"
80            );
81        }
82    }
83
84    #[test]
85    fn the_binary_name_falls_back_to_the_canonical_one() {
86        // Not empty, whatever the test harness passes as argv[0].
87        assert!(!invoked_as().is_empty());
88    }
89}