Automatic shell completion generation

* Add automatic shell completion generation.
Add automatic generation of shell completion scripts for various shells
(the ones supported by `clap_complete`). The scripts can be generated
using the `generate-shell-completion` xtask, which outputs the shell
script to stdout.

* Improve shell completion generation xtask.
General improvements to both the shell completion generation as well as
the xtask package itself. Update the README to match the new additions.
This commit is contained in:
Thomas Frans
2023-03-09 19:02:11 +01:00
committed by GitHub
parent c457efc6fc
commit e8adff444c
6 changed files with 113 additions and 17 deletions

View File

@@ -7,6 +7,7 @@ edition = "2021"
[dependencies]
clap_mangen = "0.2.8"
clap_complete = "4.1.4"
clap = "4.1.6"
[dependencies.ncspot]

View File

@@ -4,10 +4,14 @@ use std::{env, fs};
use clap::builder::PathBufValueParser;
use clap::error::{Error, ErrorKind};
use clap::ArgMatches;
use ncspot::AUTHOR;
use clap_complete::Shell;
use ncspot::{AUTHOR, BIN_NAME};
static DEFAULT_OUTPUT_DIRECTORY: &str = "misc";
enum XTaskSubcommand {
GenerateManpage,
GenerateShellCompletion,
}
impl TryFrom<&ArgMatches> for XTaskSubcommand {
@@ -17,6 +21,7 @@ impl TryFrom<&ArgMatches> for XTaskSubcommand {
if let Some(subcommand) = value.subcommand() {
match subcommand.0 {
"generate-manpage" => Ok(XTaskSubcommand::GenerateManpage),
"generate-shell-completion" => Ok(XTaskSubcommand::GenerateShellCompletion),
_ => Err(Error::new(clap::error::ErrorKind::InvalidSubcommand)),
}
} else {
@@ -45,48 +50,115 @@ fn try_main() -> Result<(), DynError> {
.long_about(
"
Cargo xtask is a convention that allows easy integration of third party commands into the regular
cargo workflox. Xtask's are defined as a separate package and can be used for all kinds of
automation.
",
cargo workflow. Xtask's are defined as a separate package and can be used for all kinds of
automation.",
)
.subcommand(
.subcommands([
clap::Command::new("generate-manpage")
.visible_alias("gm")
.args([clap::Arg::new("output")
.short('o')
.long("output")
.value_name("PATH")
.default_value("misc")
.help("Output directory for the generated man page.")
.value_parser(PathBufValueParser::new())])
.about("Automatic man page generation."),
);
clap::Command::new("generate-shell-completion")
.visible_alias("gsc")
.args([
clap::Arg::new("shells")
.short('s')
.long("shells")
.value_name("SHELLS")
.default_values(["bash", "zsh", "fish"])
.value_delimiter(',')
.help("The shells for which completion should be generated."),
clap::Arg::new("output")
.short('o')
.long("output")
.value_name("PATH")
.default_value("misc")
.help("Output directory for the generated completion script.")
.value_parser(PathBufValueParser::new()),
])
.about("Automatic shell completion generation.")
.long_about(
"
Automatic shell completion generation.
Supported shells: bash,zsh,fish,elvish,powershell",
),
]);
let program_parsed_arguments = arguments_model.get_matches();
let parsed_subcommand = XTaskSubcommand::try_from(&program_parsed_arguments)?;
let subcommand_parsed_arguments = program_parsed_arguments.subcommand().unwrap().1;
match parsed_subcommand {
XTaskSubcommand::GenerateManpage => {
generate_manpage(program_parsed_arguments.subcommand().unwrap().1)
XTaskSubcommand::GenerateManpage => generate_manpage(subcommand_parsed_arguments),
XTaskSubcommand::GenerateShellCompletion => {
generate_shell_completion(subcommand_parsed_arguments)
}
}
}
fn generate_manpage(subcommand_arguments: &ArgMatches) -> Result<(), DynError> {
let output_directory =
if let Some(output_argument) = subcommand_arguments.get_one::<PathBuf>("output") {
output_argument.clone()
} else {
fs::create_dir_all("misc")?;
PathBuf::from("misc")
};
let default_output_directory = PathBuf::from(DEFAULT_OUTPUT_DIRECTORY);
let output_directory = subcommand_arguments
.get_one::<PathBuf>("output")
.unwrap_or(&default_output_directory);
let cmd = ncspot::program_arguments();
let man = clap_mangen::Man::new(cmd);
let mut buffer: Vec<u8> = Default::default();
man.render(&mut buffer)?;
if *output_directory == default_output_directory {
fs::create_dir_all(DEFAULT_OUTPUT_DIRECTORY)?;
}
man.render(&mut buffer)?;
std::fs::write(output_directory.join("ncspot.1"), buffer)?;
Ok(())
}
fn generate_shell_completion(subcommand_arguments: &ArgMatches) -> Result<(), DynError> {
let default_output_directory = PathBuf::from(DEFAULT_OUTPUT_DIRECTORY);
let output_directory = subcommand_arguments
.get_one::<PathBuf>("output")
.unwrap_or(&default_output_directory);
let shells = subcommand_arguments
.get_many::<String>("shells")
.map(|shells| {
shells
.map(|shell| match shell.as_str() {
"bash" => Shell::Bash,
"zsh" => Shell::Zsh,
"fish" => Shell::Fish,
"elvish" => Shell::Elvish,
"powershell" => Shell::PowerShell,
_ => {
eprintln!("Unrecognized shell: {}", shell);
std::process::exit(-1);
}
})
.collect()
})
.unwrap_or(vec![Shell::Bash, Shell::Zsh, Shell::Fish]);
if *output_directory == default_output_directory {
fs::create_dir_all(DEFAULT_OUTPUT_DIRECTORY)?;
}
for shell in shells {
clap_complete::generate_to(
shell,
&mut ncspot::program_arguments(),
BIN_NAME,
output_directory,
)?;
}
Ok(())
}