diff --git a/integration/fixtures/v1/libexec/invalid-usage b/integration/fixtures/v1/libexec/invalid-usage new file mode 100755 index 0000000..d9dc325 --- /dev/null +++ b/integration/fixtures/v1/libexec/invalid-usage @@ -0,0 +1 @@ +# Usage: diff --git a/integration/validate.bats b/integration/validate.bats new file mode 100644 index 0000000..fd0f3f8 --- /dev/null +++ b/integration/validate.bats @@ -0,0 +1,14 @@ +#!/usr/bin/env bats +load test_helper + +PROJECT_DIR="$SUB_TEST_DIR/v1" + +@test "sub: validates all subcommands in the project directory" { + fixture "v1" + + run $SUB_BIN --name main --absolute "$PROJECT_DIR" --validate + + assert_failure + assert_output "$PROJECT_DIR/libexec/invalid-usage: invalid usage string + found end of input but expected \"{\"" +} diff --git a/src/commands/directory.rs b/src/commands/directory.rs index c07792f..2d7da55 100644 --- a/src/commands/directory.rs +++ b/src/commands/directory.rs @@ -159,4 +159,14 @@ impl<'a> Command for DirectoryCommand<'a> { Ok(0) } + + fn validate(&self) -> Vec<(PathBuf, Error)> { + let mut errors = Vec::new(); + + for subcommand in self.subcommands() { + errors.extend(subcommand.validate()); + } + + errors + } } diff --git a/src/commands/file.rs b/src/commands/file.rs index 3884ea0..7a964eb 100644 --- a/src/commands/file.rs +++ b/src/commands/file.rs @@ -98,4 +98,11 @@ impl<'a> Command for FileCommand<'a> { None => Err(Error::SubCommandInterrupted), } } + + fn validate(&self) -> Vec<(PathBuf, Error)> { + match self.usage.validate() { + Ok(_) => Vec::new(), + Err(e) => vec![(self.path.clone(), e)], + } + } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 55f4412..534ef6d 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,6 +1,7 @@ pub mod file; pub mod directory; +use std::path::PathBuf; use std::os::unix::fs::PermissionsExt; use crate::config::Config; @@ -17,6 +18,7 @@ pub trait Command { fn completions(&self) -> Result; fn invoke(&self) -> Result; fn help(&self) -> Result; + fn validate(&self) -> Vec<(PathBuf, Error)>; } pub fn subcommand(config: &Config, mut cliargs: Vec) -> Result> { diff --git a/src/main.rs b/src/main.rs index 263c4d9..309cb4f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ extern crate sub; extern crate clap; -use clap::{value_parser, Arg, ArgGroup, Command}; +use clap::{value_parser, Arg, ArgAction, ArgGroup, Command}; use std::path::{Path, PathBuf}; use std::process::exit; @@ -16,6 +16,28 @@ fn main() { let config = Config::new(sub_cli_args.name, sub_cli_args.root, sub_cli_args.color, sub_cli_args.infer_long_arguments); + if sub_cli_args.validate { + let top_level_command = match subcommand(&config, Vec::new()) { + Ok(subcommand) => subcommand, + Err(error) => handle_error( + &config, + error, + false, + ), + }; + + let errors = top_level_command.validate(); + for error in &errors { + println!("{}: {}", error.0.display(), print_error(error.1.clone())); + } + + if errors.is_empty() { + exit(0); + } else { + exit(1); + } + } + let user_cli_command = config.user_cli_command(&config.name); let user_cli_args = parse_user_cli_args(&user_cli_command, sub_cli_args.cliargs); @@ -69,6 +91,23 @@ fn main() { } } +fn print_error(error: Error) -> String { + match error { + Error::NoCompletions => "no completions".to_string(), + Error::SubCommandInterrupted => "sub command interrupted".to_string(), + Error::NonExecutable(_) => "non-executable".to_string(), + Error::UnknownSubCommand(name) => format!("unknown sub command '{}'", name), + Error::InvalidUsageString(errors) => { + let mut message = "invalid usage string".to_string(); + for error in errors { + message.push_str(&format!("\n {}", error)); + } + message + } + Error::InvalidUTF8 => "invalid UTF-8".to_string(), + } +} + fn handle_error(config: &Config, error: Error, silent: bool) -> ! { match error { Error::NoCompletions => exit(1), @@ -142,6 +181,13 @@ fn init_sub_cli() -> Command { .value_parser(absolute_path) .help("Sets how to find the root directory as an absolute path"), ) + .arg( + Arg::new("validate") + .long("validate") + .num_args(0) + .action(ArgAction::SetTrue) + .help("Validate that the CLI is correctly configured"), + ) .group( ArgGroup::new("path") .args(["bin", "absolute"]) @@ -160,6 +206,7 @@ struct SubCliArgs { color: Color, root: PathBuf, infer_long_arguments: bool, + validate: bool, cliargs: Vec, } @@ -233,6 +280,8 @@ fn parse_sub_cli_args() -> SubCliArgs { infer_long_arguments: args.get_one::("infer-long-arguments").cloned().unwrap_or(false), + validate: args.get_flag("validate"), + cliargs: args .get_many("cliargs") .map(|cmds| cmds.cloned().collect::>())