Skip to content

Commit

Permalink
feat: render help in cli parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
jdx committed Oct 14, 2024
1 parent c458d8c commit 7c49fcb
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 18 deletions.
9 changes: 9 additions & 0 deletions lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@ 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,
#[label = "{0}"] SourceSpan,
#[source_code] NamedSource,
),

#[error("Missing required arg: <{0}>")]
MissingArg(String),

#[error("{0}")]
Help(String),

#[error("Invalid usage config")]
#[diagnostic(transparent)]
Miette(#[from] miette::MietteError),
Expand Down
48 changes: 37 additions & 11 deletions lib/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -15,7 +16,7 @@ pub struct ParseOutput {
pub flags: IndexMap<SpecFlag, ParseValue>,
pub available_flags: BTreeMap<String, SpecFlag>,
pub flag_awaiting_value: Option<SpecFlag>,
pub errors: Vec<String>,
pub errors: Vec<UsageErr>,
}

#[derive(Debug, EnumTryAs)]
Expand All @@ -28,8 +29,11 @@ pub enum ParseValue {

pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
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)
}
Expand Down Expand Up @@ -100,8 +104,14 @@ pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miett
} else {
if let Some(choices) = &arg.choices {
if !choices.choices.contains(&w) {
if is_help_arg(spec, &w) {
// TODO: render based on current args
out.errors
.push(UsageErr::Help(docs::cli::render_help(spec)));
continue;
}
bail!(
"invalid choice for option {}: {w}, expected one of {}",
"Invalid choice for option {}: {w}, expected one of {}",
flag.name,
choices.choices.join(", ")
);
Expand Down Expand Up @@ -185,8 +195,14 @@ pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miett
} else {
if let Some(choices) = &arg.choices {
if !choices.choices.contains(&w) {
if is_help_arg(spec, &w) {
// TODO: render based on current args
out.errors
.push(UsageErr::Help(docs::cli::render_help(spec)));
continue;
}
bail!(
"invalid choice for arg {}: {w}, expected one of {}",
"Invalid choice for arg {}: {w}, expected one of {}",
arg.name,
choices.choices.join(", ")
);
Expand All @@ -197,28 +213,38 @@ pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miett
}
continue;
}
if is_help_arg(spec, &w) {
// TODO: render based on current args
out.errors
.push(UsageErr::Help(docs::cli::render_help(spec)));
continue;
}
bail!("unexpected word: {w}");
}

for arg in out.cmd.args.iter().skip(out.args.len()) {
if arg.required {
out.errors
.push(format!("missing required arg <{}>", 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<String, String> {
let mut env = BTreeMap::new();
Expand Down
10 changes: 10 additions & 0 deletions lib/src/spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub struct Spec {
pub about: Option<String>,
pub about_long: Option<String>,
pub about_md: Option<String>,
pub disable_help: Option<bool>,
}

impl Spec {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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()));
Expand Down
25 changes: 18 additions & 7 deletions lib/tests/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
}
}
)*
Expand All @@ -23,12 +23,12 @@ tests! {
required_arg:
spec=r#"arg "<name>""#,
args="",
expected=r#"missing required arg <name>"#,
expected=r#"Missing required arg: <name>"#,

required_option:
required_flag:
spec=r#"flag "--name <name>" required=true"#,
args="",
expected=r#"missing required option --name <name>"#,
expected=r#"Missing required flag: --name <name>"#,

negate:
spec=r#"flag "--force" negate="--no-force""#,
Expand Down Expand Up @@ -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 "<shell>" {
Expand All @@ -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 "<shell>" {
choices "bash" "fish" "zsh"
}"#,
args="--help",
expected=r#"Usage: <shell>
Arguments:
<shell>
"#,
}

0 comments on commit 7c49fcb

Please sign in to comment.