|
| 1 | +//! [`COMPLETE=$SHELL <bin>`][CompleteEnv] completion integration |
| 2 | +//! |
| 3 | +//! See [`CompleteEnv`]: |
| 4 | +//! ```rust |
| 5 | +//! # use clap_complete::dynamic::CompleteEnv; |
| 6 | +//! fn cli() -> clap::Command { |
| 7 | +//! // ... |
| 8 | +//! # clap::Command::new("empty") |
| 9 | +//! } |
| 10 | +//! |
| 11 | +//! fn main() { |
| 12 | +//! CompleteEnv::with_factory(cli) |
| 13 | +//! .complete(); |
| 14 | +//! |
| 15 | +//! // ... rest of application logic |
| 16 | +//! } |
| 17 | +//! ``` |
| 18 | +//! |
| 19 | +//! To source your completions: |
| 20 | +//! |
| 21 | +//! Bash |
| 22 | +//! ```bash |
| 23 | +//! echo "source <(COMPLETE=bash your_program complete)" >> ~/.bashrc |
| 24 | +//! ``` |
| 25 | +//! |
| 26 | +//! Elvish |
| 27 | +//! ```elvish |
| 28 | +//! echo "eval (COMPLETE=elvish your_program complete)" >> ~/.elvish/rc.elv |
| 29 | +//! ``` |
| 30 | +//! |
| 31 | +//! Fish |
| 32 | +//! ```fish |
| 33 | +//! echo "source (COMPLETE=fish your_program complete | psub)" >> ~/.config/fish/config.fish |
| 34 | +//! ``` |
| 35 | +//! |
| 36 | +//! Powershell |
| 37 | +//! ```powershell |
| 38 | +//! echo "COMPLETE=powershell your_program complete | Invoke-Expression" >> $PROFILE |
| 39 | +//! ``` |
| 40 | +//! |
| 41 | +//! Zsh |
| 42 | +//! ```zsh |
| 43 | +//! echo "source <(COMPLETE=zsh your_program complete)" >> ~/.zshrc |
| 44 | +//! ``` |
| 45 | +
|
| 46 | +mod shells; |
| 47 | + |
| 48 | +use std::ffi::OsString; |
| 49 | +use std::io::Write as _; |
| 50 | + |
| 51 | +pub use shells::*; |
| 52 | + |
| 53 | +/// Environment-activated completions for your CLI |
| 54 | +/// |
| 55 | +/// Benefits over CLI a completion argument or subcommand |
| 56 | +/// - Performance: we don't need to general [`clap::Command`] twice or parse arguments |
| 57 | +/// - Flexibility: there is no concern over it interfering with other CLI logic |
| 58 | +/// |
| 59 | +/// ```rust |
| 60 | +/// # use clap_complete::dynamic::CompleteEnv; |
| 61 | +/// fn cli() -> clap::Command { |
| 62 | +/// // ... |
| 63 | +/// # clap::Command::new("empty") |
| 64 | +/// } |
| 65 | +/// |
| 66 | +/// fn main() { |
| 67 | +/// CompleteEnv::with_factory(cli) |
| 68 | +/// .complete() |
| 69 | +/// |
| 70 | +/// // ... rest of application logic |
| 71 | +/// } |
| 72 | +/// ``` |
| 73 | +pub struct CompleteEnv<'s, F> { |
| 74 | + factory: F, |
| 75 | + var: &'static str, |
| 76 | + shells: Shells<'s>, |
| 77 | +} |
| 78 | + |
| 79 | +impl<'s, F: FnOnce() -> clap::Command> CompleteEnv<'s, F> { |
| 80 | + /// Complete a [`clap::Command`] |
| 81 | + /// |
| 82 | + /// # Example |
| 83 | + /// |
| 84 | + /// Builder: |
| 85 | + /// ```rust |
| 86 | + /// # use clap_complete::dynamic::CompleteEnv; |
| 87 | + /// fn cli() -> clap::Command { |
| 88 | + /// // ... |
| 89 | + /// # clap::Command::new("empty") |
| 90 | + /// } |
| 91 | + /// |
| 92 | + /// fn main() { |
| 93 | + /// CompleteEnv::with_factory(cli) |
| 94 | + /// .complete() |
| 95 | + /// |
| 96 | + /// // ... rest of application logic |
| 97 | + /// } |
| 98 | + /// ``` |
| 99 | + /// |
| 100 | + /// Derive: |
| 101 | + /// ``` |
| 102 | + /// # use clap::Parser; |
| 103 | + /// # use clap_complete::dynamic::CompleteEnv; |
| 104 | + /// use clap::CommandFactory as _; |
| 105 | + /// |
| 106 | + /// #[derive(Debug, Parser)] |
| 107 | + /// struct Cli { |
| 108 | + /// custom: Option<String>, |
| 109 | + /// } |
| 110 | + /// |
| 111 | + /// fn main() { |
| 112 | + /// CompleteEnv::with_factory(|| Cli::command()) |
| 113 | + /// .complete() |
| 114 | + /// |
| 115 | + /// // ... rest of application logic |
| 116 | + /// } |
| 117 | + /// ``` |
| 118 | + pub fn with_factory(factory: F) -> Self { |
| 119 | + Self { |
| 120 | + factory, |
| 121 | + var: "COMPLETE", |
| 122 | + shells: Shells::builtins(), |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + /// Override the environment variable used for enabling completions |
| 127 | + pub fn var(mut self, var: &'static str) -> Self { |
| 128 | + self.var = var; |
| 129 | + self |
| 130 | + } |
| 131 | + |
| 132 | + /// Override the shells supported for completions |
| 133 | + pub fn shells(mut self, shells: Shells<'s>) -> Self { |
| 134 | + self.shells = shells; |
| 135 | + self |
| 136 | + } |
| 137 | +} |
| 138 | + |
| 139 | +impl<'s, F: FnOnce() -> clap::Command> CompleteEnv<'s, F> { |
| 140 | + /// Process the completion request and exit |
| 141 | + /// |
| 142 | + /// **Warning:** `stdout` should not be written to before this has had a |
| 143 | + /// chance to run. |
| 144 | + pub fn complete(self) { |
| 145 | + let args = std::env::args_os(); |
| 146 | + let current_dir = std::env::current_dir().ok(); |
| 147 | + if self |
| 148 | + .try_complete(args, current_dir.as_deref()) |
| 149 | + .unwrap_or_else(|e| e.exit()) |
| 150 | + { |
| 151 | + std::process::exit(0) |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + /// Process the completion request |
| 156 | + /// |
| 157 | + /// **Warning:** `stdout` should not be written to before or after this has run. |
| 158 | + /// |
| 159 | + /// Returns `true` if a command was completed and `false` if this is a regular run of your |
| 160 | + /// application |
| 161 | + pub fn try_complete( |
| 162 | + self, |
| 163 | + args: impl IntoIterator<Item = impl Into<OsString>>, |
| 164 | + current_dir: Option<&std::path::Path>, |
| 165 | + ) -> clap::error::Result<bool> { |
| 166 | + self.try_complete_(args.into_iter().map(|a| a.into()).collect(), current_dir) |
| 167 | + } |
| 168 | + |
| 169 | + fn try_complete_( |
| 170 | + self, |
| 171 | + mut args: Vec<OsString>, |
| 172 | + current_dir: Option<&std::path::Path>, |
| 173 | + ) -> clap::error::Result<bool> { |
| 174 | + let Some(name) = std::env::var_os(self.var) else { |
| 175 | + return Ok(false); |
| 176 | + }; |
| 177 | + |
| 178 | + // Ensure any child processes called for custom completers don't activate their own |
| 179 | + // completion logic. |
| 180 | + std::env::remove_var(self.var); |
| 181 | + |
| 182 | + // Strip off the parent dir in case `$SHELL` was used |
| 183 | + let name = std::path::Path::new(&name).file_stem().unwrap_or(&name); |
| 184 | + // lossy won't match but this will delegate to unknown |
| 185 | + // error |
| 186 | + let name = name.to_string_lossy(); |
| 187 | + |
| 188 | + let shell = self.shells.completer(&name).ok_or_else(|| { |
| 189 | + let shells = self |
| 190 | + .shells |
| 191 | + .names() |
| 192 | + .enumerate() |
| 193 | + .map(|(i, name)| { |
| 194 | + let prefix = if i == 0 { "" } else { ", " }; |
| 195 | + format!("{prefix}`{name}`") |
| 196 | + }) |
| 197 | + .collect::<String>(); |
| 198 | + std::io::Error::new( |
| 199 | + std::io::ErrorKind::Other, |
| 200 | + format!("unknown shell `{name}`, expected one of {shells}"), |
| 201 | + ) |
| 202 | + })?; |
| 203 | + |
| 204 | + let mut cmd = (self.factory)(); |
| 205 | + cmd.build(); |
| 206 | + |
| 207 | + let escape_index = args |
| 208 | + .iter() |
| 209 | + .position(|a| *a == "--") |
| 210 | + .map(|i| i + 1) |
| 211 | + .unwrap_or(args.len()); |
| 212 | + args.drain(0..escape_index); |
| 213 | + if args.is_empty() { |
| 214 | + let name = cmd.get_name(); |
| 215 | + let bin = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name()); |
| 216 | + |
| 217 | + let mut buf = Vec::new(); |
| 218 | + shell.write_registration(self.var, name, bin, bin, &mut buf)?; |
| 219 | + std::io::stdout().write_all(&buf)?; |
| 220 | + } else { |
| 221 | + let mut buf = Vec::new(); |
| 222 | + shell.write_complete(&mut cmd, args, current_dir, &mut buf)?; |
| 223 | + std::io::stdout().write_all(&buf)?; |
| 224 | + } |
| 225 | + |
| 226 | + Ok(true) |
| 227 | + } |
| 228 | +} |
| 229 | + |
| 230 | +/// Collection of shell-specific completers |
| 231 | +pub struct Shells<'s>(pub &'s [&'s dyn EnvCompleter]); |
| 232 | + |
| 233 | +impl<'s> Shells<'s> { |
| 234 | + /// Select all of the built-in shells |
| 235 | + pub fn builtins() -> Self { |
| 236 | + Self(&[&Bash, &Elvish, &Fish, &Powershell, &Zsh]) |
| 237 | + } |
| 238 | + |
| 239 | + /// Find the specified [`EnvCompleter`] |
| 240 | + pub fn completer(&self, name: &str) -> Option<&dyn EnvCompleter> { |
| 241 | + self.0.iter().copied().find(|c| c.is(name)) |
| 242 | + } |
| 243 | + |
| 244 | + /// Collect all [`EnvCompleter::name`]s |
| 245 | + pub fn names(&self) -> impl Iterator<Item = &'static str> + 's { |
| 246 | + self.0.iter().map(|c| c.name()) |
| 247 | + } |
| 248 | +} |
| 249 | + |
| 250 | +/// Shell-integration for completions |
| 251 | +/// |
| 252 | +/// This will generally be called by [`CompleteEnv`]. |
| 253 | +/// |
| 254 | +/// This handles adapting between the shell and [`completer`][crate::dynamic::complete()]. |
| 255 | +/// A `EnvCompleter` can choose how much of that lives within the registration script or |
| 256 | +/// lives in [`EnvCompleter::write_complete`]. |
| 257 | +pub trait EnvCompleter { |
| 258 | + /// Canonical name for this shell |
| 259 | + /// |
| 260 | + /// **Post-conditions:** |
| 261 | + /// ```rust,ignore |
| 262 | + /// assert!(completer.is(completer.name())); |
| 263 | + /// ``` |
| 264 | + fn name(&self) -> &'static str; |
| 265 | + /// Whether the name matches this shell |
| 266 | + /// |
| 267 | + /// This should match [`EnvCompleter::name`] and any alternative names, particularly used by |
| 268 | + /// `$SHELL`. |
| 269 | + fn is(&self, name: &str) -> bool; |
| 270 | + /// Register for completions |
| 271 | + /// |
| 272 | + /// Write the `buf` the logic needed for calling into `<VAR>=<shell> <cmd> --`, passing needed |
| 273 | + /// arguments to [`EnvCompleter::write_complete`] through the environment. |
| 274 | + fn write_registration( |
| 275 | + &self, |
| 276 | + var: &str, |
| 277 | + name: &str, |
| 278 | + bin: &str, |
| 279 | + completer: &str, |
| 280 | + buf: &mut dyn std::io::Write, |
| 281 | + ) -> Result<(), std::io::Error>; |
| 282 | + /// Complete the given command |
| 283 | + /// |
| 284 | + /// Adapt information from arguments and [`EnvCompleter::write_registration`]-defined env |
| 285 | + /// variables to what is needed for [`completer`][crate::dynamic::complete()]. |
| 286 | + /// |
| 287 | + /// Write out the [`CompletionCandidate`][crate::dynamic::CompletionCandidate]s in a way the shell will understand. |
| 288 | + fn write_complete( |
| 289 | + &self, |
| 290 | + cmd: &mut clap::Command, |
| 291 | + args: Vec<OsString>, |
| 292 | + current_dir: Option<&std::path::Path>, |
| 293 | + buf: &mut dyn std::io::Write, |
| 294 | + ) -> Result<(), std::io::Error>; |
| 295 | +} |
0 commit comments