Skip to content

Instantly share code, notes, and snippets.

@nytpu
Created March 6, 2025 01:31
Show Gist options
  • Select an option

  • Save nytpu/2275342140d440e03f6f8aead7b0f013 to your computer and use it in GitHub Desktop.

Select an option

Save nytpu/2275342140d440e03f6f8aead7b0f013 to your computer and use it in GitHub Desktop.
// depends on anyhow, bpaf, shlex
use anyhow::{Result, anyhow};
use bpaf::{Args, Bpaf, OptionParser, Parser};
use std::{
fmt::Debug,
io::{BufRead, Write},
path::PathBuf,
};
const BPAF_PRINT_WIDTH: usize = 120;
// All commands
#[derive(Bpaf, Debug, PartialEq, Eq, Clone, Hash)]
enum Command {
#[bpaf(command)]
CommandOne,
#[bpaf(command)]
CommandTwo,
// ...
#[bpaf(command)]
CommandTwenty,
/// Print software version
#[bpaf(command)]
Version,
/// Print help information
#[bpaf(command)]
Help {
/// Subcommand to view detailed help for
#[bpaf(positional("COMMAND"))]
command: Option<String>,
},
}
/// Return a [`Command`], but as an `OptionParser` suitable for direct parsing rather than a plain
/// `Parser`, for parsing options from the REPL
fn command_options() -> OptionParser<Command> {
// fallback_to_usage feels nicest when in the REPL
// TODO: disable --help somehow
command().to_options().fallback_to_usage()
}
// Used for parsing CLI options from the shell, has some additional top-level options
#[derive(Bpaf, Debug, PartialEq, Eq, Clone, Hash)]
#[bpaf(options)]
struct CliOptions {
/// Override the root path, used for resolving relative names
#[bpaf(short, long, env("MY_PROGRAM_CONFIG"), argument("PATH"))]
config: Option<PathBuf>,
/// Print software version
#[bpaf(short, short('V'), long)]
version: bool,
#[bpaf(external, optional)]
command: Option<Command>,
}
fn main() -> Result<()> {
let options = cli_options().run();
if options.version {
print_version();
return Ok(());
}
if let Some(Command::Help { command }) = &options.command {
print_help(command, cli_options());
return Ok(());
}
match options.command {
Some(command) => run_command(&command),
None => repl(),
}
}
/// REPL loop dropped into if no command is provided in the shell.
fn repl() -> Result<()> {
// Hacked together discount readline
let mut stdin = std::io::stdin().lock();
let mut line = String::new();
loop {
print!("test> ");
std::io::stdout().flush()?;
line.clear();
if stdin.read_line(&mut line)? == 0 {
break;
}
let split = shlex::split(&line).ok_or(anyhow!("Invalid input line: {line}"))?;
match command_options().run_inner(split.as_slice()) {
Ok(command) => {
if let Command::Help { command } = &command {
print_help(command, command_options());
} else if let Err(e) = run_command(&command) {
eprintln!("{}", e);
}
}
Err(err) => err.print_message(BPAF_PRINT_WIDTH),
};
}
Ok(())
}
/// Execute the given command
fn run_command(command: &Command) -> Result<()> {
match command {
Command::Version => print_version(),
// Has to be handled by the caller because we want to print the help for the specific
// OptionsParser used to parse this command
Command::Help { .. } => panic!("Help not handled prior to run_command"),
_ => println!("{command:?}"),
}
Ok(())
}
/// Print the current program name and version as specified in Cargo
// I much prefer the classic version format of just "Program vXX.YY.ZZ" instead of bpaf's
// "Version: XX.YY.ZZ", and providing a custom --version seems to be the only way to override it
fn print_version() {
println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
}
/// Given an `OptionParser` to extract help info from, and optionally a subcommand to get help for,
/// print the output as if passing --help to the option parser.
fn print_help<T: Debug>(command_name: &Option<String>, options: OptionParser<T>) {
let args: &[&str] = match command_name {
Some(command) => &[command, "--help"],
None => &["--help"],
};
options
.run_inner(Args::from(args).set_name(env!("CARGO_PKG_NAME")))
.unwrap_err()
.print_message(BPAF_PRINT_WIDTH);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment