diff --git a/clap_complete/src/dynamic/shells/bash.rs b/clap_complete/src/dynamic/shells/bash.rs deleted file mode 100644 index 012dcba4..00000000 --- a/clap_complete/src/dynamic/shells/bash.rs +++ /dev/null @@ -1,122 +0,0 @@ -use unicode_xid::UnicodeXID as _; - -/// A [`ShellCompleter`][crate::dynamic::shells::ShellCompleter] for Bash -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub struct Bash; - -impl crate::dynamic::shells::ShellCompleter 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::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(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, candidate) in completions.iter().enumerate() { - if i != 0 { - write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; - } - write!(buf, "{}", candidate.get_content().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/command.rs b/clap_complete/src/dynamic/shells/command.rs new file mode 100644 index 00000000..e442310e --- /dev/null +++ b/clap_complete/src/dynamic/shells/command.rs @@ -0,0 +1,626 @@ +use std::ffi::OsString; +use std::io::Write as _; + +use unicode_xid::UnicodeXID as _; + +use super::Shell; + +/// A completion subcommand to add to your CLI +/// +/// If you aren't using a subcommand, you can annotate a field with this type as `#[command(subcommand)]`. +/// +/// If you are using subcommands, see [`CompleteArgs`]. +/// +/// **Warning:** `stdout` should not be written to before [`CompleteCommand::complete`] has had a +/// chance to run. +/// +/// # Examples +/// +/// To integrate completions into an application without subcommands: +/// ```no_run +/// // src/main.rs +/// use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; +/// use clap_complete::dynamic::CompleteCommand; +/// +/// #[derive(Parser, Debug)] +/// #[clap(name = "dynamic", about = "A dynamic command line tool")] +/// struct Cli { +/// /// The subcommand to run complete +/// #[command(subcommand)] +/// complete: Option, +/// /// Input file path +/// #[clap(short, long, value_hint = clap::ValueHint::FilePath)] +/// input: Option, +/// /// Output format +/// #[clap(short = 'F', long, value_parser = ["json", "yaml", "toml"])] +/// format: Option, +/// } +/// +/// fn main() { +/// let cli = Cli::parse(); +/// if let Some(completions) = cli.complete { +/// completions.complete(&mut Cli::command()); +/// } +/// +/// // normal logic continues... +/// } +///``` +/// +/// To source your completions: +/// +/// Bash +/// ```bash +/// echo "source <(your_program complete --shell bash --register -)" >> ~/.bashrc +/// ``` +/// +/// Elvish +/// ```elvish +/// echo "eval (your_program complete --shell elvish --register -)" >> ~/.elvish/rc.elv +/// ``` +/// +/// Fish +/// ```fish +/// echo "source (your_program complete --shell fish --register - | psub)" >> ~/.config/fish/config.fish +/// ``` +/// +/// Powershell +/// ```powershell +/// echo "your_program complete --shell powershell --register - | Invoke-Expression" >> $PROFILE +/// ``` +/// +/// Zsh +/// ```zsh +/// echo "source <(your_program complete --shell zsh --register -)" >> ~/.zshrc +/// ``` +#[derive(clap::Subcommand)] +#[allow(missing_docs)] +#[derive(Clone, Debug)] +#[command(about = None, long_about = None)] +pub enum CompleteCommand { + /// Register shell completions for this program + #[command(hide = true)] + Complete(CompleteArgs), +} + +impl CompleteCommand { + /// 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, 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 + /// + /// **Warning:** `stdout` should not be written to before or after this has run. + pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> { + debug!("CompleteCommand::try_complete: {self:?}"); + let CompleteCommand::Complete(args) = self; + args.try_complete(cmd) + } +} + +/// A completion subcommand to add to your CLI +/// +/// If you are using subcommands, add a `Complete(CompleteArgs)` variant. +/// +/// If you aren't using subcommands, generally you will want [`CompleteCommand`]. +/// +/// **Warning:** `stdout` should not be written to before [`CompleteArgs::complete`] has had a +/// chance to run. +/// +/// # Examples +/// +/// To integrate completions into an application without subcommands: +/// ```no_run +/// // src/main.rs +/// use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; +/// use clap_complete::dynamic::CompleteArgs; +/// +/// #[derive(Parser, Debug)] +/// #[clap(name = "dynamic", about = "A dynamic command line tool")] +/// struct Cli { +/// #[command(subcommand)] +/// complete: Command, +/// } +/// +/// #[derive(Subcommand, Debug)] +/// enum Command { +/// Complete(CompleteArgs), +/// Print, +/// } +/// +/// fn main() { +/// let cli = Cli::parse(); +/// match cli.complete { +/// Command::Complete(completions) => { +/// completions.complete(&mut Cli::command()); +/// }, +/// Command::Print => { +/// println!("Hello world!"); +/// } +/// } +/// } +///``` +/// +/// To source your completions: +/// +/// Bash +/// ```bash +/// echo "source <(your_program complete --shell bash)" >> ~/.bashrc +/// ``` +/// +/// Elvish +/// ```elvish +/// echo "eval (your_program complete --shell elvish)" >> ~/.elvish/rc.elv +/// ``` +/// +/// Fish +/// ```fish +/// echo "source (your_program complete --shell fish | psub)" >> ~/.config/fish/config.fish +/// ``` +/// +/// Powershell +/// ```powershell +/// echo "your_program complete --shell powershell | Invoke-Expression" >> $PROFILE +/// ``` +/// +/// Zsh +/// ```zsh +/// echo "source <(your_program complete --shell zsh)" >> ~/.zshrc +/// ``` +#[derive(clap::Args, Clone, Debug)] +#[command(about = None, long_about = None)] +pub struct CompleteArgs { + /// Path to write completion-registration to + #[arg(long, value_name = "PATH")] + register: Option, + + #[arg( + raw = true, + value_name = "ARG", + hide = true, + conflicts_with = "register" + )] + comp_words: Option>, + + /// Specify shell to complete for + #[arg(long, value_name = "NAME")] + shell: Option, +} + +impl CompleteArgs { + /// 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, 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 + /// + /// **Warning:** `stdout` should not be written to before or after this has run. + pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> { + debug!("CompleteCommand::try_complete: {self:?}"); + + let shell = self + .shell + .or_else(|| Shell::from_env()) + .unwrap_or(Shell::Bash); + + if let Some(comp_words) = self.comp_words.as_ref() { + let current_dir = std::env::current_dir().ok(); + + let mut buf = Vec::new(); + shell.write_complete(cmd, comp_words.clone(), current_dir.as_deref(), &mut buf)?; + std::io::stdout().write_all(&buf)?; + } else { + let out_path = self + .register + .as_deref() + .unwrap_or(std::path::Path::new("-")); + 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(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(shell.file_name(name)); + std::fs::write(out_path, buf)?; + } else { + std::fs::write(out_path, buf)?; + } + } + + Ok(()) + } +} + +/// Shell-integration for completions +/// +/// This will generally be called by [`CompleteCommand`] or [`CompleteArgs`]. +/// +/// This handles adapting between the shell and [`completer`][crate::dynamic::complete()]. +/// A `ShellCompleter` can choose how much of that lives within the registration script and or +/// lives in [`ShellCompleter::write_complete`]. +pub trait ShellCompleter { + /// The recommended file name for the registration code + fn file_name(&self, name: &str) -> String; + /// Register for completions + /// + /// Write the `buf` the logic needed for calling into ` complete`, passing needed + /// arguments to [`ShellCompleter::write_complete`] through the environment. + fn write_registration( + &self, + 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 [`ShellCompleter::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, + current_dir: Option<&std::path::Path>, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error>; +} + +impl ShellCompleter for super::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::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(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, candidate) in completions.iter().enumerate() { + if i != 0 { + write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; + } + write!(buf, "{}", candidate.get_content().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 + } +} +impl ShellCompleter for super::Elvish { + fn file_name(&self, name: &str) -> String { + format!("{name}.elv") + } + fn write_registration( + &self, + _name: &str, + bin: &str, + completer: &str, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + let bin = shlex::try_quote(bin).unwrap_or(std::borrow::Cow::Borrowed(bin)); + let completer = + shlex::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(completer)); + + let script = r#" +set edit:completion:arg-completer[BIN] = { |@words| + set E:_CLAP_IFS = "\n" + + var index = (count $words) + set index = (- $index 1) + set E:_CLAP_COMPLETE_INDEX = (to-string $index) + + put (COMPLETER complete --shell elvish -- $@words) | to-lines +} +"# + .replace("COMPLETER", &completer) + .replace("BIN", &bin); + + 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 ifs: Option = std::env::var("_CLAP_IFS").ok().and_then(|i| i.parse().ok()); + let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; + + for (i, candidate) in completions.iter().enumerate() { + if i != 0 { + write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; + } + write!(buf, "{}", candidate.get_content().to_string_lossy())?; + } + Ok(()) + } +} + +impl ShellCompleter for super::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::try_quote(bin).unwrap_or(std::borrow::Cow::Borrowed(bin)); + let completer = + shlex::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(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 candidate in completions { + write!(buf, "{}", candidate.get_content().to_string_lossy())?; + if let Some(help) = candidate.get_help() { + write!( + buf, + "\t{}", + help.to_string().lines().next().unwrap_or_default() + )?; + } + writeln!(buf)?; + } + Ok(()) + } +} + +impl ShellCompleter for super::Powershell { + fn file_name(&self, name: &str) -> String { + format!("{name}.ps1") + } + + fn write_registration( + &self, + _name: &str, + bin: &str, + completer: &str, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + let bin = shlex::try_quote(bin).unwrap_or(std::borrow::Cow::Borrowed(bin)); + let completer = + shlex::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(completer)); + + writeln!( + buf, + r#" +Register-ArgumentCompleter -Native -CommandName {bin} -ScriptBlock {{ + param($wordToComplete, $commandAst, $cursorPosition) + + $results = Invoke-Expression "&{completer} complete --shell powershell -- $($commandAst.ToString())"; + $results | ForEach-Object {{ + $split = $_.Split("`t"); + $cmd = $split[0]; + + if ($split.Length -eq 2) {{ + $help = $split[1]; + }} + else {{ + $help = $split[0]; + }} + + [System.Management.Automation.CompletionResult]::new($cmd, $cmd, 'ParameterValue', $help) + }} +}}; + "# + ) + } + + 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 candidate in completions { + write!(buf, "{}", candidate.get_content().to_string_lossy())?; + if let Some(help) = candidate.get_help() { + write!( + buf, + "\t{}", + help.to_string().lines().next().unwrap_or_default() + )?; + } + writeln!(buf)?; + } + Ok(()) + } +} + +impl ShellCompleter for super::Zsh { + fn file_name(&self, name: &str) -> String { + format!("{name}.zsh") + } + fn write_registration( + &self, + _name: &str, + bin: &str, + completer: &str, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + let bin = shlex::try_quote(bin).unwrap_or(std::borrow::Cow::Borrowed(bin)); + let completer = + shlex::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(completer)); + + let script = r#"#compdef BIN +function _clap_dynamic_completer() { + export _CLAP_COMPLETE_INDEX=$(expr $CURRENT - 1) + export _CLAP_IFS=$'\n' + + local completions=("${(@f)$(COMPLETER complete --shell zsh -- ${words} 2>/dev/null)}") + + if [[ -n $completions ]]; then + compadd -a completions + fi +} + +compdef _clap_dynamic_completer BIN"# + .replace("COMPLETER", &completer) + .replace("BIN", &bin); + + 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 ifs: Option = std::env::var("_CLAP_IFS").ok().and_then(|i| i.parse().ok()); + + // If the current word is empty, add an empty string to the args + let mut args = args.clone(); + if args.len() == index { + args.push("".into()); + } + let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; + + for (i, candidate) in completions.iter().enumerate() { + if i != 0 { + write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; + } + write!(buf, "{}", candidate.get_content().to_string_lossy())?; + } + Ok(()) + } +} diff --git a/clap_complete/src/dynamic/shells/elvish.rs b/clap_complete/src/dynamic/shells/elvish.rs deleted file mode 100644 index cd0f7477..00000000 --- a/clap_complete/src/dynamic/shells/elvish.rs +++ /dev/null @@ -1,59 +0,0 @@ -/// A [`ShellCompleter`][crate::dynamic::shells::ShellCompleter] for Elvish -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub struct Elvish; - -impl crate::dynamic::shells::ShellCompleter for Elvish { - fn file_name(&self, name: &str) -> String { - format!("{name}.elv") - } - fn write_registration( - &self, - _name: &str, - bin: &str, - completer: &str, - buf: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error> { - let bin = shlex::try_quote(bin).unwrap_or(std::borrow::Cow::Borrowed(bin)); - let completer = - shlex::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(completer)); - - let script = r#" -set edit:completion:arg-completer[BIN] = { |@words| - set E:_CLAP_IFS = "\n" - - var index = (count $words) - set index = (- $index 1) - set E:_CLAP_COMPLETE_INDEX = (to-string $index) - - put (COMPLETER complete --shell elvish -- $@words) | to-lines -} -"# - .replace("COMPLETER", &completer) - .replace("BIN", &bin); - - 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 ifs: Option = std::env::var("_CLAP_IFS").ok().and_then(|i| i.parse().ok()); - let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; - - for (i, candidate) in completions.iter().enumerate() { - if i != 0 { - write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; - } - write!(buf, "{}", candidate.get_content().to_string_lossy())?; - } - Ok(()) - } -} diff --git a/clap_complete/src/dynamic/shells/fish.rs b/clap_complete/src/dynamic/shells/fish.rs deleted file mode 100644 index 59a22299..00000000 --- a/clap_complete/src/dynamic/shells/fish.rs +++ /dev/null @@ -1,48 +0,0 @@ -/// A [`ShellCompleter`][crate::dynamic::shells::ShellCompleter] for Fish -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub struct Fish; - -impl crate::dynamic::shells::ShellCompleter 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::try_quote(bin).unwrap_or(std::borrow::Cow::Borrowed(bin)); - let completer = - shlex::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(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 candidate in completions { - write!(buf, "{}", candidate.get_content().to_string_lossy())?; - if let Some(help) = candidate.get_help() { - write!( - buf, - "\t{}", - help.to_string().lines().next().unwrap_or_default() - )?; - } - writeln!(buf)?; - } - Ok(()) - } -} diff --git a/clap_complete/src/dynamic/shells/mod.rs b/clap_complete/src/dynamic/shells/mod.rs index fa23f203..e876c45d 100644 --- a/clap_complete/src/dynamic/shells/mod.rs +++ b/clap_complete/src/dynamic/shells/mod.rs @@ -1,291 +1,192 @@ //! Shell completion support, see [`CompleteCommand`] for more details -mod bash; -mod elvish; -mod fish; -mod powershell; -mod shell; -mod zsh; +mod command; -pub use bash::*; -pub use elvish::*; -pub use fish::*; -pub use powershell::*; -pub use shell::*; -pub use zsh::*; +pub use command::*; -use std::ffi::OsString; -use std::io::Write as _; +use std::fmt::Display; +use std::str::FromStr; -/// A completion subcommand to add to your CLI -/// -/// If you aren't using a subcommand, you can annotate a field with this type as `#[command(subcommand)]`. -/// -/// If you are using subcommands, see [`CompleteArgs`]. -/// -/// **Warning:** `stdout` should not be written to before [`CompleteCommand::complete`] has had a -/// chance to run. -/// -/// # Examples -/// -/// To integrate completions into an application without subcommands: -/// ```no_run -/// // src/main.rs -/// use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; -/// use clap_complete::dynamic::CompleteCommand; -/// -/// #[derive(Parser, Debug)] -/// #[clap(name = "dynamic", about = "A dynamic command line tool")] -/// struct Cli { -/// /// The subcommand to run complete -/// #[command(subcommand)] -/// complete: Option, -/// /// Input file path -/// #[clap(short, long, value_hint = clap::ValueHint::FilePath)] -/// input: Option, -/// /// Output format -/// #[clap(short = 'F', long, value_parser = ["json", "yaml", "toml"])] -/// format: Option, -/// } -/// -/// fn main() { -/// let cli = Cli::parse(); -/// if let Some(completions) = cli.complete { -/// completions.complete(&mut Cli::command()); -/// } -/// -/// // normal logic continues... -/// } -///``` -/// -/// To source your completions: -/// -/// Bash -/// ```bash -/// echo "source <(your_program complete --shell bash --register -)" >> ~/.bashrc -/// ``` -/// -/// Elvish -/// ```elvish -/// echo "eval (your_program complete --shell elvish --register -)" >> ~/.elvish/rc.elv -/// ``` -/// -/// Fish -/// ```fish -/// echo "source (your_program complete --shell fish --register - | psub)" >> ~/.config/fish/config.fish -/// ``` -/// -/// Powershell -/// ```powershell -/// echo "your_program complete --shell powershell --register - | Invoke-Expression" >> $PROFILE -/// ``` -/// -/// Zsh -/// ```zsh -/// echo "source <(your_program complete --shell zsh --register -)" >> ~/.zshrc -/// ``` -#[derive(clap::Subcommand)] -#[allow(missing_docs)] -#[derive(Clone, Debug)] -#[command(about = None, long_about = None)] -pub enum CompleteCommand { - /// Register shell completions for this program - #[command(hide = true)] - Complete(CompleteArgs), +use clap::builder::PossibleValue; +use clap::ValueEnum; + +/// Completion support for built-in shells +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +pub enum Shell { + /// Bourne Again `SHell` (bash) + Bash, + /// Elvish shell + Elvish, + /// Friendly Interactive `SHell` (fish) + Fish, + /// `PowerShell` + Powershell, + /// Z `SHell` (zsh) + Zsh, } -impl CompleteCommand { - /// Process the completion request and exit +impl Shell { + /// Parse a shell from a path to the executable for the shell /// - /// **Warning:** `stdout` should not be written to before this has had a - /// chance to run. - 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) + /// # Examples + /// + /// ``` + /// use clap_complete::shells::Shell; + /// + /// assert_eq!(Shell::from_shell_path("/bin/bash"), Some(Shell::Bash)); + /// assert_eq!(Shell::from_shell_path("/usr/bin/zsh"), Some(Shell::Zsh)); + /// assert_eq!(Shell::from_shell_path("/opt/my_custom_shell"), None); + /// ``` + pub fn from_shell_path(path: impl AsRef) -> Option { + parse_shell_from_path(path.as_ref()) } - /// Process the completion request + /// Determine the user's current shell from the environment /// - /// **Warning:** `stdout` should not be written to before or after this has run. - pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> { - debug!("CompleteCommand::try_complete: {self:?}"); - let CompleteCommand::Complete(args) = self; - args.try_complete(cmd) - } -} - -/// A completion subcommand to add to your CLI -/// -/// If you are using subcommands, add a `Complete(CompleteArgs)` variant. -/// -/// If you aren't using subcommands, generally you will want [`CompleteCommand`]. -/// -/// **Warning:** `stdout` should not be written to before [`CompleteArgs::complete`] has had a -/// chance to run. -/// -/// # Examples -/// -/// To integrate completions into an application without subcommands: -/// ```no_run -/// // src/main.rs -/// use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; -/// use clap_complete::dynamic::CompleteArgs; -/// -/// #[derive(Parser, Debug)] -/// #[clap(name = "dynamic", about = "A dynamic command line tool")] -/// struct Cli { -/// #[command(subcommand)] -/// complete: Command, -/// } -/// -/// #[derive(Subcommand, Debug)] -/// enum Command { -/// Complete(CompleteArgs), -/// Print, -/// } -/// -/// fn main() { -/// let cli = Cli::parse(); -/// match cli.complete { -/// Command::Complete(completions) => { -/// completions.complete(&mut Cli::command()); -/// }, -/// Command::Print => { -/// println!("Hello world!"); -/// } -/// } -/// } -///``` -/// -/// To source your completions: -/// -/// Bash -/// ```bash -/// echo "source <(your_program complete --shell bash)" >> ~/.bashrc -/// ``` -/// -/// Elvish -/// ```elvish -/// echo "eval (your_program complete --shell elvish)" >> ~/.elvish/rc.elv -/// ``` -/// -/// Fish -/// ```fish -/// echo "source (your_program complete --shell fish | psub)" >> ~/.config/fish/config.fish -/// ``` -/// -/// Powershell -/// ```powershell -/// echo "your_program complete --shell powershell | Invoke-Expression" >> $PROFILE -/// ``` -/// -/// Zsh -/// ```zsh -/// echo "source <(your_program complete --shell zsh)" >> ~/.zshrc -/// ``` -#[derive(clap::Args, Clone, Debug)] -#[command(about = None, long_about = None)] -pub struct CompleteArgs { - /// Path to write completion-registration to - #[arg(long, value_name = "PATH")] - register: Option, - - #[arg( - raw = true, - value_name = "ARG", - hide = true, - conflicts_with = "register" - )] - comp_words: Option>, - - /// Specify shell to complete for - #[arg(long, value_name = "NAME")] - shell: Option, -} - -impl CompleteArgs { - /// Process the completion request and exit + /// This will read the SHELL environment variable and try to determine which shell is in use + /// from that. /// - /// **Warning:** `stdout` should not be written to before this has had a - /// chance to run. - 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 + /// If SHELL is not set, then on windows, it will default to powershell, and on + /// other operating systems it will return `None`. /// - /// **Warning:** `stdout` should not be written to before or after this has run. - pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> { - debug!("CompleteCommand::try_complete: {self:?}"); - - let shell = self - .shell - .or_else(|| Shell::from_env()) - .unwrap_or(Shell::Bash); - - if let Some(comp_words) = self.comp_words.as_ref() { - let current_dir = std::env::current_dir().ok(); - - let mut buf = Vec::new(); - shell.write_complete(cmd, comp_words.clone(), current_dir.as_deref(), &mut buf)?; - std::io::stdout().write_all(&buf)?; + /// If SHELL is set, but contains a value that doesn't correspond to one of the supported shell + /// types, then return `None`. + /// + /// # Example: + /// + /// ```no_run + /// # use clap::Command; + /// use clap_complete::{generate, shells::Shell}; + /// # fn build_cli() -> Command { + /// # Command::new("compl") + /// # } + /// let shell = Shell::from_env(); + /// println!("{shell:?}"); + /// ``` + pub fn from_env() -> Option { + if let Some(env_shell) = std::env::var_os("SHELL") { + Shell::from_shell_path(env_shell) + } else if cfg!(windows) { + Some(Shell::Powershell) } else { - let out_path = self - .register - .as_deref() - .unwrap_or(std::path::Path::new("-")); - let name = cmd.get_name(); - let bin = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name()); + None + } + } +} - let mut buf = Vec::new(); - 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(shell.file_name(name)); - std::fs::write(out_path, buf)?; - } else { - std::fs::write(out_path, buf)?; +// use a separate function to avoid having to monomorphize the entire function due +// to from_shell_path being generic +fn parse_shell_from_path(path: &std::path::Path) -> Option { + let name = path.file_stem()?.to_str()?; + match name { + "bash" => Some(Shell::Bash), + "elvish" => Some(Shell::Elvish), + "fish" => Some(Shell::Fish), + "powershell" | "powershell_ise" => Some(Shell::Powershell), + "zsh" => Some(Shell::Zsh), + _ => None, + } +} + +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); } } - - Ok(()) + Err(format!("invalid variant: {s}")) } } -/// Shell-integration for completions -/// -/// This will generally be called by [`CompleteCommand`] or [`CompleteArgs`]. -/// -/// This handles adapting between the shell and [`completer`][crate::dynamic::complete()]. -/// A `ShellCompleter` can choose how much of that lives within the registration script and or -/// lives in [`ShellCompleter::write_complete`]. -pub trait ShellCompleter { - /// The recommended file name for the registration code - fn file_name(&self, name: &str) -> String; - /// Register for completions - /// - /// Write the `buf` the logic needed for calling into ` complete`, passing needed - /// arguments to [`ShellCompleter::write_complete`] through the environment. +// 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::Elvish, + Shell::Fish, + Shell::Powershell, + Shell::Zsh, + ] + } + + fn to_possible_value(&self) -> Option { + Some(match self { + Shell::Bash => PossibleValue::new("bash"), + Shell::Elvish => PossibleValue::new("elvish"), + Shell::Fish => PossibleValue::new("fish"), + Shell::Powershell => PossibleValue::new("powershell"), + Shell::Zsh => PossibleValue::new("zsh"), + }) + } +} + +impl Shell { + fn completer(&self) -> &dyn ShellCompleter { + match self { + Self::Bash => &Bash, + Self::Elvish => &Elvish, + Self::Fish => &Fish, + Self::Powershell => &Powershell, + Self::Zsh => &Zsh, + } + } +} + +impl ShellCompleter 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>; - /// Complete the given command - /// - /// Adapt information from arguments and [`ShellCompleter::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. + ) -> Result<(), std::io::Error> { + self.completer() + .write_registration(name, bin, completer, buf) + } fn write_complete( &self, cmd: &mut clap::Command, - args: Vec, + args: Vec, current_dir: Option<&std::path::Path>, buf: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error>; + ) -> Result<(), std::io::Error> { + self.completer().write_complete(cmd, args, current_dir, buf) + } } + +/// A [`ShellCompleter`] for Bash +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Bash; + +/// A [`ShellCompleter`] for Elvish +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Elvish; + +/// A [`ShellCompleter`] for Fish +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Fish; + +/// A [`ShellCompleter`] for Powershell +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Powershell; + +/// A [`ShellCompleter`] for zsh +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Zsh; diff --git a/clap_complete/src/dynamic/shells/powershell.rs b/clap_complete/src/dynamic/shells/powershell.rs deleted file mode 100644 index 32cd0098..00000000 --- a/clap_complete/src/dynamic/shells/powershell.rs +++ /dev/null @@ -1,69 +0,0 @@ -/// Completion support for Powershell -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub struct Powershell; - -impl crate::dynamic::shells::ShellCompleter for Powershell { - fn file_name(&self, name: &str) -> String { - format!("{name}.ps1") - } - - fn write_registration( - &self, - _name: &str, - bin: &str, - completer: &str, - buf: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error> { - let bin = shlex::try_quote(bin).unwrap_or(std::borrow::Cow::Borrowed(bin)); - let completer = - shlex::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(completer)); - - writeln!( - buf, - r#" -Register-ArgumentCompleter -Native -CommandName {bin} -ScriptBlock {{ - param($wordToComplete, $commandAst, $cursorPosition) - - $results = Invoke-Expression "&{completer} complete --shell powershell -- $($commandAst.ToString())"; - $results | ForEach-Object {{ - $split = $_.Split("`t"); - $cmd = $split[0]; - - if ($split.Length -eq 2) {{ - $help = $split[1]; - }} - else {{ - $help = $split[0]; - }} - - [System.Management.Automation.CompletionResult]::new($cmd, $cmd, 'ParameterValue', $help) - }} -}}; - "# - ) - } - - 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 candidate in completions { - write!(buf, "{}", candidate.get_content().to_string_lossy())?; - if let Some(help) = candidate.get_help() { - write!( - buf, - "\t{}", - help.to_string().lines().next().unwrap_or_default() - )?; - } - writeln!(buf)?; - } - Ok(()) - } -} diff --git a/clap_complete/src/dynamic/shells/shell.rs b/clap_complete/src/dynamic/shells/shell.rs deleted file mode 100644 index 7b2598ba..00000000 --- a/clap_complete/src/dynamic/shells/shell.rs +++ /dev/null @@ -1,166 +0,0 @@ -use std::fmt::Display; -use std::str::FromStr; - -use clap::builder::PossibleValue; -use clap::ValueEnum; - -/// Completion support for built-in shells -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -#[non_exhaustive] -pub enum Shell { - /// Bourne Again `SHell` (bash) - Bash, - /// Elvish shell - Elvish, - /// Friendly Interactive `SHell` (fish) - Fish, - /// `PowerShell` - Powershell, - /// Z `SHell` (zsh) - Zsh, -} - -impl Shell { - /// Parse a shell from a path to the executable for the shell - /// - /// # Examples - /// - /// ``` - /// use clap_complete::shells::Shell; - /// - /// assert_eq!(Shell::from_shell_path("/bin/bash"), Some(Shell::Bash)); - /// assert_eq!(Shell::from_shell_path("/usr/bin/zsh"), Some(Shell::Zsh)); - /// assert_eq!(Shell::from_shell_path("/opt/my_custom_shell"), None); - /// ``` - pub fn from_shell_path(path: impl AsRef) -> Option { - parse_shell_from_path(path.as_ref()) - } - - /// Determine the user's current shell from the environment - /// - /// This will read the SHELL environment variable and try to determine which shell is in use - /// from that. - /// - /// If SHELL is not set, then on windows, it will default to powershell, and on - /// other operating systems it will return `None`. - /// - /// If SHELL is set, but contains a value that doesn't correspond to one of the supported shell - /// types, then return `None`. - /// - /// # Example: - /// - /// ```no_run - /// # use clap::Command; - /// use clap_complete::{generate, shells::Shell}; - /// # fn build_cli() -> Command { - /// # Command::new("compl") - /// # } - /// let shell = Shell::from_env(); - /// println!("{shell:?}"); - /// ``` - pub fn from_env() -> Option { - if let Some(env_shell) = std::env::var_os("SHELL") { - Shell::from_shell_path(env_shell) - } else if cfg!(windows) { - Some(Shell::Powershell) - } else { - None - } - } -} - -// use a separate function to avoid having to monomorphize the entire function due -// to from_shell_path being generic -fn parse_shell_from_path(path: &std::path::Path) -> Option { - let name = path.file_stem()?.to_str()?; - match name { - "bash" => Some(Shell::Bash), - "elvish" => Some(Shell::Elvish), - "fish" => Some(Shell::Fish), - "powershell" | "powershell_ise" => Some(Shell::Powershell), - "zsh" => Some(Shell::Zsh), - _ => None, - } -} - -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::Elvish, - Shell::Fish, - Shell::Powershell, - Shell::Zsh, - ] - } - - fn to_possible_value(&self) -> Option { - Some(match self { - Shell::Bash => PossibleValue::new("bash"), - Shell::Elvish => PossibleValue::new("elvish"), - Shell::Fish => PossibleValue::new("fish"), - Shell::Powershell => PossibleValue::new("powershell"), - Shell::Zsh => PossibleValue::new("zsh"), - }) - } -} - -impl Shell { - fn completer(&self) -> &dyn crate::dynamic::shells::ShellCompleter { - match self { - Self::Bash => &super::Bash, - Self::Elvish => &super::Elvish, - Self::Fish => &super::Fish, - Self::Powershell => &super::Powershell, - Self::Zsh => &super::Zsh, - } - } -} - -impl crate::dynamic::shells::ShellCompleter 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/src/dynamic/shells/zsh.rs b/clap_complete/src/dynamic/shells/zsh.rs deleted file mode 100644 index 4863f0eb..00000000 --- a/clap_complete/src/dynamic/shells/zsh.rs +++ /dev/null @@ -1,67 +0,0 @@ -/// A [`ShellCompleter`][crate::dynamic::shells::ShellCompleter] for zsh -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub struct Zsh; - -impl crate::dynamic::shells::ShellCompleter for Zsh { - fn file_name(&self, name: &str) -> String { - format!("{name}.zsh") - } - fn write_registration( - &self, - _name: &str, - bin: &str, - completer: &str, - buf: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error> { - let bin = shlex::try_quote(bin).unwrap_or(std::borrow::Cow::Borrowed(bin)); - let completer = - shlex::try_quote(completer).unwrap_or(std::borrow::Cow::Borrowed(completer)); - - let script = r#"#compdef BIN -function _clap_dynamic_completer() { - export _CLAP_COMPLETE_INDEX=$(expr $CURRENT - 1) - export _CLAP_IFS=$'\n' - - local completions=("${(@f)$(COMPLETER complete --shell zsh -- ${words} 2>/dev/null)}") - - if [[ -n $completions ]]; then - compadd -a completions - fi -} - -compdef _clap_dynamic_completer BIN"# - .replace("COMPLETER", &completer) - .replace("BIN", &bin); - - 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 ifs: Option = std::env::var("_CLAP_IFS").ok().and_then(|i| i.parse().ok()); - - // If the current word is empty, add an empty string to the args - let mut args = args.clone(); - if args.len() == index { - args.push("".into()); - } - let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; - - for (i, candidate) in completions.iter().enumerate() { - if i != 0 { - write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; - } - write!(buf, "{}", candidate.get_content().to_string_lossy())?; - } - Ok(()) - } -}