Skip to content

Commit 6f88091

Browse files
authored
Merge pull request #5671 from epage/env
feat(complete): Env hook for dynamic completions
2 parents 84cfd92 + c402ec6 commit 6f88091

File tree

23 files changed

+967
-20
lines changed

23 files changed

+967
-20
lines changed

clap_complete/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ required-features = ["unstable-dynamic", "unstable-command"]
5757
[features]
5858
default = []
5959
unstable-doc = ["unstable-dynamic", "unstable-command"] # for docs.rs
60-
unstable-dynamic = ["dep:clap_lex", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"]
61-
unstable-command = ["unstable-dynamic", "dep:shlex", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"]
60+
unstable-dynamic = ["dep:clap_lex", "dep:shlex", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"]
61+
unstable-command = ["unstable-dynamic", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"]
6262
debug = ["clap/debug"]
6363

6464
[lints]

clap_complete/examples/dynamic.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ fn command() -> clap::Command {
2020
}
2121

2222
fn main() {
23+
clap_complete::dynamic::CompleteEnv::with_factory(command).complete();
24+
2325
let cmd = command();
2426
let matches = cmd.get_matches();
2527
if let Ok(completions) = clap_complete::dynamic::CompleteCommand::from_arg_matches(&matches) {

clap_complete/examples/exhaustive.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ use clap::{FromArgMatches, Subcommand};
44
use clap_complete::{generate, Generator, Shell};
55

66
fn main() {
7+
#[cfg(feature = "unstable-dynamic")]
8+
clap_complete::dynamic::CompleteEnv::with_factory(cli).complete();
9+
710
let matches = cli().get_matches();
811
if let Some(generator) = matches.get_one::<Shell>("generate") {
912
let mut cmd = cli();

clap_complete/src/dynamic/env/mod.rs

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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

Comments
 (0)