From 7c49fcba4567da7ad8c7af9c4bb72a7c276a4a57 Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdx@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:37:51 -0500 Subject: [PATCH] feat: render help in cli parsing --- lib/src/error.rs | 9 +++++++++ lib/src/parse.rs | 48 ++++++++++++++++++++++++++++++++++----------- lib/src/spec/mod.rs | 10 ++++++++++ lib/tests/parse.rs | 25 ++++++++++++++++------- 4 files changed, 74 insertions(+), 18 deletions(-) diff --git a/lib/src/error.rs b/lib/src/error.rs index d976529..89671cc 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -6,6 +6,9 @@ pub enum UsageErr { #[error("Invalid flag: {0}")] InvalidFlag(String, #[label] SourceSpan, #[source_code] String), + #[error("Missing required flag: --{0} <{0}>")] + MissingFlag(String), + #[error("Invalid usage config")] InvalidInput( String, @@ -13,6 +16,12 @@ pub enum UsageErr { #[source_code] NamedSource, ), + #[error("Missing required arg: <{0}>")] + MissingArg(String), + + #[error("{0}")] + Help(String), + #[error("Invalid usage config")] #[diagnostic(transparent)] Miette(#[from] miette::MietteError), diff --git a/lib/src/parse.rs b/lib/src/parse.rs index 668aa75..5063c13 100644 --- a/lib/src/parse.rs +++ b/lib/src/parse.rs @@ -6,7 +6,8 @@ use std::collections::{BTreeMap, VecDeque}; use std::fmt::{Debug, Display, Formatter}; use strum::EnumTryAs; -use crate::{Spec, SpecArg, SpecCommand, SpecFlag}; +use crate::error::UsageErr; +use crate::{docs, Spec, SpecArg, SpecCommand, SpecFlag}; pub struct ParseOutput { pub cmd: SpecCommand, @@ -15,7 +16,7 @@ pub struct ParseOutput { pub flags: IndexMap, pub available_flags: BTreeMap, pub flag_awaiting_value: Option, - pub errors: Vec, + pub errors: Vec, } #[derive(Debug, EnumTryAs)] @@ -28,8 +29,11 @@ pub enum ParseValue { pub fn parse(spec: &Spec, input: &[String]) -> Result { let out = parse_partial(spec, input)?; + if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) { + bail!("{err}"); + } if !out.errors.is_empty() { - bail!("{}", out.errors.join("\n")); + bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n")); } Ok(out) } @@ -100,8 +104,14 @@ pub fn parse_partial(spec: &Spec, input: &[String]) -> Result Result Result", arg.name)); + out.errors.push(UsageErr::MissingArg(arg.name.clone())); } } for flag in out.available_flags.values() { if flag.required && !out.flags.contains_key(flag) { - out.errors.push(format!( - "missing required option --{} <{}>", - flag.name, flag.name - )); + out.errors.push(UsageErr::MissingFlag(flag.name.clone())); } } Ok(out) } +fn is_help_arg(spec: &Spec, w: &str) -> bool { + spec.disable_help != Some(true) + && (w == "--help" + || w == "-h" + || w == "-?" + || (spec.cmd.subcommands.is_empty() && w == "help")) +} + impl ParseOutput { pub fn as_env(&self) -> BTreeMap { let mut env = BTreeMap::new(); diff --git a/lib/src/spec/mod.rs b/lib/src/spec/mod.rs index 1436502..6ba49a3 100644 --- a/lib/src/spec/mod.rs +++ b/lib/src/spec/mod.rs @@ -40,6 +40,7 @@ pub struct Spec { pub about: Option, pub about_long: Option, pub about_md: Option, + pub disable_help: Option, } impl Spec { @@ -115,6 +116,7 @@ impl Spec { let complete = SpecComplete::parse(ctx, &node)?; schema.complete.insert(complete.name.clone(), complete); } + "disable_help" => schema.disable_help = Some(node.arg(0)?.ensure_bool()?), "include" => { let file = node .props() @@ -168,6 +170,9 @@ impl Spec { if !other.complete.is_empty() { self.complete.extend(other.complete); } + if other.disable_help.is_some() { + self.disable_help = other.disable_help; + } self.cmd.merge(other.cmd); } } @@ -253,6 +258,11 @@ impl Display for Spec { node.push(KdlEntry::new(KdlValue::RawString(long_about.clone()))); nodes.push(node); } + if let Some(disable_help) = self.disable_help { + let mut node = KdlNode::new("disable_help"); + node.push(KdlEntry::new(disable_help)); + nodes.push(node); + } if !self.usage.is_empty() { let mut node = KdlNode::new("usage"); node.push(KdlEntry::new(self.usage.clone())); diff --git a/lib/tests/parse.rs b/lib/tests/parse.rs index 09f6fe6..9546164 100644 --- a/lib/tests/parse.rs +++ b/lib/tests/parse.rs @@ -11,8 +11,8 @@ macro_rules! tests { let mut args = shell_words::split($args).unwrap(); args.insert(0, "test".to_string()); match parse(&spec, &args) { - Ok(env) => assert_str_eq!(format!("{:?}", env.as_env()), $expected.trim()), - Err(e) => assert_str_eq!(format!("{e}"), $expected.trim()), + Ok(env) => assert_str_eq!(format!("{:?}", env.as_env()).trim(), $expected.trim()), + Err(e) => assert_str_eq!(format!("{e}").trim(), $expected.trim()), } } )* @@ -23,12 +23,12 @@ tests! { required_arg: spec=r#"arg """#, args="", - expected=r#"missing required arg "#, + expected=r#"Missing required arg: "#, - required_option: + required_flag: spec=r#"flag "--name " required=true"#, args="", - expected=r#"missing required option --name "#, + expected=r#"Missing required flag: --name "#, negate: spec=r#"flag "--force" negate="--no-force""#, @@ -57,7 +57,7 @@ tests! { choices "bash" "fish" "zsh" }"#, args="-s invalid", - expected=r#"invalid choice for option shell: invalid, expected one of bash, fish, zsh"#, + expected=r#"Invalid choice for option shell: invalid, expected one of bash, fish, zsh"#, arg_choices_ok: spec=r#"arg "" { @@ -71,5 +71,16 @@ tests! { choices "bash" "fish" "zsh" }"#, args="invalid", - expected=r#"invalid choice for arg shell: invalid, expected one of bash, fish, zsh"#, + expected=r#"Invalid choice for arg shell: invalid, expected one of bash, fish, zsh"#, + + arg_choices_help: + spec=r#"arg "" { + choices "bash" "fish" "zsh" +}"#, + args="--help", + expected=r#"Usage: + +Arguments: + +"#, }