Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,14 +265,14 @@ tasks --usage"`, so task names are meant to come from running that. usage-argv d

### Then: what a CLI framework has to have

- [x] **Help rendering** — `-h` and `--help` both match usage-lib byte for byte across all 211
of mise's commands: the usage line, the descriptions, commands, arguments and flags grouped
by heading, examples, the surrounding text, hidden-item filtering, and for the long form the
column alignment and wrapping to `COLUMNS`. What is left is the wiring — `--help`/`-h` on
every command, `Error::Help`, and a real `help` subcommand. Holding it to parity found ten
things a spec could say that the derive could not, plus two bugs in usage-lib's own
renderer; the last of them was `after_long_help`, which 115 of mise's commands use to carry
their Examples section, so a page without it was missing what a reader came for.
- [x] **Help rendering** — `-h` and `--help` both match usage-lib byte for byte across all 211 of
mise's commands, and both are wired: the parser recognises them itself, after a command's own
flags so a CLI that declares its own keeps it, and a request comes back as `Error::Help`
carrying the command it was asked about. `parse()` renders and exits; `parse_from` returns
it, so a library embedding this decides for itself. Costs 111 instructions, since the check
is only reached by a flag that matched nothing. What is left is the `help` _subcommand_,
which every CLI with subcommands should have. Holding the rendering to parity found ten
things a spec could say that the derive could not, and two bugs in usage-lib's own renderer.
- [ ] **Completions, self-contained** — `<bin> completion <shell>` emits the
script; a hidden `<bin> complete-word` serves requests from the binary's own
embedded spec. Same dispatch shape usage-cli uses today, without requiring
Expand Down
47 changes: 47 additions & 0 deletions argv/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use core::fmt::Write as _;

use crate::spec::{ArgMeta, CommandMeta, Example, FlagMeta, Spec};
use crate::Command;
use crate::DoubleDash;

/// How many flags or arguments are listed individually before collapsing to a placeholder.
Expand Down Expand Up @@ -668,3 +669,49 @@ fn long_commands_section(out: &mut String, path: &[&str], meta: &CommandMeta<'_>
" help\n Print this message or the help of the given subcommand(s)"
);
}

/// The path and metadata for a command, found by identity within a spec.
///
/// [`Error::Help`](crate::Error::Help) carries the `Command` the request was about, because the
/// parse tables are what a parse walks and the metadata is behind a feature. Rendering needs the
/// metadata and the path a user typed to reach it, and both are in the tree — so this walks it,
/// comparing addresses rather than names, which two commands can share.
///
/// `None` when the command is not in this spec, which means the two came from different CLIs.
pub fn find<'a>(
spec: &'a Spec<'a>,
cmd: &Command<'_>,
) -> Option<(Vec<&'a str>, &'a CommandMeta<'a>)> {
fn walk<'a>(
path: &mut Vec<&'a str>,
meta: &'a CommandMeta<'a>,
cmd: &Command<'_>,
) -> Option<&'a CommandMeta<'a>> {
if core::ptr::eq(meta.cmd, cmd) {
return Some(meta);
}
for sub in meta.subcommands {
path.push(sub.cmd.name);
if let Some(found) = walk(path, sub, cmd) {
return Some(found);
}
path.pop();
}
None
}

let mut path = vec![spec.bin.unwrap_or(spec.name)];
walk(&mut path, spec.root, cmd).map(|meta| (path, meta))
}

/// The page a help request asks for, ready to print.
///
/// The two forms differ as clap has them: `-h` is the short one and `--help` the long one.
pub fn render(spec: &Spec<'_>, cmd: &Command<'_>, long: bool) -> Option<String> {
let (path, meta) = find(spec, cmd)?;
Some(if long {
long_help(spec, &path, meta)
} else {
short_help(spec, &path, meta)
})
}
137 changes: 136 additions & 1 deletion argv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,16 @@ pub enum Error<'t, 'v> {
InvalidValue(::std::boxed::Box<InvalidValue<'t>>),
/// A subcommand was required, and none was given.
MissingSubcommand,
/// `--help` or `-h` was given, and `cmd` is what it was asked about.
///
/// Not a failure, and returned as one anyway: a parse that stops to print help has not
/// produced a value, and every caller already handles the "no value" shape. clap does the
/// same thing for the same reason.
///
/// `long` distinguishes the two: `-h` prints the short form and `--help` the long one, as
/// clap has them. The caller renders — this crate does not print, because a library that
/// writes to stdout on its own is one an adopter cannot embed.
Help { cmd: &'t Command<'t>, long: bool },
}

/// The high half of every key one declaration's items get.
Expand Down Expand Up @@ -547,6 +557,40 @@ pub const fn concat_args<const N: usize>(
out
}

/// The key `--help` answers to, and the one `-h` does.
///
/// Reserved rather than generated: a derive builds keys from a hash of the type they came from
/// in the high half and an index in the low half, so the top of the range belongs to nobody.
/// Generated code compares against these to tell a help request from a flag of its own.
pub const HELP_LONG_KEY: u64 = u64::MAX;
/// See [`HELP_LONG_KEY`].
pub const HELP_SHORT_KEY: u64 = u64::MAX - 1;

/// `--help`, which every command answers to.
///
/// In the parse table and *not* in the metadata, which is the whole trick: the parser has to
/// recognise the flag, and help output must not list it — a spec does not declare `--help`, so
/// showing one would make the rendered page disagree with the spec it came from.
pub static HELP_LONG: Flag<'static> = Flag {
key: HELP_LONG_KEY,
name: "help",
longs: &["help"],
..Flag::BOOL
};

