Skip to content

Commit

Permalink
cli: add shell completion subcommand
Browse files Browse the repository at this point in the history
As the clap_complete dynamic module is unstable and hard-codes the
subcommand as `complete`, which conflicts with the current `collect`
subcommand. Let's still use the legacy static way to generate the
completion file.

Usage:
$ retis sh-complete -h
Generate completions file for a specified shell

Usage: retis sh-complete [OPTIONS]

Options:
      --shell <SHELL>        Specify shell to complete for [possible values: bash, elvish, fish, powershell, zsh]
      --register <REGISTER>  Path to write completion-registration to
  -h, --help                 Print help

$ retis sh-complete --shell bash --register .
$ source retis.bash

Close: #68
Signed-off-by: Hangbin Liu <liuhangbin@gmail.com>
  • Loading branch information
liuhangbin committed Feb 1, 2024
1 parent 19d8d90 commit f009cdb
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 0 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ btf-rs = "1.0"
byteorder = "1.5"
caps = "0.5"
clap = { version = "4.0", features = ["derive", "string"] }
clap_complete = "4.4"
elf = "0.7"
flate2 = "1.0"
libbpf-rs = "0.22"
Expand Down
7 changes: 7 additions & 0 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,10 @@ only and Retis won't be able to fully filter probes.
$ sudo setcap cap_sys_admin,cap_bpf,cap_syslog=ep $(which retis)
$ retis collect
```

### Shell auto-completion

Retis can generate completion files for shells (Bash, Zsh, Fish...).
For example to enable auto-completion of Retis command in Bash, you can
add line `source <(retis sh-complete --shell bash)` in .bashrc, then
the command parameter could be auto-completed when pressing <Tab>.
6 changes: 6 additions & 0 deletions src/cli/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use super::dynamic::DynamicCommand;
use crate::benchmark::cli::Benchmark;
use crate::{
collect::cli::Collect,
generate::Complete,
module::{ModuleId, Modules},
process::cli::*,
profiles::{cli::ProfileCmd, Profile},
Expand Down Expand Up @@ -386,6 +387,10 @@ impl FullCli {
pub(crate) fn get_subcommand_mut(&mut self) -> Result<&mut dyn SubCommand> {
Ok(self.subcommand.as_mut())
}

pub(crate) fn get_command(&self) -> Command {
self.command.clone()
}
}

/// CliConfig represents the result of the Full CLI parsing
Expand Down Expand Up @@ -421,6 +426,7 @@ pub(crate) fn get_cli() -> Result<ThinCli> {
cli.add_subcommand(Box::new(Sort::new()?))?;
cli.add_subcommand(Box::new(Pcap::new()?))?;
cli.add_subcommand(Box::new(ProfileCmd::new()?))?;
cli.add_subcommand(Box::new(Complete::new()?))?;

#[cfg(feature = "benchmark")]
cli.add_subcommand(Box::new(Benchmark::new()?))?;
Expand Down
98 changes: 98 additions & 0 deletions src/generate/completion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//! # Completion
//!
//! Generate a completions file for a specified shell at runtime.

use std::{any::Any, io::Write, path::PathBuf};

use anyhow::Result;
use clap::{
error::Error as ClapError,
{value_parser, ArgMatches, Command, Parser},
};
use clap_complete::{generate, Generator, Shell};

use crate::{cli::*, module::Modules};

/// Generate completion file for a specified shell
#[derive(Parser, Debug, Default)]
#[command(name = "sh-complete")]
pub(crate) struct Complete {
/// Specify shell to complete for
// We use an Option and require the parameter to be set here to allow
// deriving Default on Complete.
#[arg(long, required = true, value_parser(value_parser!(Shell)))]
shell: Option<Shell>,

/// Path to write completion-registration to
#[arg(long)]
register: Option<PathBuf>,
}

impl SubCommand for Complete {
fn new() -> Result<Self>
where
Self: Sized,
{
Ok(Self::default())
}

fn name(&self) -> String {
<Self as clap::CommandFactory>::command()
.get_name()
.to_string()
}

fn as_any(&self) -> &dyn Any {
self
}

fn as_any_mut(&mut self) -> &mut dyn Any {
self
}

fn full(&self) -> Result<Command> {
Ok(<Self as clap::CommandFactory>::command())
}

fn update_from_arg_matches(&mut self, args: &ArgMatches) -> Result<(), ClapError> {
<Self as clap::FromArgMatches>::update_from_arg_matches(self, args)
}

fn runner(&self) -> Result<Box<dyn SubCommandRunner>> {
Ok(Box::new(CompleteRunner {}))
}
}

#[derive(Debug)]
pub(crate) struct CompleteRunner {}

impl SubCommandRunner for CompleteRunner {
fn check_prerequisites(&self) -> Result<()> {
Ok(())
}

fn run(&mut self, cli: FullCli, _modules: Modules) -> Result<()> {
let mut cmd = cli.get_command();
let matches = cli.get_command().get_matches();

if let Some(sub_m) = matches.subcommand_matches("sh-complete") {
if let Some(generator) = sub_m.get_one::<Shell>("shell") {
let mut buf = Vec::new();
let name = cmd.get_name().to_string();

generate(*generator, &mut cmd, name.clone(), &mut buf);
if let Some(out_path) = sub_m.get_one::<PathBuf>("register") {
if out_path.is_dir() {
let out_path = out_path.join(generator.file_name(&name));
let _ = std::fs::write(out_path, buf);
} else {
let _ = std::fs::write(out_path, buf);
}
} else {
let _ = std::io::stdout().write_all(&buf);
}
}
}
Ok(())
}
}
6 changes: 6 additions & 0 deletions src/generate/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//! # Generate
//!
//! Generate a completions file for a specified shell at runtime.

pub(crate) mod completion;
pub(crate) use self::completion::*;
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use log::{info, trace, warn, LevelFilter};
mod cli;
mod collect;
mod core;
mod generate;
mod module;
mod process;
mod profiles;
Expand Down

0 comments on commit f009cdb

Please sign in to comment.