Skip to content

Commit e518c7b

Browse files
committed
refactor(cli): document node in --help and drop the one-variant enum
Review follow-up on the sub-command module. The token is stripped before clap ever sees it, so `--help` advertised no sub-command at all and `node` was undiscoverable from the help output. It is now listed there, from a HELP_NOTE const this module owns and cli.rs only points at, and a test pins it. Invocation had a single variant, so it bought nothing that returning CliOptions does not: main.rs destructured it irrefutably, and a second variant would force that line to become a match either way. Gone until there is a second entry point to name. Also pin that a *second* `node` token is left for clap to reject like any other stray positional, and record why the two parses are compared through Debug rather than PartialEq.
1 parent 67ee7d6 commit e518c7b

3 files changed

Lines changed: 42 additions & 23 deletions

File tree

bin/ethlambda/src/cli.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ use std::path::PathBuf;
66
use crate::version;
77

88
#[derive(Debug, clap::Parser)]
9-
#[command(name = "ethlambda", author = "LambdaClass", version = version::CLIENT_VERSION, about = "ethlambda consensus client")]
9+
#[command(
10+
name = "ethlambda",
11+
author = "LambdaClass",
12+
version = version::CLIENT_VERSION,
13+
about = "ethlambda consensus client",
14+
after_help = crate::command::HELP_NOTE
15+
)]
1016
pub(crate) struct CliOptions {
1117
/// Path to the chain genesis config (e.g., config.yaml).
1218
#[arg(long)]

bin/ethlambda/src/command.rs

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
//! Sub-command dispatch.
1+
//! Sub-command handling.
22
//!
33
//! `node` is the default sub-command: it can be named explicitly
44
//! (`ethlambda node --genesis ...`) or left out entirely
55
//! (`ethlambda --genesis ...`). Leaving it out is what the Dockerfile,
66
//! lean-quickstart, the hive shim and the devnet skills all do, so that form
77
//! stays the one this module is careful about: the token is simply removed
88
//! before parsing, and the very same [`CliOptions`] parser then sees the very
9-
//! same arguments it saw before this module existed. Help text, error
10-
//! messages, exit codes and `--version` are therefore unchanged for it, by
11-
//! construction rather than by test.
9+
//! same arguments it saw before this module existed. Its error messages, exit
10+
//! codes and `--version` output are therefore unchanged by construction rather
11+
//! than by test; only `--help` differs, by the [`HELP_NOTE`] it appends.
1212
1313
use std::ffi::OsString;
1414

@@ -23,23 +23,18 @@ use crate::cli::CliOptions;
2323
/// lands there and is never mistaken for it.
2424
const NODE: &str = "node";
2525

26-
/// What the command line asked the binary to do.
27-
#[derive(Debug)]
28-
pub(crate) enum Invocation {
29-
/// Run the consensus node.
30-
Node(CliOptions),
31-
}
26+
/// Appended to `--help` by `CliOptions`. The token never reaches clap, so
27+
/// without this the sub-command would be undiscoverable from the help output.
28+
pub(crate) const HELP_NOTE: &str = "Sub-commands:\n node \
29+
Run the consensus node (assumed when omitted)";
3230

3331
/// Parse the process arguments, exiting the way clap does on a parse error,
3432
/// `--help` or `--version`.
35-
pub(crate) fn parse() -> Invocation {
36-
match try_parse_from(std::env::args_os()) {
37-
Ok(invocation) => invocation,
38-
Err(err) => err.exit(),
39-
}
33+
pub(crate) fn parse() -> CliOptions {
34+
try_parse_from(std::env::args_os()).unwrap_or_else(|err| err.exit())
4035
}
4136

42-
fn try_parse_from<I>(args: I) -> Result<Invocation, clap::Error>
37+
fn try_parse_from<I>(args: I) -> Result<CliOptions, clap::Error>
4338
where
4439
I: IntoIterator,
4540
I::Item: Into<OsString>,
@@ -48,7 +43,7 @@ where
4843
if args.get(1).is_some_and(|arg| arg == NODE) {
4944
args.remove(1);
5045
}
51-
CliOptions::try_parse_from(args).map(Invocation::Node)
46+
CliOptions::try_parse_from(args)
5247
}
5348

5449
#[cfg(test)]
@@ -90,9 +85,7 @@ mod tests {
9085
}
9186

9287
fn node_options(args: &[&str]) -> CliOptions {
93-
let Invocation::Node(options) =
94-
try_parse_from(args.iter().map(OsString::from)).expect("invocation parses");
95-
options
88+
try_parse_from(args.iter().map(OsString::from)).expect("invocation parses")
9689
}
9790

9891
#[test]
@@ -109,6 +102,9 @@ mod tests {
109102
fn node_sub_command_accepts_the_same_flags_as_the_flat_form() {
110103
let flat = node_options(FLAT);
111104
let scoped = node_options(&with_node_token());
105+
// Compared through `Debug`, which the derive prints field by field,
106+
// because `CliOptions` derives no `PartialEq` — and deriving one for a
107+
// test would touch the parser this module deliberately leaves alone.
112108
assert_eq!(format!("{flat:?}"), format!("{scoped:?}"));
113109
}
114110

@@ -134,6 +130,17 @@ mod tests {
134130
assert_eq!(err.kind(), ErrorKind::UnknownArgument);
135131
}
136132

133+
#[test]
134+
fn only_the_leading_node_token_is_stripped() {
135+
// `ethlambda node node --genesis ...`: the second token is left for
136+
// clap, which rejects it like any other stray positional.
137+
let mut args = with_node_token();
138+
args.insert(1, NODE);
139+
let err = try_parse_from(args.iter().map(OsString::from))
140+
.expect_err("only one leading token is a sub-command");
141+
assert_eq!(err.kind(), ErrorKind::UnknownArgument);
142+
}
143+
137144
#[test]
138145
fn missing_required_flag_keeps_the_clap_error_in_both_forms() {
139146
// `--genesis config.yaml` dropped from the front of the flag list.
@@ -176,4 +183,11 @@ mod tests {
176183
assert_eq!(err.kind(), expected);
177184
}
178185
}
186+
187+
#[test]
188+
fn help_documents_the_node_sub_command() {
189+
let err = try_parse_from(["ethlambda", "--help"].iter().map(OsString::from))
190+
.expect_err("--help short-circuits parsing");
191+
assert!(err.to_string().contains(NODE), "{err}");
192+
}
179193
}

bin/ethlambda/src/main.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ use std::{
3232
};
3333
use tokio_util::sync::CancellationToken;
3434

35-
use command::Invocation;
3635
use ethlambda_blockchain::MILLISECONDS_PER_SLOT;
3736
use ethlambda_blockchain::block_builder::ProposerConfig;
3837
use ethlambda_blockchain::key_manager::ValidatorKeyPair;
@@ -80,7 +79,7 @@ async fn main() -> eyre::Result<()> {
8079
tracing::subscriber::set_global_default(subscriber)
8180
.wrap_err("failed to set global tracing subscriber")?;
8281

83-
let Invocation::Node(options) = command::parse();
82+
let options = command::parse();
8483
options.validate_discovery()?;
8584

8685
#[cfg(feature = "shadow-integration")]

0 commit comments

Comments
 (0)