diff --git a/clap_complete/examples/dynamic.rs b/clap_complete/examples/dynamic.rs index ccaf7d8ad2d..6c2211c80fe 100644 --- a/clap_complete/examples/dynamic.rs +++ b/clap_complete/examples/dynamic.rs @@ -16,14 +16,14 @@ fn command() -> clap::Command { .value_parser(["json", "yaml", "toml"]), ) .args_conflicts_with_subcommands(true); - clap_complete::dynamic::shells::CompleteCommand::augment_subcommands(cmd) + clap_complete::dynamic::shells::command::CompleteCommand::augment_subcommands(cmd) } fn main() { let cmd = command(); let matches = cmd.get_matches(); if let Ok(completions) = - clap_complete::dynamic::shells::CompleteCommand::from_arg_matches(&matches) + clap_complete::dynamic::shells::command::CompleteCommand::from_arg_matches(&matches) { completions.complete(&mut command()); } else { diff --git a/clap_complete/examples/exhaustive.rs b/clap_complete/examples/exhaustive.rs index de00da622a1..245621014e3 100644 --- a/clap_complete/examples/exhaustive.rs +++ b/clap_complete/examples/exhaustive.rs @@ -14,7 +14,7 @@ fn main() { #[cfg(feature = "unstable-dynamic")] if let Ok(completions) = - clap_complete::dynamic::shells::CompleteCommand::from_arg_matches(&matches) + clap_complete::dynamic::shells::command::CompleteCommand::from_arg_matches(&matches) { completions.complete(&mut cli()); return; @@ -198,6 +198,6 @@ fn cli() -> clap::Command { ]), ]); #[cfg(feature = "unstable-dynamic")] - let cli = clap_complete::dynamic::shells::CompleteCommand::augment_subcommands(cli); + let cli = clap_complete::dynamic::shells::command::CompleteCommand::augment_subcommands(cli); cli } diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/complete.rs similarity index 94% rename from clap_complete/src/dynamic/completer.rs rename to clap_complete/src/dynamic/complete.rs index 3813e910a0a..cd8cf710d66 100644 --- a/clap_complete/src/dynamic/completer.rs +++ b/clap_complete/src/dynamic/complete.rs @@ -1,31 +1,10 @@ +//! General completion logic for all shells. use std::ffi::OsStr; use std::ffi::OsString; use clap::builder::StyledStr; use clap_lex::OsStrExt as _; -/// Shell-specific completions -pub trait Completer { - /// The recommended file name for the registration code - fn file_name(&self, name: &str) -> String; - /// Register for completions - fn write_registration( - &self, - name: &str, - bin: &str, - completer: &str, - buf: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error>; - /// Complete the command - fn write_complete( - &self, - cmd: &mut clap::Command, - args: Vec, - current_dir: Option<&std::path::Path>, - buf: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error>; -} - /// Complete the command specified pub fn complete( cmd: &mut clap::Command, diff --git a/clap_complete/src/dynamic/mod.rs b/clap_complete/src/dynamic/mod.rs index f7c985704c9..f89a65754fa 100644 --- a/clap_complete/src/dynamic/mod.rs +++ b/clap_complete/src/dynamic/mod.rs @@ -1,7 +1,5 @@ //! Complete commands within shells -mod completer; - +pub mod complete; +pub mod registrar; pub mod shells; - -pub use completer::*; diff --git a/clap_complete/src/dynamic/registrar.rs b/clap_complete/src/dynamic/registrar.rs new file mode 100644 index 00000000000..447abd5ed76 --- /dev/null +++ b/clap_complete/src/dynamic/registrar.rs @@ -0,0 +1,15 @@ +//! Register shell autocomplete file trait. + +/// Register shell autocomplete file. +pub trait Registrar { + /// The recommended file name for the registration code + fn file_name(&self, name: &str) -> String; + /// Register for completions + fn write_registration( + &self, + name: &str, + bin: &str, + completer: &str, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error>; +} diff --git a/clap_complete/src/dynamic/shells/bash.rs b/clap_complete/src/dynamic/shells/bash.rs deleted file mode 100644 index 43c128e5b4e..00000000000 --- a/clap_complete/src/dynamic/shells/bash.rs +++ /dev/null @@ -1,121 +0,0 @@ -use unicode_xid::UnicodeXID as _; - -/// Bash completions -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub struct Bash; - -impl crate::dynamic::Completer for Bash { - fn file_name(&self, name: &str) -> String { - format!("{name}.bash") - } - fn write_registration( - &self, - name: &str, - bin: &str, - completer: &str, - buf: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error> { - let escaped_name = name.replace('-', "_"); - debug_assert!( - escaped_name.chars().all(|c| c.is_xid_continue()), - "`name` must be an identifier, got `{escaped_name}`" - ); - let mut upper_name = escaped_name.clone(); - upper_name.make_ascii_uppercase(); - - let completer = shlex::quote(completer); - - let script = r#" -_clap_complete_NAME() { - export IFS=$'\013' - export _CLAP_COMPLETE_INDEX=${COMP_CWORD} - export _CLAP_COMPLETE_COMP_TYPE=${COMP_TYPE} - if compopt +o nospace 2> /dev/null; then - export _CLAP_COMPLETE_SPACE=false - else - export _CLAP_COMPLETE_SPACE=true - fi - COMPREPLY=( $("COMPLETER" complete --shell bash -- "${COMP_WORDS[@]}") ) - if [[ $? != 0 ]]; then - unset COMPREPLY - elif [[ $SUPPRESS_SPACE == 1 ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then - compopt -o nospace - fi -} -complete -o nospace -o bashdefault -F _clap_complete_NAME BIN -"# - .replace("NAME", &escaped_name) - .replace("BIN", bin) - .replace("COMPLETER", &completer) - .replace("UPPER", &upper_name); - - writeln!(buf, "{script}")?; - Ok(()) - } - fn write_complete( - &self, - cmd: &mut clap::Command, - args: Vec, - current_dir: Option<&std::path::Path>, - buf: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error> { - let index: usize = std::env::var("_CLAP_COMPLETE_INDEX") - .ok() - .and_then(|i| i.parse().ok()) - .unwrap_or_default(); - let _comp_type: CompType = std::env::var("_CLAP_COMPLETE_COMP_TYPE") - .ok() - .and_then(|i| i.parse().ok()) - .unwrap_or_default(); - let _space: Option = std::env::var("_CLAP_COMPLETE_SPACE") - .ok() - .and_then(|i| i.parse().ok()); - let ifs: Option = std::env::var("IFS").ok().and_then(|i| i.parse().ok()); - let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; - - for (i, (completion, _)) in completions.iter().enumerate() { - if i != 0 { - write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; - } - write!(buf, "{}", completion.to_string_lossy())?; - } - Ok(()) - } -} - -/// Type of completion attempted that caused a completion function to be called -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -enum CompType { - /// Normal completion - Normal, - /// List completions after successive tabs - Successive, - /// List alternatives on partial word completion - Alternatives, - /// List completions if the word is not unmodified - Unmodified, - /// Menu completion - Menu, -} - -impl std::str::FromStr for CompType { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "9" => Ok(Self::Normal), - "63" => Ok(Self::Successive), - "33" => Ok(Self::Alternatives), - "64" => Ok(Self::Unmodified), - "37" => Ok(Self::Menu), - _ => Err(format!("unsupported COMP_TYPE `{}`", s)), - } - } -} - -impl Default for CompType { - fn default() -> Self { - Self::Normal - } -} diff --git a/clap_complete/src/dynamic/shells/bash/comp_type.rs b/clap_complete/src/dynamic/shells/bash/comp_type.rs new file mode 100644 index 00000000000..d940dcd30d1 --- /dev/null +++ b/clap_complete/src/dynamic/shells/bash/comp_type.rs @@ -0,0 +1,97 @@ +/// Type of completion attempted that caused a completion function to be called +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum CompType { + /// Normal completion + Normal, + /// List completions after successive tabs + Successive, + /// List alternatives on partial word completion + Alternatives, + /// List completions if the word is not unmodified + Unmodified, + /// Menu completion + Menu, +} + +impl std::str::FromStr for CompType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "9" => Ok(Self::Normal), + "63" => Ok(Self::Successive), + "33" => Ok(Self::Alternatives), + "64" => Ok(Self::Unmodified), + "37" => Ok(Self::Menu), + _ => Err(format!("unsupported COMP_TYPE `{}`", s)), + } + } +} + +impl Default for CompType { + fn default() -> Self { + Self::Normal + } +} + +impl clap::ValueEnum for CompType { + fn value_variants<'a>() -> &'a [Self] { + &[ + Self::Normal, + Self::Successive, + Self::Alternatives, + Self::Unmodified, + Self::Menu, + ] + } + fn to_possible_value(&self) -> ::std::option::Option { + match self { + Self::Normal => { + let value = "9"; + debug_assert_eq!(b'\t'.to_string(), value); + Some( + clap::builder::PossibleValue::new(value) + .alias("normal") + .help("Normal completion"), + ) + } + Self::Successive => { + let value = "63"; + debug_assert_eq!(b'?'.to_string(), value); + Some( + clap::builder::PossibleValue::new(value) + .alias("successive") + .help("List completions after successive tabs"), + ) + } + Self::Alternatives => { + let value = "33"; + debug_assert_eq!(b'!'.to_string(), value); + Some( + clap::builder::PossibleValue::new(value) + .alias("alternatives") + .help("List alternatives on partial word completion"), + ) + } + Self::Unmodified => { + let value = "64"; + debug_assert_eq!(b'@'.to_string(), value); + Some( + clap::builder::PossibleValue::new(value) + .alias("unmodified") + .help("List completions if the word is not unmodified"), + ) + } + Self::Menu => { + let value = "37"; + debug_assert_eq!(b'%'.to_string(), value); + Some( + clap::builder::PossibleValue::new(value) + .alias("menu") + .help("Menu completion"), + ) + } + } + } +} diff --git a/clap_complete/src/dynamic/shells/bash/complete.rs b/clap_complete/src/dynamic/shells/bash/complete.rs new file mode 100644 index 00000000000..851c266cf7b --- /dev/null +++ b/clap_complete/src/dynamic/shells/bash/complete.rs @@ -0,0 +1,107 @@ +use crate::dynamic::complete::complete; + +use super::comp_type::CompType; +use std::ffi::OsString; +use std::io::Write; + +#[derive(clap::Args)] +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub struct BashCompleteArgs { + /// `COMP_CWORD` environment variable from Bash. + /// + /// An index into ${COMP_WORDS} of the word containing the current cursor + /// position. This variable is available only in shell functions invoked by + /// the programmable completion facilities. + /// + /// Quoted from the bash man pages: + /// https://man7.org/linux/man-pages/man1/bash.1.html. + #[arg( + long, + required = true, + value_name = "COMP_CWORD", + hide_short_help = true + )] + index: Option, + + /// `IFS` environment variable from Bash. + /// + /// The Internal Field Separator that is used for word splitting after + /// expansion and to split lines into words with the read builtin command. + /// The default value is ``. + /// + /// Quoted from the bash man pages: + /// https://man7.org/linux/man-pages/man1/bash.1.html. + #[arg(long, hide_short_help = true)] + ifs: Option, + + /// `COMP_TYPE` environment variable from Bash. + /// + /// Set to an integer value corresponding to the type of completion + /// attempted that caused a completion function to be called: TAB, for + /// normal completion, ?, for listing completions after successive tabs, !, + /// for listing alternatives on partial word completion, @, to list + /// completions if the word is not unmodified, or %, for menu completion. + /// This variable is available only in shell functions and external commands + /// invoked by the programmable completion facilities. + /// + /// Quoted from the bash man pages: + /// https://man7.org/linux/man-pages/man1/bash.1.html. + #[arg(long = "type", required = true, hide_short_help = true)] + comp_type: Option, + + /// Disable the `nospace` options from `complete` in Bash. + /// + /// See https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html. + #[arg(long, hide_short_help = true)] + space: bool, + + /// Enable the `nospace` options from `complete` in Bash. + /// + /// See https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html. + #[arg(long, conflicts_with = "space", hide_short_help = true)] + no_space: bool, + + /// `COMP_WORDS` environment variable from Bash. + /// + /// An array variable (see Arrays below) consisting of the individual words + /// in the current command line. The line is split into words as readline + /// would split it, using COMP_WORDBREAKS as described above. This variable + /// is available only in shell functions invoked by the programmable + /// completion facilities. + /// + /// Quoted from the bash man pages: + /// https://man7.org/linux/man-pages/man1/bash.1.html. + #[arg(raw = true, hide_short_help = true)] + comp_words: Vec, +} + +impl BashCompleteArgs { + /// Process the completion request + pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> { + let index = self.index.unwrap_or_default(); + // let _comp_type = self.comp_type.unwrap_or_default(); + // let _space = match (self.space, self.no_space) { + // (true, false) => Some(true), + // (false, true) => Some(false), + // (true, true) => { + // unreachable!("`--space` and `--no-space` set, clap should prevent this") + // } + // (false, false) => None, + // } + // .unwrap(); + let current_dir = std::env::current_dir().ok(); + let completions = complete(cmd, self.comp_words.clone(), index, current_dir.as_deref())?; + + let mut buf = Vec::new(); + for (i, (suggestion, _)) in completions.iter().enumerate() { + if i != 0 { + write!(&mut buf, "{}", self.ifs.as_deref().unwrap_or("\n"))?; + } + write!(&mut buf, "{}", suggestion.to_string_lossy())?; + } + std::io::stdout().write_all(&buf)?; + + Ok(()) + } +} diff --git a/clap_complete/src/dynamic/shells/bash/generate.rs b/clap_complete/src/dynamic/shells/bash/generate.rs new file mode 100644 index 00000000000..6b9e1a5c136 --- /dev/null +++ b/clap_complete/src/dynamic/shells/bash/generate.rs @@ -0,0 +1,67 @@ +use unicode_xid::UnicodeXID; + +use crate::dynamic::registrar::Registrar; + +/// Bash autocomplete file generation. +#[derive(clap::Args, Clone, Debug)] +pub struct BashGenerateArgs {} + +impl BashGenerateArgs { + #[cfg(test)] + pub fn new() -> Self { + Self {} + } +} + +impl Registrar for BashGenerateArgs { + fn file_name(&self, name: &str) -> String { + format!("{name}.bash") + } + + fn write_registration( + &self, + name: &str, + _bin: &str, + completer: &str, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + let escaped_name = name.replace('-', "_"); + debug_assert!( + escaped_name.chars().all(|c| c.is_xid_continue()), + "`name` must be an identifier, got `{escaped_name}`" + ); + let mut upper_name = escaped_name.clone(); + upper_name.make_ascii_uppercase(); + + let completer = shlex::quote(completer); + + let script = r#" +_clap_complete_NAME() { + export IFS=$'\013' + local SUPPRESS_SPACE=0 + if compopt +o nospace 2> /dev/null; then + SUPPRESS_SPACE=1 + fi + if [[ ${SUPPRESS_SPACE} == 1 ]]; then + SPACE_ARG="--no-space" + else + SPACE_ARG="--space" + fi + + COMPREPLY=( $("COMPLETER" complete bash --index ${COMP_CWORD} --type ${COMP_TYPE} ${SPACE_ARG} --ifs="$IFS" -- "${COMP_WORDS[@]}") ) + if [[ $? != 0 ]]; then + unset COMPREPLY + elif [[ $SUPPRESS_SPACE == 1 ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then + compopt -o nospace + fi +} +complete -o nosort -o noquote -o nospace -F _clap_complete_NAME BIN +"# + .replace("NAME", &escaped_name) + .replace("COMPLETER", &completer) + .replace("UPPER", &upper_name); + + writeln!(buf, "{script}")?; + Ok(()) + } +} diff --git a/clap_complete/src/dynamic/shells/bash/mod.rs b/clap_complete/src/dynamic/shells/bash/mod.rs new file mode 100644 index 00000000000..9734fdf5d44 --- /dev/null +++ b/clap_complete/src/dynamic/shells/bash/mod.rs @@ -0,0 +1,5 @@ +//! Bash dynamic autocompletion. + +pub mod comp_type; +pub mod complete; +pub mod generate; diff --git a/clap_complete/src/dynamic/shells/command.rs b/clap_complete/src/dynamic/shells/command.rs new file mode 100644 index 00000000000..6d294daf713 --- /dev/null +++ b/clap_complete/src/dynamic/shells/command.rs @@ -0,0 +1,98 @@ +use super::{bash, fish}; +use crate::dynamic::registrar::Registrar; +use std::io::Write as _; + +#[derive(clap::Subcommand)] +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub enum CompleteCommand { + /// Complete a command for a given shell, to be called from autocomplete + /// scripts primarily. + Complete(CompleteArgs), + /// Generate shell completions for this program + Generate(GenerateArgs), +} + +#[allow(missing_docs)] +#[derive(clap::Args, Clone, Debug)] +pub struct CompleteArgs { + #[command(subcommand)] + command: CompleteShellCommands, +} + +#[derive(clap::Subcommand)] +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub enum CompleteShellCommands { + Bash(bash::complete::BashCompleteArgs), + Fish(fish::complete::FishCompleteArgs), +} + +#[derive(clap::Subcommand)] +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub enum GenerateShellCommands { + Bash(bash::generate::BashGenerateArgs), + Fish(fish::generate::FishGenerateArgs), +} + +#[derive(clap::Args)] +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub struct GenerateArgs { + /// Path to write completion-registration to. + #[arg(long, short = 'o', default_value = "-")] + output: std::path::PathBuf, + + #[command(subcommand)] + command: GenerateShellCommands, +} + +impl CompleteCommand { + /// Process the completion request + pub fn complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible { + self.try_complete(cmd).unwrap_or_else(|e| e.exit()); + std::process::exit(0) + } + + /// Process the completion request + pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> { + debug!("CompleteCommand::try_run: {self:?}"); + match self { + CompleteCommand::Complete(args) => match args.command { + CompleteShellCommands::Bash(ref args) => args.try_complete(cmd), + CompleteShellCommands::Fish(ref args) => args.try_complete(cmd), + }, + CompleteCommand::Generate(args) => { + let mut buf = Vec::new(); + let name = cmd.get_name(); + let bin = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name()); + + if args.output.is_dir() { + return Err(clap::error::Error::raw( + clap::error::ErrorKind::InvalidValue, + "output is a directory", + )); + } + + match args.command { + GenerateShellCommands::Bash(ref args) => { + // TODO Figure out what to pass for complter, just assuming bin now. + args.write_registration(name, bin, bin, &mut buf)? + } + GenerateShellCommands::Fish(ref args) => { + args.write_registration(name, bin, bin, &mut buf)? + } + } + + if args.output == std::path::Path::new("-") { + std::io::stdout().write_all(&buf)?; + } else { + std::fs::write(args.output.as_path(), buf)?; + } + + Ok(()) + } + } + } +} diff --git a/clap_complete/src/dynamic/shells/fish.rs b/clap_complete/src/dynamic/shells/fish.rs deleted file mode 100644 index 9d7e8c6846b..00000000000 --- a/clap_complete/src/dynamic/shells/fish.rs +++ /dev/null @@ -1,46 +0,0 @@ -/// Fish completions -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub struct Fish; - -impl crate::dynamic::Completer for Fish { - fn file_name(&self, name: &str) -> String { - format!("{name}.fish") - } - fn write_registration( - &self, - _name: &str, - bin: &str, - completer: &str, - buf: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error> { - let bin = shlex::quote(bin); - let completer = shlex::quote(completer); - writeln!( - buf, - r#"complete -x -c {bin} -a "("'{completer}'" complete --shell fish -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))""# - ) - } - fn write_complete( - &self, - cmd: &mut clap::Command, - args: Vec, - current_dir: Option<&std::path::Path>, - buf: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error> { - let index = args.len() - 1; - let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; - - for (completion, help) in completions { - write!(buf, "{}", completion.to_string_lossy())?; - if let Some(help) = help { - write!( - buf, - "\t{}", - help.to_string().lines().next().unwrap_or_default() - )?; - } - writeln!(buf)?; - } - Ok(()) - } -} diff --git a/clap_complete/src/dynamic/shells/fish/complete.rs b/clap_complete/src/dynamic/shells/fish/complete.rs new file mode 100644 index 00000000000..ba524d131d5 --- /dev/null +++ b/clap_complete/src/dynamic/shells/fish/complete.rs @@ -0,0 +1,39 @@ +use std::{ffi::OsString, io::Write}; + +use crate::dynamic::complete::complete; + +#[derive(clap::Args)] +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub struct FishCompleteArgs { + #[arg(raw = true, hide_short_help = true)] + args: Vec, +} + +impl FishCompleteArgs { + pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> { + let index = self.args.len() - 1; + let completions = complete( + cmd, + self.args.clone(), + index, + std::env::current_dir().ok().as_deref(), + )?; + + let mut buf = Vec::new(); + for (completion, help) in completions { + write!(buf, "{}", completion.to_string_lossy())?; + if let Some(help) = help { + write!( + buf, + "\t{}", + help.to_string().lines().next().unwrap_or_default() + )?; + } + writeln!(buf)?; + } + std::io::stdout().write_all(&buf)?; + + Ok(()) + } +} diff --git a/clap_complete/src/dynamic/shells/fish/generate.rs b/clap_complete/src/dynamic/shells/fish/generate.rs new file mode 100644 index 00000000000..14b9c50ca61 --- /dev/null +++ b/clap_complete/src/dynamic/shells/fish/generate.rs @@ -0,0 +1,27 @@ +use crate::dynamic::registrar::Registrar; + +#[derive(clap::Args)] +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub struct FishGenerateArgs {} + +impl Registrar for FishGenerateArgs { + fn file_name(&self, name: &str) -> String { + format!("{name}.fish") + } + + fn write_registration( + &self, + _name: &str, + bin: &str, + completer: &str, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + let bin = shlex::quote(bin); + let completer = shlex::quote(completer); + writeln!( + buf, + r#"complete -x -c {bin} -a "("'{completer}'" complete fish -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))""# + ) + } +} diff --git a/clap_complete/src/dynamic/shells/fish/mod.rs b/clap_complete/src/dynamic/shells/fish/mod.rs new file mode 100644 index 00000000000..b70f8cd1dc8 --- /dev/null +++ b/clap_complete/src/dynamic/shells/fish/mod.rs @@ -0,0 +1,4 @@ +//! Fish dynamic autocompletion. + +pub mod complete; +pub mod generate; diff --git a/clap_complete/src/dynamic/shells/mod.rs b/clap_complete/src/dynamic/shells/mod.rs index 54d23a3d4fb..55775c0d267 100644 --- a/clap_complete/src/dynamic/shells/mod.rs +++ b/clap_complete/src/dynamic/shells/mod.rs @@ -1,82 +1,7 @@ //! Shell support -mod bash; -mod fish; -mod shell; +pub mod bash; +pub mod fish; -pub use bash::*; -pub use fish::*; -pub use shell::*; - -use std::ffi::OsString; -use std::io::Write as _; - -use crate::dynamic::Completer as _; - -#[derive(clap::Subcommand)] #[allow(missing_docs)] -#[derive(Clone, Debug)] -pub enum CompleteCommand { - /// Register shell completions for this program - #[command(hide = true)] - Complete(CompleteArgs), -} - -#[derive(clap::Args)] -#[command(arg_required_else_help = true)] -#[command(group = clap::ArgGroup::new("complete").multiple(true).conflicts_with("register"))] -#[allow(missing_docs)] -#[derive(Clone, Debug)] -pub struct CompleteArgs { - /// Specify shell to complete for - #[arg(long)] - shell: Shell, - - /// Path to write completion-registration to - #[arg(long, required = true)] - register: Option, - - #[arg(raw = true, hide_short_help = true, group = "complete")] - comp_words: Vec, -} - -impl CompleteCommand { - /// Process the completion request - pub fn complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible { - self.try_complete(cmd).unwrap_or_else(|e| e.exit()); - std::process::exit(0) - } - - /// Process the completion request - pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> { - debug!("CompleteCommand::try_complete: {self:?}"); - let CompleteCommand::Complete(args) = self; - if let Some(out_path) = args.register.as_deref() { - let mut buf = Vec::new(); - let name = cmd.get_name(); - let bin = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name()); - args.shell.write_registration(name, bin, bin, &mut buf)?; - if out_path == std::path::Path::new("-") { - std::io::stdout().write_all(&buf)?; - } else if out_path.is_dir() { - let out_path = out_path.join(args.shell.file_name(name)); - std::fs::write(out_path, buf)?; - } else { - std::fs::write(out_path, buf)?; - } - } else { - let current_dir = std::env::current_dir().ok(); - - let mut buf = Vec::new(); - args.shell.write_complete( - cmd, - args.comp_words.clone(), - current_dir.as_deref(), - &mut buf, - )?; - std::io::stdout().write_all(&buf)?; - } - - Ok(()) - } -} +pub mod command; diff --git a/clap_complete/src/dynamic/shells/shell.rs b/clap_complete/src/dynamic/shells/shell.rs deleted file mode 100644 index a9f48cee935..00000000000 --- a/clap_complete/src/dynamic/shells/shell.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::fmt::Display; -use std::str::FromStr; - -use clap::builder::PossibleValue; -use clap::ValueEnum; - -/// Shell with auto-generated completion script available. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -#[non_exhaustive] -pub enum Shell { - /// Bourne Again SHell (bash) - Bash, - /// Friendly Interactive SHell (fish) - Fish, -} - -impl Display for Shell { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.to_possible_value() - .expect("no values are skipped") - .get_name() - .fmt(f) - } -} - -impl FromStr for Shell { - type Err = String; - - fn from_str(s: &str) -> Result { - for variant in Self::value_variants() { - if variant.to_possible_value().unwrap().matches(s, false) { - return Ok(*variant); - } - } - Err(format!("invalid variant: {s}")) - } -} - -// Hand-rolled so it can work even when `derive` feature is disabled -impl ValueEnum for Shell { - fn value_variants<'a>() -> &'a [Self] { - &[Shell::Bash, Shell::Fish] - } - - fn to_possible_value<'a>(&self) -> Option { - Some(match self { - Shell::Bash => PossibleValue::new("bash"), - Shell::Fish => PossibleValue::new("fish"), - }) - } -} - -impl Shell { - fn completer(&self) -> &dyn crate::dynamic::Completer { - match self { - Self::Bash => &super::Bash, - Self::Fish => &super::Fish, - } - } -} - -impl crate::dynamic::Completer for Shell { - fn file_name(&self, name: &str) -> String { - self.completer().file_name(name) - } - fn write_registration( - &self, - name: &str, - bin: &str, - completer: &str, - buf: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error> { - self.completer() - .write_registration(name, bin, completer, buf) - } - fn write_complete( - &self, - cmd: &mut clap::Command, - args: Vec, - current_dir: Option<&std::path::Path>, - buf: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error> { - self.completer().write_complete(cmd, args, current_dir, buf) - } -} diff --git a/clap_complete/tests/testsuite/bash.rs b/clap_complete/tests/testsuite/bash.rs index b9692a421c9..2e7379306bb 100644 --- a/clap_complete/tests/testsuite/bash.rs +++ b/clap_complete/tests/testsuite/bash.rs @@ -99,14 +99,14 @@ fn value_terminator() { #[cfg(feature = "unstable-dynamic")] #[test] fn register_minimal() { - use clap_complete::dynamic::Completer; + use clap_complete::dynamic::registrar::Registrar; let name = "my-app"; let bin = name; let completer = name; let mut buf = Vec::new(); - clap_complete::dynamic::shells::Bash + clap_complete::dynamic::shells::bash::generate::BashGenerateArgs::new() .write_registration(name, bin, completer, &mut buf) .unwrap(); snapbox::Assert::new() diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index 7389713874f..1886aa4eb56 100644 --- a/clap_complete/tests/testsuite/dynamic.rs +++ b/clap_complete/tests/testsuite/dynamic.rs @@ -142,7 +142,7 @@ fn complete(cmd: &mut Command, args: impl AsRef, current_dir: Option<&Path> arg_index = args.len() - 1; } - clap_complete::dynamic::complete(cmd, args, arg_index, current_dir) + clap_complete::dynamic::complete::complete(cmd, args, arg_index, current_dir) .unwrap() .into_iter() .map(|(compl, help)| {