-
Notifications
You must be signed in to change notification settings - Fork 471
Rewatch cli help: do not show build command options in the root help #7715
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
90241cd
ccb5ead
c1a358d
3761d00
abd0db0
f7889aa
0c39710
cd61d37
15a8ab2
75b10d3
8186450
64181e9
f638944
5ee35e9
c554209
7dea5e5
b9d2427
603172f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,25 @@ | ||
use anyhow::Result; | ||
use clap::Parser; | ||
use clap::{CommandFactory, Parser, error::ErrorKind}; | ||
use log::LevelFilter; | ||
use std::{io::Write, path::Path}; | ||
use std::{env, ffi::OsString, io::Write, path::Path}; | ||
|
||
use rescript::{build, cli, cmd, format, lock, watcher}; | ||
|
||
fn main() -> Result<()> { | ||
let args = cli::Cli::parse(); | ||
// Use `args_os` so non-UTF bytes still reach clap for proper error reporting on platforms that | ||
// allow arbitrary argv content. | ||
let raw_args: Vec<OsString> = env::args_os().collect(); | ||
let cli = parse_cli(raw_args).unwrap_or_else(|err| err.exit()); | ||
|
||
let log_level_filter = args.verbose.log_level_filter(); | ||
let log_level_filter = cli.verbose.log_level_filter(); | ||
|
||
env_logger::Builder::new() | ||
.format(|buf, record| writeln!(buf, "{}:\n{}", record.level(), record.args())) | ||
.filter_level(log_level_filter) | ||
.target(env_logger::fmt::Target::Stdout) | ||
.init(); | ||
|
||
let mut command = args.command.unwrap_or(cli::Command::Build(args.build_args)); | ||
let mut command = cli.command; | ||
|
||
if let cli::Command::Build(build_args) = &command { | ||
if build_args.watch { | ||
|
@@ -111,3 +114,151 @@ fn get_lock(folder: &str) -> lock::Lock { | |
acquired_lock => acquired_lock, | ||
} | ||
} | ||
|
||
fn parse_cli(raw_args: Vec<OsString>) -> Result<cli::Cli, clap::Error> { | ||
match cli::Cli::try_parse_from(&raw_args) { | ||
Ok(cli) => Ok(cli), | ||
Err(err) => { | ||
if should_default_to_build(&err, &raw_args) { | ||
let mut fallback_args = raw_args.clone(); | ||
let insert_at = index_after_global_flags(&fallback_args); | ||
fallback_args.insert(insert_at, OsString::from("build")); | ||
|
||
|
||
match cli::Cli::try_parse_from(&fallback_args) { | ||
Ok(cli) => Ok(cli), | ||
Err(fallback_err) => Err(fallback_err), | ||
} | ||
} else { | ||
Err(err) | ||
} | ||
} | ||
} | ||
} | ||
|
||
fn should_default_to_build(err: &clap::Error, args: &[OsString]) -> bool { | ||
match err.kind() { | ||
ErrorKind::MissingSubcommand | ||
| ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand | ||
| ErrorKind::UnknownArgument | ||
| ErrorKind::InvalidSubcommand => { | ||
let first_non_global = first_non_global_arg(args); | ||
match first_non_global { | ||
Some(arg) => !is_known_subcommand(arg), | ||
None => true, | ||
} | ||
} | ||
_ => false, | ||
} | ||
} | ||
|
||
fn index_after_global_flags(args: &[OsString]) -> usize { | ||
let mut idx = 1; | ||
while let Some(arg) = args.get(idx) { | ||
if is_global_flag(arg) { | ||
idx += 1; | ||
} else { | ||
break; | ||
} | ||
} | ||
idx.min(args.len()) | ||
} | ||
|
||
fn is_global_flag(arg: &OsString) -> bool { | ||
matches!( | ||
arg.to_str(), | ||
Some( | ||
"-v" | "-vv" | ||
| "-vvv" | ||
| "-vvvv" | ||
| "-q" | ||
| "-qq" | ||
| "-qqq" | ||
| "-qqqq" | ||
| "--verbose" | ||
| "--quiet" | ||
| "-h" | ||
| "--help" | ||
| "-V" | ||
| "--version" | ||
) | ||
) | ||
} | ||
|
||
fn first_non_global_arg(args: &[OsString]) -> Option<&OsString> { | ||
args.iter().skip(1).find(|arg| !is_global_flag(arg)) | ||
} | ||
|
||
fn is_known_subcommand(arg: &OsString) -> bool { | ||
let Some(arg_str) = arg.to_str() else { | ||
return false; | ||
}; | ||
|
||
cli::Cli::command().get_subcommands().any(|subcommand| { | ||
subcommand.get_name() == arg_str || subcommand.get_all_aliases().any(|alias| alias == arg_str) | ||
}) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use std::ffi::OsString; | ||
|
||
fn parse(args: &[&str]) -> Result<cli::Cli, clap::Error> { | ||
parse_cli(args.iter().map(OsString::from).collect()) | ||
} | ||
|
||
#[test] | ||
fn defaults_to_build_without_args() { | ||
let cli = parse(&["rescript"]).expect("expected default build command"); | ||
|
||
match cli.command { | ||
cli::Command::Build(build_args) => assert_eq!(build_args.folder.folder, "."), | ||
other => panic!("expected build command, got {other:?}"), | ||
} | ||
} | ||
|
||
#[test] | ||
fn defaults_to_build_with_folder_shortcut() { | ||
let cli = parse(&["rescript", "someFolder"]).expect("expected build command"); | ||
|
||
match cli.command { | ||
cli::Command::Build(build_args) => assert_eq!(build_args.folder.folder, "someFolder"), | ||
other => panic!("expected build command, got {other:?}"), | ||
} | ||
} | ||
|
||
#[test] | ||
fn respects_global_flag_before_subcommand() { | ||
let cli = parse(&["rescript", "-v", "watch"]).expect("expected watch command"); | ||
|
||
assert!(matches!(cli.command, cli::Command::Watch(_))); | ||
} | ||
|
||
#[test] | ||
fn invalid_option_for_subcommand_does_not_fallback() { | ||
let err = parse(&["rescript", "watch", "--no-timing"]).expect_err("expected watch parse failure"); | ||
assert_eq!(err.kind(), ErrorKind::UnknownArgument); | ||
} | ||
|
||
#[test] | ||
fn help_flag_does_not_default_to_build() { | ||
let err = parse(&["rescript", "--help"]).expect_err("expected clap help error"); | ||
assert_eq!(err.kind(), ErrorKind::DisplayHelp); | ||
} | ||
|
||
#[test] | ||
fn version_flag_does_not_default_to_build() { | ||
let err = parse(&["rescript", "--version"]).expect_err("expected clap version error"); | ||
assert_eq!(err.kind(), ErrorKind::DisplayVersion); | ||
} | ||
|
||
#[cfg(unix)] | ||
#[test] | ||
fn non_utf_argument_returns_error() { | ||
use std::os::unix::ffi::OsStringExt; | ||
|
||
let args = vec![OsString::from("rescript"), OsString::from_vec(vec![0xff])]; | ||
let err = parse_cli(args).expect_err("expected clap to report invalid utf8"); | ||
assert_eq!(err.kind(), ErrorKind::InvalidUtf8); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes me wonder if we can check if the first argument is any of the verbs, if not prepend the vector with
build
.That way we avoid the double parse in the error case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what I had in the beginning, and it had other issues.
See the previous codex reviews, it took quite a few attempts until everything worked correctly.