Skip to content

Commit

Permalink
feat(complete): Env hook for dynamic completions
Browse files Browse the repository at this point in the history
Fixes #3930
  • Loading branch information
epage committed Aug 12, 2024
1 parent 6288e11 commit c402ec6
Show file tree
Hide file tree
Showing 17 changed files with 944 additions and 2 deletions.
4 changes: 2 additions & 2 deletions clap_complete/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ required-features = ["unstable-dynamic", "unstable-command"]
[features]
default = []
unstable-doc = ["unstable-dynamic", "unstable-command"] # for docs.rs
unstable-dynamic = ["dep:clap_lex", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"]
unstable-command = ["unstable-dynamic", "dep:shlex", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"]
unstable-dynamic = ["dep:clap_lex", "dep:shlex", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"]
unstable-command = ["unstable-dynamic", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"]
debug = ["clap/debug"]

[lints]
Expand Down
2 changes: 2 additions & 0 deletions clap_complete/examples/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ fn command() -> clap::Command {
}

fn main() {
clap_complete::dynamic::CompleteEnv::with_factory(command).complete();

let cmd = command();
let matches = cmd.get_matches();
if let Ok(completions) = clap_complete::dynamic::CompleteCommand::from_arg_matches(&matches) {
Expand Down
3 changes: 3 additions & 0 deletions clap_complete/examples/exhaustive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ use clap::{FromArgMatches, Subcommand};
use clap_complete::{generate, Generator, Shell};

fn main() {
#[cfg(feature = "unstable-dynamic")]
clap_complete::dynamic::CompleteEnv::with_factory(cli).complete();

let matches = cli().get_matches();
if let Some(generator) = matches.get_one::<Shell>("generate") {
let mut cmd = cli();
Expand Down
295 changes: 295 additions & 0 deletions clap_complete/src/dynamic/env/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
//! [`COMPLETE=$SHELL <bin>`][CompleteEnv] completion integration
//!
//! See [`CompleteEnv`]:
//! ```rust
//! # use clap_complete::dynamic::CompleteEnv;
//! fn cli() -> clap::Command {
//! // ...
//! # clap::Command::new("empty")
//! }
//!
//! fn main() {
//! CompleteEnv::with_factory(cli)
//! .complete();
//!
//! // ... rest of application logic
//! }
//! ```
//!
//! To source your completions:
//!
//! Bash
//! ```bash
//! echo "source <(COMPLETE=bash your_program complete)" >> ~/.bashrc
//! ```
//!
//! Elvish
//! ```elvish
//! echo "eval (COMPLETE=elvish your_program complete)" >> ~/.elvish/rc.elv
//! ```
//!
//! Fish
//! ```fish
//! echo "source (COMPLETE=fish your_program complete | psub)" >> ~/.config/fish/config.fish
//! ```
//!
//! Powershell
//! ```powershell
//! echo "COMPLETE=powershell your_program complete | Invoke-Expression" >> $PROFILE
//! ```
//!
//! Zsh
//! ```zsh
//! echo "source <(COMPLETE=zsh your_program complete)" >> ~/.zshrc
//! ```
mod shells;

use std::ffi::OsString;
use std::io::Write as _;

pub use shells::*;

/// Environment-activated completions for your CLI
///
/// Benefits over CLI a completion argument or subcommand
/// - Performance: we don't need to general [`clap::Command`] twice or parse arguments
/// - Flexibility: there is no concern over it interfering with other CLI logic
///
/// ```rust
/// # use clap_complete::dynamic::CompleteEnv;
/// fn cli() -> clap::Command {
/// // ...
/// # clap::Command::new("empty")
/// }
///
/// fn main() {
/// CompleteEnv::with_factory(cli)
/// .complete()
///
/// // ... rest of application logic
/// }
/// ```
pub struct CompleteEnv<'s, F> {
factory: F,
var: &'static str,
shells: Shells<'s>,
}

impl<'s, F: FnOnce() -> clap::Command> CompleteEnv<'s, F> {
/// Complete a [`clap::Command`]
///
/// # Example
///
/// Builder:
/// ```rust
/// # use clap_complete::dynamic::CompleteEnv;
/// fn cli() -> clap::Command {
/// // ...
/// # clap::Command::new("empty")
/// }
///
/// fn main() {
/// CompleteEnv::with_factory(cli)
/// .complete()
///
/// // ... rest of application logic
/// }
/// ```
///
/// Derive:
/// ```
/// # use clap::Parser;
/// # use clap_complete::dynamic::CompleteEnv;
/// use clap::CommandFactory as _;
///
/// #[derive(Debug, Parser)]
/// struct Cli {
/// custom: Option<String>,
/// }
///
/// fn main() {
/// CompleteEnv::with_factory(|| Cli::command())
/// .complete()
///
/// // ... rest of application logic
/// }
/// ```
pub fn with_factory(factory: F) -> Self {
Self {
factory,
var: "COMPLETE",
shells: Shells::builtins(),
}
}

/// Override the environment variable used for enabling completions
pub fn var(mut self, var: &'static str) -> Self {
self.var = var;
self
}

/// Override the shells supported for completions
pub fn shells(mut self, shells: Shells<'s>) -> Self {
self.shells = shells;
self
}
}

impl<'s, F: FnOnce() -> clap::Command> CompleteEnv<'s, F> {
/// Process the completion request and exit
///
/// **Warning:** `stdout` should not be written to before this has had a
/// chance to run.
pub fn complete(self) {
let args = std::env::args_os();
let current_dir = std::env::current_dir().ok();
if self
.try_complete(args, current_dir.as_deref())
.unwrap_or_else(|e| e.exit())
{
std::process::exit(0)
}
}

/// Process the completion request
///
/// **Warning:** `stdout` should not be written to before or after this has run.
///
/// Returns `true` if a command was completed and `false` if this is a regular run of your
/// application
pub fn try_complete(
self,
args: impl IntoIterator<Item = impl Into<OsString>>,
current_dir: Option<&std::path::Path>,
) -> clap::error::Result<bool> {
self.try_complete_(args.into_iter().map(|a| a.into()).collect(), current_dir)
}

fn try_complete_(
self,
mut args: Vec<OsString>,
current_dir: Option<&std::path::Path>,
) -> clap::error::Result<bool> {
let Some(name) = std::env::var_os(self.var) else {
return Ok(false);
};

// Ensure any child processes called for custom completers don't activate their own
// completion logic.
std::env::remove_var(self.var);

// Strip off the parent dir in case `$SHELL` was used
let name = std::path::Path::new(&name).file_stem().unwrap_or(&name);
// lossy won't match but this will delegate to unknown
// error
let name = name.to_string_lossy();

let shell = self.shells.completer(&name).ok_or_else(|| {
let shells = self
.shells
.names()
.enumerate()
.map(|(i, name)| {
let prefix = if i == 0 { "" } else { ", " };
format!("{prefix}`{name}`")
})
.collect::<String>();
std::io::Error::new(
std::io::ErrorKind::Other,
format!("unknown shell `{name}`, expected one of {shells}"),
)
})?;

let mut cmd = (self.factory)();
cmd.build();

let escape_index = args
.iter()
.position(|a| *a == "--")
.map(|i| i + 1)
.unwrap_or(args.len());
args.drain(0..escape_index);
if args.is_empty() {
let name = cmd.get_name();
let bin = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name());

let mut buf = Vec::new();
shell.write_registration(self.var, name, bin, bin, &mut buf)?;
std::io::stdout().write_all(&buf)?;
} else {
let mut buf = Vec::new();
shell.write_complete(&mut cmd, args, current_dir, &mut buf)?;
std::io::stdout().write_all(&buf)?;
}

Ok(true)
}
}

/// Collection of shell-specific completers
pub struct Shells<'s>(pub &'s [&'s dyn EnvCompleter]);

impl<'s> Shells<'s> {
/// Select all of the built-in shells
pub fn builtins() -> Self {
Self(&[&Bash, &Elvish, &Fish, &Powershell, &Zsh])
}

/// Find the specified [`EnvCompleter`]
pub fn completer(&self, name: &str) -> Option<&dyn EnvCompleter> {
self.0.iter().copied().find(|c| c.is(name))
}

/// Collect all [`EnvCompleter::name`]s
pub fn names(&self) -> impl Iterator<Item = &'static str> + 's {
self.0.iter().map(|c| c.name())
}
}

/// Shell-integration for completions
///
/// This will generally be called by [`CompleteEnv`].
///
/// This handles adapting between the shell and [`completer`][crate::dynamic::complete()].
/// A `EnvCompleter` can choose how much of that lives within the registration script or
/// lives in [`EnvCompleter::write_complete`].
pub trait EnvCompleter {
/// Canonical name for this shell
///
/// **Post-conditions:**
/// ```rust,ignore
/// assert!(completer.is(completer.name()));
/// ```
fn name(&self) -> &'static str;
/// Whether the name matches this shell
///
/// This should match [`EnvCompleter::name`] and any alternative names, particularly used by
/// `$SHELL`.
fn is(&self, name: &str) -> bool;
/// Register for completions
///
/// Write the `buf` the logic needed for calling into `<VAR>=<shell> <cmd> --`, passing needed
/// arguments to [`EnvCompleter::write_complete`] through the environment.
fn write_registration(
&self,
var: &str,
name: &str,
bin: &str,
completer: &str,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error>;
/// Complete the given command
///
/// Adapt information from arguments and [`EnvCompleter::write_registration`]-defined env
/// variables to what is needed for [`completer`][crate::dynamic::complete()].
///
/// Write out the [`CompletionCandidate`][crate::dynamic::CompletionCandidate]s in a way the shell will understand.
fn write_complete(
&self,
cmd: &mut clap::Command,
args: Vec<OsString>,
current_dir: Option<&std::path::Path>,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error>;
}
Loading

0 comments on commit c402ec6

Please sign in to comment.