diff --git a/Cargo.lock b/Cargo.lock index 391b33be..ce6395f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,7 @@ dependencies = [ "async-recursion", "blake2", "clap", + "clap_complete", "common-path", "dirs", "dunce", @@ -270,6 +271,15 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_complete" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.0" diff --git a/Cargo.toml b/Cargo.toml index f6650710..102eeb09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ tokio = { version = "1.27", features = ["full"] } async-recursion = "1.0" clap = { version = "4.0", features = ["derive"] } +clap_complete = "4.0" semver = { version = "1.0", features = ["serde"] } blake2 = "0.10" typed-arena = "2" diff --git a/README.md b/README.md index 98fb1cbd..e3e015e5 100644 --- a/README.md +++ b/README.md @@ -445,7 +445,7 @@ Calling update with the `--fetch/-f` flag will force all git dependencies to be ### `clone` --- Clone dependency to make modifications -The `bender clone ` command checks out the package `PKG` into a directory (default `working_dir`, can be overridden with `-p / --path `). +The `bender clone ` command checks out the package `PKG` into a directory (default `working_dir`, can be overridden with `-p / --path `). To ensure the package is correctly linked in bender, the `Bender.local` file is modified to include a `path` dependency override, linking to the corresponding package. This can be used for development of dependent packages within the parent repository, allowing to test uncommitted and committed changes, without the worry that bender would update the dependency. @@ -535,6 +535,23 @@ This branch can then be rebased and a pull request can be opened from it as usua Note: when using mappings in your `vendor_package`, the patches will be relative to the mapped directory. Hence, for upstreaming, you might need to use `git am --directory=` instead of plain `git am`. +### `completion` --- Generate shell completion script + +The `bender completion ` command prints a completion script for the given shell. + +Installation and usage of these scripts is shell-dependent. Please refer to your shell's documentation +for information on how to install and use the generated script +([bash](https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html), +[zsh](https://zsh.sourceforge.io/Doc/Release/Completion-System.html), +[fish](https://fishshell.com/docs/current/completions.html)). + +Supported shells: +- `bash` +- `elvish` +- `fish` +- `powershell` +- `zsh` + [aur-bender]: https://aur.archlinux.org/packages/bender [releases]: https://github.com/pulp-platform/bender/releases [rust-installation]: https://doc.rust-lang.org/book/ch01-01-installation.html diff --git a/src/cli.rs b/src/cli.rs index 7ccc42a1..dce46071 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -37,6 +37,9 @@ pub fn main() -> Result<()> { .version(env!("CARGO_PKG_VERSION")) .author(env!("CARGO_PKG_AUTHORS")) .about("A dependency management tool for hardware projects.") + .after_help( + "Type 'bender --help' for more information about a bender subcommand.", + ) .arg( Arg::new("dir") .short('d') @@ -77,6 +80,7 @@ pub fn main() -> Result<()> { .subcommand(cmd::clone::new()) .subcommand(cmd::packages::new()) .subcommand(cmd::sources::new()) + .subcommand(cmd::completion::new()) .subcommand(cmd::config::new()) .subcommand(cmd::script::new()) .subcommand(cmd::checkout::new()) @@ -99,7 +103,7 @@ pub fn main() -> Result<()> { }; // Parse the arguments. - let matches = app.get_matches(); + let matches = app.clone().get_matches(); // Enable debug outputs if needed. if matches.contains_id("debug") && matches.get_flag("debug") { @@ -110,6 +114,11 @@ pub fn main() -> Result<()> { return cmd::init::run(matches); } + if let Some(("completion", matches)) = matches.subcommand() { + let mut app = app; + return cmd::completion::run(matches, &mut app); + } + let mut force_fetch = false; if let Some(("update", intern_matches)) = matches.subcommand() { force_fetch = intern_matches.get_flag("fetch"); diff --git a/src/cmd/completion.rs b/src/cmd/completion.rs new file mode 100644 index 00000000..925f2a22 --- /dev/null +++ b/src/cmd/completion.rs @@ -0,0 +1,44 @@ +// Copyright (c) 2017-2024 ETH Zurich +// Philipp Schilk + +//! The `completion` subcommand. + +use std::io; + +use crate::error::*; +use clap::{builder::PossibleValue, Arg, ArgMatches, Command}; + +/// Assemble the `completion` subcommand. +pub fn new() -> Command { + Command::new("completion") + .about("Emit shell completion script") + .arg( + Arg::new("completion_shell") + .help("Shell completion script style") + .required(true) + .num_args(1) + .value_name("SHELL") + .value_parser([ + PossibleValue::new("bash"), + PossibleValue::new("elvish"), + PossibleValue::new("fish"), + PossibleValue::new("powershell"), + PossibleValue::new("zsh"), + ]), + ) +} + +/// Execute the `completion` subcommand. +pub fn run(matches: &ArgMatches, app: &mut Command) -> Result<()> { + let shell = matches.get_one::("completion_shell").unwrap(); + let shell = match shell.as_str() { + "bash" => clap_complete::Shell::Bash, + "elvish" => clap_complete::Shell::Elvish, + "fish" => clap_complete::Shell::Fish, + "powershell" => clap_complete::Shell::PowerShell, + "zsh" => clap_complete::Shell::Zsh, + _ => unreachable!(), + }; + clap_complete::generate(shell, app, "bender", &mut io::stdout()); + Ok(()) +} diff --git a/src/cmd/fusesoc.rs b/src/cmd/fusesoc.rs index 489be436..a52230c9 100644 --- a/src/cmd/fusesoc.rs +++ b/src/cmd/fusesoc.rs @@ -27,7 +27,7 @@ use crate::target::TargetSpec; /// Assemble the `fusesoc` subcommand. pub fn new() -> Command { Command::new("fusesoc") - .about("Creates a FuseSoC `.core` file for all dependencies where none is present.") + .about("Creates a FuseSoC `.core` file for all dependencies where none is present") .arg( Arg::new("single") .long("single") diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 3c383995..abd9236d 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -9,6 +9,7 @@ pub mod checkout; pub mod clone; +pub mod completion; pub mod config; pub mod fusesoc; pub mod init; diff --git a/src/cmd/packages.rs b/src/cmd/packages.rs index c8f584b8..680a015f 100644 --- a/src/cmd/packages.rs +++ b/src/cmd/packages.rs @@ -24,7 +24,8 @@ pub fn new() -> Command { .long("flat") .num_args(0) .action(ArgAction::SetTrue) - .help("Do not group packages by topological rank. If the `--graph` option is specified, print multiple lines per package, one for each dependency.") + .help("Do not group packages by topological rank") + .long_help("Do not group packages by topological rank. If the `--graph` option is specified, print multiple lines per package, one for each dependency.") ) } diff --git a/src/cmd/vendor.rs b/src/cmd/vendor.rs index e85f4edb..b9fd61ea 100644 --- a/src/cmd/vendor.rs +++ b/src/cmd/vendor.rs @@ -34,7 +34,9 @@ pub struct PatchLink { pub fn new() -> Command { Command::new("vendor") .subcommand_required(true).arg_required_else_help(true) - .about("Copy source code from upstream external repositories into this repository. Functions similar to the lowrisc vendor.py script. Type bender vendor --help for more information about the subcommands.") + .about("Copy source code from upstream external repositories into this repository") + .long_about("Copy source code from upstream external repositories into this repository. Functions similar to the lowrisc vendor.py script.") + .after_help("Type 'bender vendor --help' for more information about a vendor subcommand.") .subcommand(Command::new("diff") .about("Display a diff of the local tree and the upstream tree with patches applied.") .arg( @@ -46,7 +48,8 @@ pub fn new() -> Command { ) ) .subcommand(Command::new("init") - .about("(Re-)initialize the external dependencies. Copies the upstream files into the target directories and applies existing patches.") + .about("(Re-)initialize the external dependencies.") + .long_about("(Re-)initialize the external dependencies. Copies the upstream files into the target directories and applies existing patches.") .arg( Arg::new("no_patch") .short('n') @@ -61,7 +64,8 @@ pub fn new() -> Command { Arg::new("plain") .action(ArgAction::SetTrue) .long("plain") - .help("Generate a plain diff instead of a format-patch. Includes all local changes (not only the staged ones)."), + .help("Generate a plain diff instead of a format-patch.") + .long_help("Generate a plain diff instead of a format-patch. Includes all local changes (not only the staged ones)."), ) .arg( Arg::new("message")