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}