diff options
author | Inna Palant <ipalant@google.com> | 2023-12-10 12:42:54 -0800 |
---|---|---|
committer | Inna Palant <ipalant@google.com> | 2023-12-10 12:42:54 -0800 |
commit | f0eba62f7c65672e37713530fe123fe47fb93921 (patch) | |
tree | 1744b4b0f078ec24dcebfdaff190ea5cead724a5 /src/generator | |
parent | 8fba264e49a6e905fec50c4912f4db40e7769989 (diff) | |
parent | e39717873aed95f4887b96af6129c809fecfc50e (diff) | |
download | clap_complete-f0eba62f7c65672e37713530fe123fe47fb93921.tar.gz |
Merge remote-tracking branch 'origin/upstream'platform-tools-34.0.5
Import b/312414193
Diffstat (limited to 'src/generator')
-rw-r--r-- | src/generator/mod.rs | 261 | ||||
-rw-r--r-- | src/generator/utils.rs | 278 |
2 files changed, 539 insertions, 0 deletions
diff --git a/src/generator/mod.rs b/src/generator/mod.rs new file mode 100644 index 0000000..a371f68 --- /dev/null +++ b/src/generator/mod.rs @@ -0,0 +1,261 @@ +//! Shell completion machinery + +pub mod utils; + +use std::ffi::OsString; +use std::fs::File; +use std::io::Error; +use std::io::Write; +use std::path::PathBuf; + +use clap::Command; + +/// Generator trait which can be used to write generators +pub trait Generator { + /// Returns the file name that is created when this generator is called during compile time. + /// + /// # Panics + /// + /// May panic when called outside of the context of [`generate`] or [`generate_to`] + /// + /// # Examples + /// + /// ``` + /// # use std::io::Write; + /// # use clap::Command; + /// use clap_complete::Generator; + /// + /// pub struct Fish; + /// + /// impl Generator for Fish { + /// fn file_name(&self, name: &str) -> String { + /// format!("{name}.fish") + /// } + /// # fn generate(&self, cmd: &Command, buf: &mut dyn Write) {} + /// } + /// ``` + fn file_name(&self, name: &str) -> String; + + /// Generates output out of [`clap::Command`](Command). + /// + /// # Panics + /// + /// May panic when called outside of the context of [`generate`] or [`generate_to`] + /// + /// # Examples + /// + /// The following example generator displays the [`clap::Command`](Command) + /// as if it is printed using [`std::println`]. + /// + /// ``` + /// use std::{io::Write, fmt::write}; + /// use clap::Command; + /// use clap_complete::Generator; + /// + /// pub struct ClapDebug; + /// + /// impl Generator for ClapDebug { + /// # fn file_name(&self, name: &str) -> String { + /// # name.into() + /// # } + /// fn generate(&self, cmd: &Command, buf: &mut dyn Write) { + /// write!(buf, "{cmd}").unwrap(); + /// } + /// } + /// ``` + fn generate(&self, cmd: &Command, buf: &mut dyn Write); +} + +/// Generate a completions file for a specified shell at compile-time. +/// +/// **NOTE:** to generate the file at compile time you must use a `build.rs` "Build Script" or a +/// [`cargo-xtask`](https://github.com/matklad/cargo-xtask) +/// +/// # Examples +/// +/// The following example generates a bash completion script via a `build.rs` script. In this +/// simple example, we'll demo a very small application with only a single subcommand and two +/// args. Real applications could be many multiple levels deep in subcommands, and have tens or +/// potentially hundreds of arguments. +/// +/// First, it helps if we separate out our `Command` definition into a separate file. Whether you +/// do this as a function, or bare Command definition is a matter of personal preference. +/// +/// ``` +/// // src/cli.rs +/// # use clap::{Command, Arg, ArgAction}; +/// pub fn build_cli() -> Command { +/// Command::new("compl") +/// .about("Tests completions") +/// .arg(Arg::new("file") +/// .help("some input file")) +/// .subcommand(Command::new("test") +/// .about("tests things") +/// .arg(Arg::new("case") +/// .long("case") +/// .action(ArgAction::Set) +/// .help("the case to test"))) +/// } +/// ``` +/// +/// In our regular code, we can simply call this `build_cli()` function, then call +/// `get_matches()`, or any of the other normal methods directly after. For example: +/// +/// ```ignore +/// // src/main.rs +/// +/// mod cli; +/// +/// fn main() { +/// let _m = cli::build_cli().get_matches(); +/// +/// // normal logic continues... +/// } +/// ``` +/// +/// Next, we set up our `Cargo.toml` to use a `build.rs` build script. +/// +/// ```toml +/// # Cargo.toml +/// build = "build.rs" +/// +/// [dependencies] +/// clap = "*" +/// +/// [build-dependencies] +/// clap = "*" +/// clap_complete = "*" +/// ``` +/// +/// Next, we place a `build.rs` in our project root. +/// +/// ```ignore +/// use clap_complete::{generate_to, shells::Bash}; +/// use std::env; +/// use std::io::Error; +/// +/// include!("src/cli.rs"); +/// +/// fn main() -> Result<(), Error> { +/// let outdir = match env::var_os("OUT_DIR") { +/// None => return Ok(()), +/// Some(outdir) => outdir, +/// }; +/// +/// let mut cmd = build_cli(); +/// let path = generate_to( +/// Bash, +/// &mut cmd, // We need to specify what generator to use +/// "myapp", // We need to specify the bin name manually +/// outdir, // We need to specify where to write to +/// )?; +/// +/// println!("cargo:warning=completion file is generated: {path:?}"); +/// +/// Ok(()) +/// } +/// ``` +/// +/// Now, once we compile there will be a `{bin_name}.bash` file in the directory. +/// Assuming we compiled with debug mode, it would be somewhere similar to +/// `<project>/target/debug/build/myapp-<hash>/out/myapp.bash`. +/// +/// **NOTE:** Please look at the individual [shells][crate::shells] +/// to see the name of the files generated. +/// +/// Using [`ValueEnum::value_variants()`][clap::ValueEnum::value_variants] you can easily loop over +/// all the supported shell variants to generate all the completions at once too. +/// +/// ```ignore +/// use clap::ValueEnum; +/// use clap_complete::{generate_to, Shell}; +/// use std::env; +/// use std::io::Error; +/// +/// include!("src/cli.rs"); +/// +/// fn main() -> Result<(), Error> { +/// let outdir = match env::var_os("OUT_DIR") { +/// None => return Ok(()), +/// Some(outdir) => outdir, +/// }; +/// +/// let mut cmd = build_cli(); +/// for &shell in Shell::value_variants() { +/// generate_to(shell, &mut cmd, "myapp", outdir)?; +/// } +/// +/// Ok(()) +/// } +/// ``` +pub fn generate_to<G, S, T>( + gen: G, + cmd: &mut Command, + bin_name: S, + out_dir: T, +) -> Result<PathBuf, Error> +where + G: Generator, + S: Into<String>, + T: Into<OsString>, +{ + cmd.set_bin_name(bin_name); + + let out_dir = PathBuf::from(out_dir.into()); + let file_name = gen.file_name(cmd.get_bin_name().unwrap()); + + let path = out_dir.join(file_name); + let mut file = File::create(&path)?; + + _generate::<G>(gen, cmd, &mut file); + Ok(path) +} + +/// Generate a completions file for a specified shell at runtime. +/// +/// Until `cargo install` can install extra files like a completion script, this may be +/// used e.g. in a command that outputs the contents of the completion script, to be +/// redirected into a file by the user. +/// +/// # Examples +/// +/// Assuming a separate `cli.rs` like the [`generate_to` example](generate_to()), +/// we can let users generate a completion script using a command: +/// +/// ```ignore +/// // src/main.rs +/// +/// mod cli; +/// use std::io; +/// use clap_complete::{generate, shells::Bash}; +/// +/// fn main() { +/// let matches = cli::build_cli().get_matches(); +/// +/// if matches.is_present("generate-bash-completions") { +/// generate(Bash, &mut cli::build_cli(), "myapp", &mut io::stdout()); +/// } +/// +/// // normal logic continues... +/// } +/// +/// ``` +/// +/// Usage: +/// +/// ```console +/// $ myapp generate-bash-completions > /usr/share/bash-completion/completions/myapp.bash +/// ``` +pub fn generate<G, S>(gen: G, cmd: &mut Command, bin_name: S, buf: &mut dyn Write) +where + G: Generator, + S: Into<String>, +{ + cmd.set_bin_name(bin_name); + _generate::<G>(gen, cmd, buf) +} + +fn _generate<G: Generator>(gen: G, cmd: &mut Command, buf: &mut dyn Write) { + cmd.build(); + gen.generate(cmd, buf) +} diff --git a/src/generator/utils.rs b/src/generator/utils.rs new file mode 100644 index 0000000..ca76d18 --- /dev/null +++ b/src/generator/utils.rs @@ -0,0 +1,278 @@ +//! Helpers for writing generators + +use clap::{Arg, Command}; + +/// Gets all subcommands including child subcommands in the form of `("name", "bin_name")`. +/// +/// Subcommand `rustup toolchain install` would be converted to +/// `("install", "rustup toolchain install")`. +pub fn all_subcommands(cmd: &Command) -> Vec<(String, String)> { + let mut subcmds: Vec<_> = subcommands(cmd); + + for sc_v in cmd.get_subcommands().map(all_subcommands) { + subcmds.extend(sc_v); + } + + subcmds +} + +/// Finds the subcommand [`clap::Command`] from the given [`clap::Command`] with the given path. +/// +/// **NOTE:** `path` should not contain the root `bin_name`. +pub fn find_subcommand_with_path<'cmd>(p: &'cmd Command, path: Vec<&str>) -> &'cmd Command { + let mut cmd = p; + + for sc in path { + cmd = cmd.find_subcommand(sc).unwrap(); + } + + cmd +} + +/// Gets subcommands of [`clap::Command`] in the form of `("name", "bin_name")`. +/// +/// Subcommand `rustup toolchain install` would be converted to +/// `("install", "rustup toolchain install")`. +pub fn subcommands(p: &Command) -> Vec<(String, String)> { + debug!("subcommands: name={}", p.get_name()); + debug!("subcommands: Has subcommands...{:?}", p.has_subcommands()); + + let mut subcmds = vec![]; + + for sc in p.get_subcommands() { + let sc_bin_name = sc.get_bin_name().unwrap(); + + debug!( + "subcommands:iter: name={}, bin_name={}", + sc.get_name(), + sc_bin_name + ); + + subcmds.push((sc.get_name().to_string(), sc_bin_name.to_string())); + } + + subcmds +} + +/// Gets all the short options, their visible aliases and flags of a [`clap::Command`]. +/// Includes `h` and `V` depending on the [`clap::Command`] settings. +pub fn shorts_and_visible_aliases(p: &Command) -> Vec<char> { + debug!("shorts: name={}", p.get_name()); + + p.get_arguments() + .filter_map(|a| { + if !a.is_positional() { + if a.get_visible_short_aliases().is_some() && a.get_short().is_some() { + let mut shorts_and_visible_aliases = a.get_visible_short_aliases().unwrap(); + shorts_and_visible_aliases.push(a.get_short().unwrap()); + Some(shorts_and_visible_aliases) + } else if a.get_visible_short_aliases().is_none() && a.get_short().is_some() { + Some(vec![a.get_short().unwrap()]) + } else { + None + } + } else { + None + } + }) + .flatten() + .collect() +} + +/// Gets all the long options, their visible aliases and flags of a [`clap::Command`]. +/// Includes `help` and `version` depending on the [`clap::Command`] settings. +pub fn longs_and_visible_aliases(p: &Command) -> Vec<String> { + debug!("longs: name={}", p.get_name()); + + p.get_arguments() + .filter_map(|a| { + if !a.is_positional() { + if a.get_visible_aliases().is_some() && a.get_long().is_some() { + let mut visible_aliases: Vec<_> = a + .get_visible_aliases() + .unwrap() + .into_iter() + .map(|s| s.to_string()) + .collect(); + visible_aliases.push(a.get_long().unwrap().to_string()); + Some(visible_aliases) + } else if a.get_visible_aliases().is_none() && a.get_long().is_some() { + Some(vec![a.get_long().unwrap().to_string()]) + } else { + None + } + } else { + None + } + }) + .flatten() + .collect() +} + +/// Gets all the flags of a [`clap::Command`](Command). +/// Includes `help` and `version` depending on the [`clap::Command`] settings. +pub fn flags(p: &Command) -> Vec<Arg> { + debug!("flags: name={}", p.get_name()); + p.get_arguments() + .filter(|a| !a.get_num_args().expect("built").takes_values() && !a.is_positional()) + .cloned() + .collect() +} + +/// Get the possible values for completion +pub fn possible_values(a: &Arg) -> Option<Vec<clap::builder::PossibleValue>> { + if !a.get_num_args().expect("built").takes_values() { + None + } else { + a.get_value_parser() + .possible_values() + .map(|pvs| pvs.collect()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Arg; + use clap::ArgAction; + + fn common_app() -> Command { + Command::new("myapp") + .subcommand( + Command::new("test").subcommand(Command::new("config")).arg( + Arg::new("file") + .short('f') + .short_alias('c') + .visible_short_alias('p') + .long("file") + .action(ArgAction::SetTrue) + .visible_alias("path"), + ), + ) + .subcommand(Command::new("hello")) + .bin_name("my-cmd") + } + + fn built() -> Command { + let mut cmd = common_app(); + + cmd.build(); + cmd + } + + fn built_with_version() -> Command { + let mut cmd = common_app().version("3.0"); + + cmd.build(); + cmd + } + + #[test] + fn test_subcommands() { + let cmd = built_with_version(); + + assert_eq!( + subcommands(&cmd), + vec![ + ("test".to_string(), "my-cmd test".to_string()), + ("hello".to_string(), "my-cmd hello".to_string()), + ("help".to_string(), "my-cmd help".to_string()), + ] + ); + } + + #[test] + fn test_all_subcommands() { + let cmd = built_with_version(); + + assert_eq!( + all_subcommands(&cmd), + vec![ + ("test".to_string(), "my-cmd test".to_string()), + ("hello".to_string(), "my-cmd hello".to_string()), + ("help".to_string(), "my-cmd help".to_string()), + ("config".to_string(), "my-cmd test config".to_string()), + ("help".to_string(), "my-cmd test help".to_string()), + ("config".to_string(), "my-cmd test help config".to_string()), + ("help".to_string(), "my-cmd test help help".to_string()), + ("test".to_string(), "my-cmd help test".to_string()), + ("hello".to_string(), "my-cmd help hello".to_string()), + ("help".to_string(), "my-cmd help help".to_string()), + ("config".to_string(), "my-cmd help test config".to_string()), + ] + ); + } + + #[test] + fn test_find_subcommand_with_path() { + let cmd = built_with_version(); + let sc_app = find_subcommand_with_path(&cmd, "test config".split(' ').collect()); + + assert_eq!(sc_app.get_name(), "config"); + } + + #[test] + fn test_flags() { + let cmd = built_with_version(); + let actual_flags = flags(&cmd); + + assert_eq!(actual_flags.len(), 2); + assert_eq!(actual_flags[0].get_long(), Some("help")); + assert_eq!(actual_flags[1].get_long(), Some("version")); + + let sc_flags = flags(find_subcommand_with_path(&cmd, vec!["test"])); + + assert_eq!(sc_flags.len(), 2); + assert_eq!(sc_flags[0].get_long(), Some("file")); + assert_eq!(sc_flags[1].get_long(), Some("help")); + } + + #[test] + fn test_flag_subcommand() { + let cmd = built(); + let actual_flags = flags(&cmd); + + assert_eq!(actual_flags.len(), 1); + assert_eq!(actual_flags[0].get_long(), Some("help")); + + let sc_flags = flags(find_subcommand_with_path(&cmd, vec!["test"])); + + assert_eq!(sc_flags.len(), 2); + assert_eq!(sc_flags[0].get_long(), Some("file")); + assert_eq!(sc_flags[1].get_long(), Some("help")); + } + + #[test] + fn test_shorts() { + let cmd = built_with_version(); + let shorts = shorts_and_visible_aliases(&cmd); + + assert_eq!(shorts.len(), 2); + assert_eq!(shorts[0], 'h'); + assert_eq!(shorts[1], 'V'); + + let sc_shorts = shorts_and_visible_aliases(find_subcommand_with_path(&cmd, vec!["test"])); + + assert_eq!(sc_shorts.len(), 3); + assert_eq!(sc_shorts[0], 'p'); + assert_eq!(sc_shorts[1], 'f'); + assert_eq!(sc_shorts[2], 'h'); + } + + #[test] + fn test_longs() { + let cmd = built_with_version(); + let longs = longs_and_visible_aliases(&cmd); + + assert_eq!(longs.len(), 2); + assert_eq!(longs[0], "help"); + assert_eq!(longs[1], "version"); + + let sc_longs = longs_and_visible_aliases(find_subcommand_with_path(&cmd, vec!["test"])); + + assert_eq!(sc_longs.len(), 3); + assert_eq!(sc_longs[0], "path"); + assert_eq!(sc_longs[1], "file"); + assert_eq!(sc_longs[2], "help"); + } +} |