Created
March 6, 2025 01:31
-
-
Save nytpu/2275342140d440e03f6f8aead7b0f013 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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