/// `-h`, which prints the shorter form.
pub static HELP_SHORT: Flag<'static> = Flag {
key: HELP_SHORT_KEY,
name: "help",
shorts: b"h",
..Flag::BOOL
};

/// Whether a flag is one of the two the parser supplies rather than the CLI declaring it.
pub fn is_help_flag(flag: &Flag<'_>) -> bool {
flag.key == HELP_LONG_KEY || flag.key == HELP_SHORT_KEY
}

/// Resolve a subcommand by name or alias, at compile time.
///
/// For [`Command::default_subcommand`], which names a command that a derive cannot see: the
Expand Down Expand Up @@ -885,6 +929,16 @@ impl<'t, 'v> Parser<'t, 'v> {
});
}

// Every CLI answers to `--help`, and none of them declares it. Asked *after* the
// command's own flags, so a CLI that declares its own `--help` keeps it.
if name == b"help" {
return Ok(Event::Flag {
flag: &HELP_LONG,
value: None,
negated: false,
});
}

Comment thread
greptile-apps[bot] marked this conversation as resolved.
if self.cmd.unknown_flags == UnknownFlags::Error {
return Err(Error::UnknownFlag { token });
}
Expand Down Expand Up @@ -1092,7 +1146,15 @@ impl<'t, 'v> Parser<'t, 'v> {
}

fn find_short(&self, byte: u8) -> Option<&'t Flag<'t>> {
self.in_scope().find(|f| f.shorts.contains(&byte))
self.in_scope()
.find(|f| f.shorts.contains(&byte))
// As for `--help`: supplied by the parser, and only where the command has not
// declared a `-h` of its own.
.or(if byte == b'h' {
Some(&HELP_SHORT)
} else {
None
})
}

fn find_subcommand(&self, name: &[u8]) -> Option<&'t Command<'t>> {
Expand Down Expand Up @@ -1908,6 +1970,79 @@ mod tests {
);
}

#[test]
fn a_wrapper_still_forwards_a_help_flag() {
// Supplying `--help` must not take the two forwarding mechanisms away from a wrapper,
// which is the one place a CLI means to hand the token on rather than answer it.
static ARGS: Arg = Arg {
key: 24,
name: "args",
..Arg::VAR
};
static WRAP: Command = Command {
name: "wrap",
args: &[&ARGS],
..Command::EMPTY
};

// A typed separator: everything after it is a value, `--help` included.
let a = argv(["--", "--help", "-h"]);
assert_eq!(
parse(&WRAP, &a).unwrap(),
vec![
Event::Arg {
arg: &ARGS,
value: b"--help"
},
Event::Arg {
arg: &ARGS,
value: b"-h"
},
]
);

// And `automatic`, for the wrapper whose caller should not have to type one: the
// first value stops flag interpretation, so the flags after it forward.
static AUTO_ARGS: Arg = Arg {
key: 25,
name: "args",
double_dash: DoubleDash::Automatic,
..Arg::VAR
};
static AUTO_WRAP: Command = Command {
name: "wrap",
args: &[&AUTO_ARGS],
..Command::EMPTY
};

let a = argv(["node", "--help"]);
assert_eq!(
parse(&AUTO_WRAP, &a).unwrap(),
vec![
Event::Arg {
arg: &AUTO_ARGS,
value: b"node"
},
Event::Arg {
arg: &AUTO_ARGS,
value: b"--help"
},
]
);

// Before either takes effect, though, the wrapper's own help is what `--help` asks
// for — `mise run --help` is a question about `run`, not a value for it.
let a = argv(["--help"]);
assert_eq!(
parse(&AUTO_WRAP, &a).unwrap(),
vec![Event::Flag {
flag: &HELP_LONG,
value: None,
negated: false
}]
);
}

#[test]
fn double_dash_required_arg() {
static CMD: Arg = Arg {
Expand Down
Loading
Loading