From db6705e84ad50d245f79f027e1bb50d44316b239 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Sun, 15 Dec 2024 22:01:33 -0800 Subject: [PATCH 1/6] fix(complete): Deduplicate bash subcmd cases **Problem:** Visible alias completions were causing duplicate case conditions when offering subcommand completions, due to the fact that they share the same function name as the command they are aliasing. **Solution:** Deduplicate them so as to not redefine the exact same case statement logic. --- clap_complete/src/aot/shells/bash.rs | 1 + clap_complete/tests/snapshots/sub_subcommands.bash | 14 -------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/clap_complete/src/aot/shells/bash.rs b/clap_complete/src/aot/shells/bash.rs index d283401949d..6d0be2d297c 100644 --- a/clap_complete/src/aot/shells/bash.rs +++ b/clap_complete/src/aot/shells/bash.rs @@ -132,6 +132,7 @@ fn subcommand_details(cmd: &Command) -> String { .collect::>(); scs.sort(); + scs.dedup(); subcmd_dets.extend(scs.iter().map(|sc| { format!( diff --git a/clap_complete/tests/snapshots/sub_subcommands.bash b/clap_complete/tests/snapshots/sub_subcommands.bash index a6a4663503d..6651d025d91 100644 --- a/clap_complete/tests/snapshots/sub_subcommands.bash +++ b/clap_complete/tests/snapshots/sub_subcommands.bash @@ -152,20 +152,6 @@ _my-app() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - my__app__some_cmd) - opts="-h -V --help --version sub_cmd help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; my__app__some_cmd__help) opts="sub_cmd help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then From 75af6df57ec4eb0828e3939797332ab7628323b8 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Dec 2024 10:46:53 -0600 Subject: [PATCH 2/6] docs: Update changelog --- clap_complete/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/clap_complete/CHANGELOG.md b/clap_complete/CHANGELOG.md index b36edc3f8f5..7184046a978 100644 --- a/clap_complete/CHANGELOG.md +++ b/clap_complete/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] - ReleaseDate +### Fixes + +- *(bash)* For AOT completions, de-duplicate when subcommand aliases are used + ## [4.5.38] - 2024-11-13 ## [4.5.37] - 2024-11-04 From a005f6695652393f997034264160639d5a9ef568 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Dec 2024 10:47:08 -0600 Subject: [PATCH 3/6] chore: Release --- Cargo.lock | 2 +- clap_complete/CHANGELOG.md | 5 ++++- clap_complete/Cargo.toml | 2 +- clap_complete/README.md | 8 ++++---- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd3ef88d449..96ee44f6e31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,7 +474,7 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.38" +version = "4.5.39" dependencies = [ "automod", "clap 4.5.23", diff --git a/clap_complete/CHANGELOG.md b/clap_complete/CHANGELOG.md index 7184046a978..0880c55ec3e 100644 --- a/clap_complete/CHANGELOG.md +++ b/clap_complete/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] - ReleaseDate +## [4.5.39] - 2024-12-16 + ### Fixes - *(bash)* For AOT completions, de-duplicate when subcommand aliases are used @@ -490,7 +492,8 @@ MSRV changed to 1.64.0 ## [3.0.1] - 2022-01-03 -[Unreleased]: https://github.com/clap-rs/clap/compare/clap_complete-v4.5.38...HEAD +[Unreleased]: https://github.com/clap-rs/clap/compare/clap_complete-v4.5.39...HEAD +[4.5.39]: https://github.com/clap-rs/clap/compare/clap_complete-v4.5.38...clap_complete-v4.5.39 [4.5.38]: https://github.com/clap-rs/clap/compare/clap_complete-v4.5.37...clap_complete-v4.5.38 [4.5.37]: https://github.com/clap-rs/clap/compare/clap_complete-v4.5.36...clap_complete-v4.5.37 [4.5.36]: https://github.com/clap-rs/clap/compare/clap_complete-v4.5.35...clap_complete-v4.5.36 diff --git a/clap_complete/Cargo.toml b/clap_complete/Cargo.toml index 9082e74796b..940a209863d 100644 --- a/clap_complete/Cargo.toml +++ b/clap_complete/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clap_complete" -version = "4.5.38" +version = "4.5.39" description = "Generate shell completion scripts for your clap::Command" categories = ["command-line-interface"] keywords = [ diff --git a/clap_complete/README.md b/clap_complete/README.md index 24817dc256c..9906c4fee94 100644 --- a/clap_complete/README.md +++ b/clap_complete/README.md @@ -5,16 +5,16 @@ [![Crates.io](https://img.shields.io/crates/v/clap_complete?style=flat-square)](https://crates.io/crates/clap_complete) [![Crates.io](https://img.shields.io/crates/d/clap_complete?style=flat-square)](https://crates.io/crates/clap_complete) -[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](https://github.com/clap-rs/clap/blob/clap_complete-v4.5.38/LICENSE-APACHE) -[![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://github.com/clap-rs/clap/blob/clap_complete-v4.5.38/LICENSE-MIT) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](https://github.com/clap-rs/clap/blob/clap_complete-v4.5.39/LICENSE-APACHE) +[![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://github.com/clap-rs/clap/blob/clap_complete-v4.5.39/LICENSE-MIT) Dual-licensed under [Apache 2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT). 1. [About](#about) 2. [API Reference](https://docs.rs/clap_complete) 3. [Questions & Discussions](https://github.com/clap-rs/clap/discussions) -4. [CONTRIBUTING](https://github.com/clap-rs/clap/blob/clap_complete-v4.5.38/clap_complete/CONTRIBUTING.md) -5. [Sponsors](https://github.com/clap-rs/clap/blob/clap_complete-v4.5.38/README.md#sponsors) +4. [CONTRIBUTING](https://github.com/clap-rs/clap/blob/clap_complete-v4.5.39/clap_complete/CONTRIBUTING.md) +5. [Sponsors](https://github.com/clap-rs/clap/blob/clap_complete-v4.5.39/README.md#sponsors) ## About From 70f66294daa486d23898c8ba7936034b6c2532f5 Mon Sep 17 00:00:00 2001 From: Brian Heise Date: Sat, 21 Dec 2024 17:18:03 +0900 Subject: [PATCH 4/6] adds infrastructure for extracing hardcoded texts --- Cargo.lock | 37 +++++++++- clap_builder/Cargo.toml | 2 + clap_builder/src/builder/command.rs | 13 ++-- clap_builder/src/builder/styled_str.rs | 7 ++ clap_builder/src/error/mod.rs | 13 ++-- clap_builder/src/lib.rs | 1 + clap_builder/src/output/help_template.rs | 8 +-- clap_builder/src/output/usage.rs | 4 +- clap_builder/src/text_provider.rs | 48 +++++++++++++ clap_builder/src/util/mod.rs | 2 + clap_builder/src/util/template.rs | 90 ++++++++++++++++++++++++ clap_builder/texts/en.yaml | 10 +++ 12 files changed, 217 insertions(+), 18 deletions(-) create mode 100644 clap_builder/src/text_provider.rs create mode 100644 clap_builder/src/util/template.rs create mode 100644 clap_builder/texts/en.yaml diff --git a/Cargo.lock b/Cargo.lock index 96ee44f6e31..1016b046b79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,6 +133,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" + [[package]] name = "arrayvec" version = "0.5.2" @@ -464,6 +470,8 @@ dependencies = [ "backtrace", "clap_lex 0.7.4", "color-print", + "lazy_static", + "serde_yml", "static_assertions", "strsim", "terminal_size 0.4.0", @@ -1475,6 +1483,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2889,6 +2907,21 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serde_yml" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" +dependencies = [ + "indexmap", + "itoa", + "libyml", + "memchr", + "ryu", + "serde", + "version_check", +] + [[package]] name = "sha2" version = "0.10.8" @@ -3651,9 +3684,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vt100" diff --git a/clap_builder/Cargo.toml b/clap_builder/Cargo.toml index be41a3277cf..75d997e4490 100644 --- a/clap_builder/Cargo.toml +++ b/clap_builder/Cargo.toml @@ -67,6 +67,8 @@ anstyle = "1.0.8" terminal_size = { version = "0.4.0", optional = true } backtrace = { version = "0.3.73", optional = true } unicode-width = { version = "0.2.0", optional = true } +serde_yml = "0.0.12" +lazy_static = "1.5.0" [dev-dependencies] static_assertions = "1.1.0" diff --git a/clap_builder/src/builder/command.rs b/clap_builder/src/builder/command.rs index b019d0085ea..289444b9f39 100644 --- a/clap_builder/src/builder/command.rs +++ b/clap_builder/src/builder/command.rs @@ -26,6 +26,7 @@ use crate::mkeymap::MKeyMap; use crate::output::fmt::Stream; use crate::output::{fmt::Colorizer, write_help, Usage}; use crate::parser::{ArgMatcher, ArgMatches, Parser}; +use crate::text_provider::DEFAULT_TEXT_PROVIDER; use crate::util::ChildGraph; use crate::util::{color::ColorChoice, Id}; use crate::{Error, INTERNAL_ERROR_MSG}; @@ -878,6 +879,8 @@ impl Command { let usage = Usage::new(self); write_help(&mut styled, self, &usage, false); + styled.render_text(&*DEFAULT_TEXT_PROVIDER); + let c = Colorizer::new(Stream::Stdout, color).with_content(styled); c.print() } @@ -905,7 +908,7 @@ impl Command { let mut styled = StyledStr::new(); let usage = Usage::new(self); write_help(&mut styled, self, &usage, true); - + styled.render_text(&*DEFAULT_TEXT_PROVIDER); let c = Colorizer::new(Stream::Stdout, color).with_content(styled); c.print() } @@ -4729,7 +4732,7 @@ impl Command { .help("Print help (see more with '--help')") .long_help("Print help (see a summary with '-h')"); } else { - arg = arg.help("Print help"); + arg = arg.help("{clap.help.short-help}"); } // Avoiding `arg_internal` to not be sensitive to `next_help_heading` / // `next_display_order` @@ -4741,7 +4744,7 @@ impl Command { .short('V') .long("version") .action(ArgAction::Version) - .help("Print version"); + .help("{clap.version.short-help}"); // Avoiding `arg_internal` to not be sensitive to `next_help_heading` / // `next_display_order` self.args.push(arg); @@ -4749,7 +4752,7 @@ impl Command { if !self.is_set(AppSettings::DisableHelpSubcommand) { debug!("Command::_check_help_and_version: Building help subcommand"); - let help_about = "Print this message or the help of the given subcommand(s)"; + let help_about = "{clap.help.about}"; let mut help_subcmd = if expand_help_tree { // Slow code path to recursively clone all other subcommand subtrees under help @@ -4771,7 +4774,7 @@ impl Command { Arg::new("subcommand") .action(ArgAction::Append) .num_args(..) - .value_name("COMMAND") + .value_name("{clap.help.command.value-name}") .help("Print help for the subcommand(s)"), ) }; diff --git a/clap_builder/src/builder/styled_str.rs b/clap_builder/src/builder/styled_str.rs index 2b08bf42bea..c4ed81c12fb 100644 --- a/clap_builder/src/builder/styled_str.rs +++ b/clap_builder/src/builder/styled_str.rs @@ -1,5 +1,7 @@ #![cfg_attr(not(feature = "usage"), allow(dead_code))] +use crate::{text_provider::TextProvider, util::interpolate}; + /// Terminal-styling container /// /// Styling may be encoded as [ANSI Escape Code](https://en.wikipedia.org/wiki/ANSI_escape_code) @@ -29,6 +31,11 @@ impl StyledStr { Self(String::new()) } + /// Interpolate values into the text + pub fn render_text(&mut self, texts: &impl TextProvider) { + self.0 = interpolate(&self.0, texts); + } + /// Display using [ANSI Escape Code](https://en.wikipedia.org/wiki/ANSI_escape_code) styling #[cfg(feature = "color")] pub fn ansi(&self) -> impl std::fmt::Display + '_ { diff --git a/clap_builder/src/error/mod.rs b/clap_builder/src/error/mod.rs index 7bd627375c1..54e318b7d52 100644 --- a/clap_builder/src/error/mod.rs +++ b/clap_builder/src/error/mod.rs @@ -17,7 +17,7 @@ use std::{ }; // Internal -use crate::builder::StyledStr; +use crate::{builder::StyledStr, text_provider::{TextProvider, DEFAULT_TEXT_PROVIDER}}; use crate::builder::Styles; use crate::output::fmt::Colorizer; use crate::output::fmt::Stream; @@ -232,7 +232,7 @@ impl Error { /// or prints to `stdout` and exits with a status of `0`. pub fn exit(&self) -> ! { // Swallow broken pipe errors - let _ = self.print(); + let _ = self.print(&*DEFAULT_TEXT_PROVIDER); std::process::exit(self.exit_code()); } @@ -253,8 +253,10 @@ impl Error { /// }, /// }; /// ``` - pub fn print(&self) -> io::Result<()> { - let style = self.formatted(); + pub fn print(&self, texts: &impl TextProvider) -> io::Result<()> { + let mut style = self.formatted().into_owned(); + style.render_text(texts); + let color_when = if matches!( self.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand, @@ -263,7 +265,8 @@ impl Error { } else { self.inner.color_when }; - let c = Colorizer::new(self.stream(), color_when).with_content(style.into_owned()); + + let c = Colorizer::new(self.stream(), color_when).with_content(style); c.print() } diff --git a/clap_builder/src/lib.rs b/clap_builder/src/lib.rs index 602c2fb654e..a02fbad7f26 100644 --- a/clap_builder/src/lib.rs +++ b/clap_builder/src/lib.rs @@ -40,6 +40,7 @@ mod derive; pub mod builder; pub mod error; pub mod parser; +pub mod text_provider; mod mkeymap; mod output; diff --git a/clap_builder/src/output/help_template.rs b/clap_builder/src/output/help_template.rs index 3b3fc3d9840..8e830530952 100644 --- a/clap_builder/src/output/help_template.rs +++ b/clap_builder/src/output/help_template.rs @@ -198,7 +198,7 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { "usage-heading" => { let _ = write!( self.writer, - "{}Usage:{}", + "{}{{clap.usage.header-text}}:{}", self.styles.get_usage().render(), self.styles.get_usage().render_reset() ); @@ -401,7 +401,7 @@ impl HelpTemplate<'_, '_> { self.writer.push_str("\n\n"); } first = false; - let default_help_heading = Str::from("Commands"); + let default_help_heading = Str::from("{clap.commands-list-header}"); let help_heading = self .cmd .get_subcommand_help_heading() @@ -417,7 +417,7 @@ impl HelpTemplate<'_, '_> { } first = false; // Write positional args if any - let help_heading = "Arguments"; + let help_heading = "{clap.arguments-list-header}"; let _ = write!(self.writer, "{header}{help_heading}:{header:#}\n",); self.write_args(&pos, "Arguments", positional_sort_key); } @@ -427,7 +427,7 @@ impl HelpTemplate<'_, '_> { self.writer.push_str("\n\n"); } first = false; - let help_heading = "Options"; + let help_heading = "{clap.options-list-header}"; let _ = write!(self.writer, "{header}{help_heading}:{header:#}\n",); self.write_args(&non_pos, "Options", option_sort_key); } diff --git a/clap_builder/src/output/usage.rs b/clap_builder/src/output/usage.rs index 7c4ddc5ffca..1657cc35246 100644 --- a/clap_builder/src/output/usage.rs +++ b/clap_builder/src/output/usage.rs @@ -13,7 +13,7 @@ use crate::util::ChildGraph; use crate::util::FlatSet; use crate::util::Id; -static DEFAULT_SUB_VALUE_NAME: &str = "COMMAND"; +static DEFAULT_SUB_VALUE_NAME: &str = "{clap.default-sub-value-name}"; const USAGE_SEP: &str = "\n "; pub(crate) struct Usage<'cmd> { @@ -161,7 +161,7 @@ impl Usage<'_> { } if used.is_empty() && self.needs_options_tag() { - let _ = write!(styled, "{placeholder}[OPTIONS]{placeholder:#} ",); + let _ = write!(styled, "{placeholder}[{{clap.usage.options}}]{placeholder:#} ",); } self.write_args(styled, used, !incl_reqs); diff --git a/clap_builder/src/text_provider.rs b/clap_builder/src/text_provider.rs new file mode 100644 index 00000000000..9c78b925766 --- /dev/null +++ b/clap_builder/src/text_provider.rs @@ -0,0 +1,48 @@ +//! Utilities for loading help texts from external sources. + +use std::collections::HashMap; +use lazy_static::lazy_static; +use crate::INTERNAL_ERROR_MSG; + +/// Implement this trait for any type that can provide texts to render into the CLI output. +/// Useful for internationalizing applications. +pub trait TextProvider { + + /// Provided a key referring to a text to render in the application, retrieve the text. For + /// internationalized applications, the internal state of the implementing type should be + /// aware of the current locale of the application. + fn get(&self, key: &str) -> &str; +} + +/// A simple [`TextProvider`] implementation which loads Clap's default English texts. +pub struct DefaultTextProvider(HashMap); + +impl DefaultTextProvider { + /// Initialize the default [`TextProvider`], which includes the default English texts of Clap. + pub fn new() -> Self { + Self::default() + } +} + +impl Default for DefaultTextProvider { + fn default() -> Self { + let text_raw = include_str!("../texts/en.yaml"); + let parsed: HashMap = + serde_yml::from_str(text_raw).expect(INTERNAL_ERROR_MSG); + + Self(parsed) + } +} + +impl TextProvider for DefaultTextProvider { + fn get(&self, key: &str) -> &str { + self.0.get(key).expect(INTERNAL_ERROR_MSG) + } +} + +lazy_static! { + /// Initializes the [`DefaultTextProvider`] statically to avoid initializing multiple times. + pub static ref DEFAULT_TEXT_PROVIDER: DefaultTextProvider = { + DefaultTextProvider::new() + }; +} \ No newline at end of file diff --git a/clap_builder/src/util/mod.rs b/clap_builder/src/util/mod.rs index 8008db5acf2..7b189aede75 100644 --- a/clap_builder/src/util/mod.rs +++ b/clap_builder/src/util/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod flat_set; mod graph; mod id; mod str_to_bool; +mod template; pub use self::id::Id; @@ -18,6 +19,7 @@ pub(crate) use self::graph::ChildGraph; pub(crate) use self::str_to_bool::str_to_bool; pub(crate) use self::str_to_bool::FALSE_LITERALS; pub(crate) use self::str_to_bool::TRUE_LITERALS; +pub(crate) use self::template::interpolate; pub(crate) mod color; diff --git a/clap_builder/src/util/template.rs b/clap_builder/src/util/template.rs new file mode 100644 index 00000000000..efc99903e86 --- /dev/null +++ b/clap_builder/src/util/template.rs @@ -0,0 +1,90 @@ +use crate::text_provider::TextProvider; + +/// Simple template engine suitable for interpolating texts only, without any bells or whistles. +pub(crate) fn interpolate(template: &str, texts: &impl TextProvider) -> String { + let mut with_texts = String::new(); + let mut parse_state = ParseState::NotStarted; + + for (i, c) in template.char_indices() { + parse_state = parse_key_template(i, c, parse_state).expect("Text interpolation failed"); + + if let ParseState::Complete(s, e) = parse_state { + let key = &template[s..e]; + with_texts.push_str(texts.get(key)); + } + + if let ParseState::NotStarted = parse_state { + with_texts.push(c); + } + } + + with_texts +} + +/// Given a [char] and a [`ParseState`], determine the next state. +/// +/// ## Inputs +/// +/// - `idx` - the index of the current [char], which is used for tracking the span of the current key +/// - `current` - the current [char], which we use for determining the next [`ParseState`] +/// - `current_state` - what stage in parsing an interpolation are we at +pub(crate) fn parse_key_template( + idx: usize, + current: char, + current_state: ParseState, +) -> Result { + match current_state { + // We haven't started parsing an interpolation or we just finished one, but we found an open brace -> transition to Started + ParseState::NotStarted | ParseState::Complete(..) if current == '{' => Ok(ParseState::Started), + // We have not started parsing an interpolation and have not encountered an open brace, so maintain current state + ParseState::NotStarted => Ok(ParseState::NotStarted), + // We have encountered the the open brace but have not found the start of a key yet, so maintain current state + ParseState::Started if current.is_ascii_whitespace() => Ok(ParseState::Started), + // We have encountered an open brace and have no encountered the start of the key, so transition to KeyStarted + ParseState::Started if current.is_ascii_alphanumeric() => Ok(ParseState::KeyStarted(idx)), + // If we find any other characters besides whitespace or a valid start char, this is an error and we fail + ParseState::Started => { + Err("Chars in braces prior to the start of the key must only be whitespace") + } + // If we have started parsing a key and we encounter whitespace, then we're done and transition to KeyFinished + ParseState::KeyStarted(s) if current.is_ascii_whitespace() => { + Ok(ParseState::KeyFinished(s)) + } + // If we are in KeyFinished or KeyStarted state and encounter a closing brance, then we are finished and transition to Complete + ParseState::KeyFinished(s) | ParseState::KeyStarted(s) if current == '}' => { + Ok(ParseState::Complete(s, idx)) + } + // If key hasstarted and we encounter any valid key chars, then we maintain current state and continue + ParseState::KeyStarted(s) + if current.is_ascii_alphanumeric() || current == '-' || current == '.' => + { + Ok(ParseState::KeyStarted(s)) + } + // If we're in key started and haven't hit an earlier pattern, that means we hit an invalid char + ParseState::KeyStarted(..) => Err("Invalid char encountered while parsing text key"), // TODO: should report more info to help diagnose issues + // If the key is finished and we found anything other than whitespace or a closing brace, that's an error + ParseState::KeyFinished(..) => Err("Failed to find closing } character for text interpolation"), + // We are in a completed state and need to transition back to NotStarted + ParseState::Complete(..) => Ok(ParseState::NotStarted), + } +} + +/// A set of states to track what stage of parsing an interplation we're at +#[derive(Debug)] +pub(crate) enum ParseState { + /// We haven't located the opening brace yet + NotStarted, + + /// We have encountered the opnening brace, but have not yet seen the first valid key character. In other words, we've + /// only seen whitespace up to now. + Started, + + /// We have encountered the first valid key character, so we have started parsing a key + KeyStarted(usize), + + /// We encountered whitespace after having started parsing a key, so the key is finished + KeyFinished(usize), + + /// We have encountered the closing brace, so the parse is complete + Complete(usize, usize), +} diff --git a/clap_builder/texts/en.yaml b/clap_builder/texts/en.yaml new file mode 100644 index 00000000000..24776cd7af3 --- /dev/null +++ b/clap_builder/texts/en.yaml @@ -0,0 +1,10 @@ +clap.usage.header-text: Usage +clap.usage.options: OPTIONS +clap.help.command.value-name: COMMAND +clap.default-sub-value-name: COMMAND +clap.commands-list-header: Commands +clap.help.about: Print this message or the help of the given subcommand(s) +clap.arguments-list-header: Arguments +clap.options-list-header: Options +clap.help.short-help: Print help +clap.version.short-help: Print version \ No newline at end of file From fa3e5062b6247b66c7bdece34249d5d0a9096907 Mon Sep 17 00:00:00 2001 From: Brian Heise Date: Sun, 22 Dec 2024 14:13:00 +0900 Subject: [PATCH 5/6] pass in default text provide to existing examples and tests --- clap_bench/benches/complex.rs | 4 ++-- clap_bench/benches/empty.rs | 3 ++- clap_bench/benches/ripgrep.rs | 8 ++++--- clap_bench/benches/rustup.rs | 6 +++-- clap_bench/benches/simple.rs | 8 ++++--- clap_builder/src/builder/command.rs | 16 +++++++------ clap_builder/src/derive.rs | 28 ++++++++++++++-------- clap_builder/src/error/mod.rs | 8 +++---- clap_builder/src/text_provider.rs | 4 ++-- clap_builder/src/util/template.rs | 2 +- clap_complete/examples/completion.rs | 4 ++-- clap_complete/examples/exhaustive.rs | 4 ++-- clap_complete_nushell/examples/test.rs | 3 ++- examples/derive_ref/augment_args.rs | 6 ++--- examples/derive_ref/augment_subcommands.rs | 6 ++--- examples/git.rs | 4 ++-- examples/multicall-busybox.rs | 4 ++-- examples/multicall-hostname.rs | 4 ++-- examples/pacman.rs | 4 ++-- examples/tutorial_builder/02_apps.rs | 4 ++-- examples/tutorial_derive/04_04_custom.rs | 7 +++--- src/bin/stdio-fixture.rs | 4 +++- tests/builder/app_settings.rs | 4 ++++ tests/builder/utils.rs | 3 ++- tests/derive/generic.rs | 16 ++++++------- 25 files changed, 95 insertions(+), 69 deletions(-) diff --git a/clap_bench/benches/complex.rs b/clap_bench/benches/complex.rs index 3e590e1e490..fe2072108de 100644 --- a/clap_bench/benches/complex.rs +++ b/clap_bench/benches/complex.rs @@ -1,6 +1,6 @@ #![allow(elided_lifetimes_in_paths)] // needed for divan -use clap::{arg, ArgMatches, Command}; +use clap::{arg, text_provider::DEFAULT_TEXT_PROVIDER, ArgMatches, Command}; macro_rules! create_app { () => {{ @@ -48,7 +48,7 @@ fn build() -> Command { #[divan::bench(args=COMPLEX_ARGS)] fn startup(args: &Args) -> ArgMatches { - create_app!().get_matches_from(args.args()) + create_app!().get_matches_from(args.args(), &*DEFAULT_TEXT_PROVIDER) } #[divan::bench] diff --git a/clap_bench/benches/empty.rs b/clap_bench/benches/empty.rs index 36c6b2c89b0..6f8aab012fe 100644 --- a/clap_bench/benches/empty.rs +++ b/clap_bench/benches/empty.rs @@ -1,5 +1,6 @@ #![allow(elided_lifetimes_in_paths)] // needed for divan +use clap::text_provider::DEFAULT_TEXT_PROVIDER; use clap::ArgMatches; use clap::Command; @@ -16,7 +17,7 @@ fn build() -> Command { #[divan::bench] fn startup() -> ArgMatches { - create_app!().get_matches_from(vec![""]) + create_app!().get_matches_from(vec![""], &*DEFAULT_TEXT_PROVIDER) } #[divan::bench] diff --git a/clap_bench/benches/ripgrep.rs b/clap_bench/benches/ripgrep.rs index 1bbccdeb0d1..e76e9080ef0 100644 --- a/clap_bench/benches/ripgrep.rs +++ b/clap_bench/benches/ripgrep.rs @@ -41,11 +41,13 @@ mod render_help { } mod startup { + use clap::text_provider::DEFAULT_TEXT_PROVIDER; + use super::{app_short, ArgMatches}; #[divan::bench] fn simple() -> ArgMatches { - app_short().get_matches_from(vec!["rg", "pat"]) + app_short().get_matches_from(vec!["rg", "pat"], &*DEFAULT_TEXT_PROVIDER) } #[divan::bench] @@ -62,7 +64,7 @@ mod startup { "-C5", "--follow", "-e some", - ]) + ], &*DEFAULT_TEXT_PROVIDER) } #[divan::bench] @@ -228,7 +230,7 @@ mod startup { "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", - ]) + ], &*DEFAULT_TEXT_PROVIDER) } } diff --git a/clap_bench/benches/rustup.rs b/clap_bench/benches/rustup.rs index a8625c4d4ea..04f7b0d502a 100644 --- a/clap_bench/benches/rustup.rs +++ b/clap_bench/benches/rustup.rs @@ -12,16 +12,18 @@ fn build() -> Command { } mod startup { + use clap::text_provider::DEFAULT_TEXT_PROVIDER; + use super::{build_cli, ArgMatches}; #[divan::bench] fn empty() -> ArgMatches { - build_cli().get_matches_from([""]) + build_cli().get_matches_from([""], &*DEFAULT_TEXT_PROVIDER) } #[divan::bench] fn sc() -> ArgMatches { - build_cli().get_matches_from(["rustup override add stable"]) + build_cli().get_matches_from(["rustup override add stable"], &*DEFAULT_TEXT_PROVIDER) } } diff --git a/clap_bench/benches/simple.rs b/clap_bench/benches/simple.rs index c341c936b88..d765268ec74 100644 --- a/clap_bench/benches/simple.rs +++ b/clap_bench/benches/simple.rs @@ -20,21 +20,23 @@ fn build() -> Command { } mod startup { + use clap::text_provider::DEFAULT_TEXT_PROVIDER; + use super::{arg, ArgMatches, Command}; #[divan::bench] fn flag() -> ArgMatches { - create_app!().get_matches_from(vec!["myprog", "-f"]) + create_app!().get_matches_from(vec!["myprog", "-f"], &*DEFAULT_TEXT_PROVIDER) } #[divan::bench] fn opt() -> ArgMatches { - create_app!().get_matches_from(vec!["myprog", "-o", "option1"]) + create_app!().get_matches_from(vec!["myprog", "-o", "option1"], &*DEFAULT_TEXT_PROVIDER) } #[divan::bench] fn pos() -> ArgMatches { - create_app!().get_matches_from(vec!["myprog", "arg1"]) + create_app!().get_matches_from(vec!["myprog", "arg1"], &*DEFAULT_TEXT_PROVIDER) } } diff --git a/clap_builder/src/builder/command.rs b/clap_builder/src/builder/command.rs index 289444b9f39..3311871e4ed 100644 --- a/clap_builder/src/builder/command.rs +++ b/clap_builder/src/builder/command.rs @@ -26,6 +26,7 @@ use crate::mkeymap::MKeyMap; use crate::output::fmt::Stream; use crate::output::{fmt::Colorizer, write_help, Usage}; use crate::parser::{ArgMatcher, ArgMatches, Parser}; +use crate::text_provider::TextProvider; use crate::text_provider::DEFAULT_TEXT_PROVIDER; use crate::util::ChildGraph; use crate::util::{color::ColorChoice, Id}; @@ -610,8 +611,8 @@ impl Command { /// [`env::args_os`]: std::env::args_os() /// [`Command::try_get_matches_from_mut`]: Command::try_get_matches_from_mut() #[inline] - pub fn get_matches(self) -> ArgMatches { - self.get_matches_from(env::args_os()) + pub fn get_matches(self, texts: &impl TextProvider) -> ArgMatches { + self.get_matches_from(env::args_os(), texts) } /// Parse [`env::args_os`], [exiting][Error::exit] on failure. @@ -634,9 +635,9 @@ impl Command { /// ``` /// [`env::args_os`]: std::env::args_os() /// [`Command::get_matches`]: Command::get_matches() - pub fn get_matches_mut(&mut self) -> ArgMatches { + pub fn get_matches_mut(&mut self, texts: &impl TextProvider) -> ArgMatches { self.try_get_matches_from_mut(env::args_os()) - .unwrap_or_else(|e| e.exit()) + .unwrap_or_else(|e| e.exit(texts)) } /// Parse [`env::args_os`], returning a [`clap::Result`] on failure. @@ -705,14 +706,14 @@ impl Command { /// [`Command::get_matches`]: Command::get_matches() /// [`clap::Result`]: Result /// [`Vec`]: std::vec::Vec - pub fn get_matches_from(mut self, itr: I) -> ArgMatches + pub fn get_matches_from(mut self, itr: I, texts: &impl TextProvider) -> ArgMatches where I: IntoIterator, T: Into + Clone, { self.try_get_matches_from_mut(itr).unwrap_or_else(|e| { drop(self); - e.exit() + e.exit(texts) }) } @@ -815,7 +816,6 @@ impl Command { { let mut raw_args = clap_lex::RawArgs::new(itr); let mut cursor = raw_args.cursor(); - if self.settings.is_set(AppSettings::Multicall) { if let Some(argv0) = raw_args.next_os(&mut cursor) { let argv0 = Path::new(&argv0); @@ -4281,7 +4281,9 @@ impl Command { // do the real parsing let mut parser = Parser::new(self); + if let Err(error) = parser.get_matches_with(&mut matcher, raw_args, args_cursor) { + if self.is_set(AppSettings::IgnoreErrors) && error.use_stderr() { debug!("Command::_do_parse: ignoring error: {error}"); } else { diff --git a/clap_builder/src/derive.rs b/clap_builder/src/derive.rs index 06a94015b23..e67e860d41c 100644 --- a/clap_builder/src/derive.rs +++ b/clap_builder/src/derive.rs @@ -2,6 +2,7 @@ //! macros in `clap_derive`. use crate::builder::PossibleValue; +use crate::text_provider::{TextProvider, DEFAULT_TEXT_PROVIDER}; use crate::{ArgMatches, Command, Error}; use std::ffi::OsString; @@ -28,15 +29,22 @@ use std::ffi::OsString; pub trait Parser: FromArgMatches + CommandFactory + Sized { /// Parse from `std::env::args_os()`, [exit][Error::exit] on error. fn parse() -> Self { - let mut matches = ::command().get_matches(); + Self::parse_with_texts(&*DEFAULT_TEXT_PROVIDER) + } + + /// Parse from `std::env::args_os()`, [exit][Error::exit] on error, but replace the default Clap [`TextProvider`] with + /// a custom implementation. + fn parse_with_texts(texts: &impl TextProvider) -> Self { + let mut matches = ::command().get_matches(texts); let res = ::from_arg_matches_mut(&mut matches) .map_err(format_error::); + match res { Ok(s) => s, Err(e) => { // Since this is more of a development-time error, we aren't doing as fancy of a quit // as `get_matches` - e.exit() + e.exit(texts) } } } @@ -48,12 +56,12 @@ pub trait Parser: FromArgMatches + CommandFactory + Sized { } /// Parse from iterator, [exit][Error::exit] on error. - fn parse_from(itr: I) -> Self + fn parse_from(itr: I, texts: &impl TextProvider) -> Self where I: IntoIterator, T: Into + Clone, { - let mut matches = ::command().get_matches_from(itr); + let mut matches = ::command().get_matches_from(itr, texts); let res = ::from_arg_matches_mut(&mut matches) .map_err(format_error::); match res { @@ -61,7 +69,7 @@ pub trait Parser: FromArgMatches + CommandFactory + Sized { Err(e) => { // Since this is more of a development-time error, we aren't doing as fancy of a quit // as `get_matches_from` - e.exit() + e.exit(texts) } } } @@ -81,18 +89,18 @@ pub trait Parser: FromArgMatches + CommandFactory + Sized { /// Unlike [`Parser::parse`], this works with an existing instance of `self`. /// The assumption is that all required fields are already provided and any [`Args`] or /// [`Subcommand`]s provided by the user will modify only what is specified. - fn update_from(&mut self, itr: I) + fn update_from(&mut self, itr: I, texts: &impl TextProvider) where I: IntoIterator, T: Into + Clone, { - let mut matches = ::command_for_update().get_matches_from(itr); + let mut matches = ::command_for_update().get_matches_from(itr, texts); let res = ::update_from_arg_matches_mut(self, &mut matches) .map_err(format_error::); if let Err(e) = res { // Since this is more of a development-time error, we aren't doing as fancy of a quit // as `get_matches_from` - e.exit() + e.exit(texts) } } @@ -321,12 +329,12 @@ impl Parser for Box { ::try_parse().map(Box::new) } - fn parse_from(itr: I) -> Self + fn parse_from(itr: I, texts: &impl TextProvider) -> Self where I: IntoIterator, It: Into + Clone, { - Box::new(::parse_from(itr)) + Box::new(::parse_from(itr, texts)) } fn try_parse_from(itr: I) -> Result diff --git a/clap_builder/src/error/mod.rs b/clap_builder/src/error/mod.rs index 54e318b7d52..44623813b0c 100644 --- a/clap_builder/src/error/mod.rs +++ b/clap_builder/src/error/mod.rs @@ -17,7 +17,6 @@ use std::{ }; // Internal -use crate::{builder::StyledStr, text_provider::{TextProvider, DEFAULT_TEXT_PROVIDER}}; use crate::builder::Styles; use crate::output::fmt::Colorizer; use crate::output::fmt::Stream; @@ -25,6 +24,7 @@ use crate::parser::features::suggestions; use crate::util::FlatMap; use crate::util::{color::ColorChoice, SUCCESS_CODE, USAGE_CODE}; use crate::Command; +use crate::{builder::StyledStr, text_provider::TextProvider}; #[cfg(feature = "error-context")] mod context; @@ -230,9 +230,9 @@ impl Error { /// /// Depending on the error kind, this either prints to `stderr` and exits with a status of `2` /// or prints to `stdout` and exits with a status of `0`. - pub fn exit(&self) -> ! { + pub fn exit(&self, texts: &impl TextProvider) -> ! { // Swallow broken pipe errors - let _ = self.print(&*DEFAULT_TEXT_PROVIDER); + let _ = self.print(texts); std::process::exit(self.exit_code()); } @@ -265,7 +265,7 @@ impl Error { } else { self.inner.color_when }; - + let c = Colorizer::new(self.stream(), color_when).with_content(style); c.print() } diff --git a/clap_builder/src/text_provider.rs b/clap_builder/src/text_provider.rs index 9c78b925766..535334e88d1 100644 --- a/clap_builder/src/text_provider.rs +++ b/clap_builder/src/text_provider.rs @@ -11,7 +11,7 @@ pub trait TextProvider { /// Provided a key referring to a text to render in the application, retrieve the text. For /// internationalized applications, the internal state of the implementing type should be /// aware of the current locale of the application. - fn get(&self, key: &str) -> &str; + fn get(&self, key: &str) -> impl AsRef; } /// A simple [`TextProvider`] implementation which loads Clap's default English texts. @@ -35,7 +35,7 @@ impl Default for DefaultTextProvider { } impl TextProvider for DefaultTextProvider { - fn get(&self, key: &str) -> &str { + fn get(&self, key: &str) -> impl AsRef { self.0.get(key).expect(INTERNAL_ERROR_MSG) } } diff --git a/clap_builder/src/util/template.rs b/clap_builder/src/util/template.rs index efc99903e86..89f341d2857 100644 --- a/clap_builder/src/util/template.rs +++ b/clap_builder/src/util/template.rs @@ -10,7 +10,7 @@ pub(crate) fn interpolate(template: &str, texts: &impl TextProvider) -> String { if let ParseState::Complete(s, e) = parse_state { let key = &template[s..e]; - with_texts.push_str(texts.get(key)); + with_texts.push_str(texts.get(key).as_ref()); } if let ParseState::NotStarted = parse_state { diff --git a/clap_complete/examples/completion.rs b/clap_complete/examples/completion.rs index 1d32b938f2f..dcea107cb42 100644 --- a/clap_complete/examples/completion.rs +++ b/clap_complete/examples/completion.rs @@ -12,7 +12,7 @@ //! $ . ./completion.fish //! $ ./target/debug/examples/completion -- //! ``` -use clap::{value_parser, Arg, Command, ValueHint}; +use clap::{text_provider::DEFAULT_TEXT_PROVIDER, value_parser, Arg, Command, ValueHint}; use clap_complete::{generate, Generator, Shell}; use std::io; @@ -99,7 +99,7 @@ fn print_completions(gen: G, cmd: &mut Command) { } fn main() { - let matches = build_cli().get_matches(); + let matches = build_cli().get_matches(&*DEFAULT_TEXT_PROVIDER); if let Some(generator) = matches.get_one::("generator") { let mut cmd = build_cli(); diff --git a/clap_complete/examples/exhaustive.rs b/clap_complete/examples/exhaustive.rs index 2c00a2c7111..354e3f395ea 100644 --- a/clap_complete/examples/exhaustive.rs +++ b/clap_complete/examples/exhaustive.rs @@ -1,4 +1,4 @@ -use clap::builder::PossibleValue; +use clap::{builder::PossibleValue, text_provider::DEFAULT_TEXT_PROVIDER}; use clap_complete::{generate, Generator, Shell}; fn main() { @@ -8,7 +8,7 @@ fn main() { .completer("exhaustive") .complete(); - let matches = cli().get_matches(); + let matches = cli().get_matches(&*DEFAULT_TEXT_PROVIDER); if let Some(generator) = matches.get_one::("generate") { let mut cmd = cli(); eprintln!("Generating completion file for {generator}..."); diff --git a/clap_complete_nushell/examples/test.rs b/clap_complete_nushell/examples/test.rs index 6db15178b18..be19d583e4b 100644 --- a/clap_complete_nushell/examples/test.rs +++ b/clap_complete_nushell/examples/test.rs @@ -1,8 +1,9 @@ +use clap::text_provider::DEFAULT_TEXT_PROVIDER; use clap_complete::generate; use clap_complete_nushell::Nushell; fn main() { - let matches = cli().get_matches(); + let matches = cli().get_matches(&*DEFAULT_TEXT_PROVIDER); if matches.contains_id("generate") { let mut cmd = cli(); generate(Nushell, &mut cmd, "test", &mut std::io::stdout()); diff --git a/examples/derive_ref/augment_args.rs b/examples/derive_ref/augment_args.rs index 39d837cfaf5..47f174784c0 100644 --- a/examples/derive_ref/augment_args.rs +++ b/examples/derive_ref/augment_args.rs @@ -1,4 +1,4 @@ -use clap::{arg, Args, Command, FromArgMatches as _}; +use clap::{arg, text_provider::DEFAULT_TEXT_PROVIDER, Args, Command, FromArgMatches as _}; #[derive(Args, Debug)] struct DerivedArgs { @@ -11,7 +11,7 @@ fn main() { // Augment built args with derived args let cli = DerivedArgs::augment_args(cli); - let matches = cli.get_matches(); + let matches = cli.get_matches(&*DEFAULT_TEXT_PROVIDER); println!("Value of built: {:?}", matches.get_flag("built")); println!( "Value of derived via ArgMatches: {:?}", @@ -21,7 +21,7 @@ fn main() { // Since DerivedArgs implements FromArgMatches, we can extract it from the unstructured ArgMatches. // This is the main benefit of using derived arguments. let derived_matches = DerivedArgs::from_arg_matches(&matches) - .map_err(|err| err.exit()) + .map_err(|err| err.exit(&*DEFAULT_TEXT_PROVIDER)) .unwrap(); println!("Value of derived: {derived_matches:#?}"); } diff --git a/examples/derive_ref/augment_subcommands.rs b/examples/derive_ref/augment_subcommands.rs index 51cbe75d2a2..6eadb752f83 100644 --- a/examples/derive_ref/augment_subcommands.rs +++ b/examples/derive_ref/augment_subcommands.rs @@ -1,4 +1,4 @@ -use clap::{Command, FromArgMatches as _, Parser, Subcommand as _}; +use clap::{text_provider::DEFAULT_TEXT_PROVIDER, Command, FromArgMatches as _, Parser, Subcommand as _}; #[derive(Parser, Debug)] enum Subcommands { @@ -13,9 +13,9 @@ fn main() { // Augment with derived subcommands let cli = Subcommands::augment_subcommands(cli); - let matches = cli.get_matches(); + let matches = cli.get_matches(&*DEFAULT_TEXT_PROVIDER); let derived_subcommands = Subcommands::from_arg_matches(&matches) - .map_err(|err| err.exit()) + .map_err(|err| err.exit(&*DEFAULT_TEXT_PROVIDER)) .unwrap(); println!("Derived subcommands: {derived_subcommands:#?}"); } diff --git a/examples/git.rs b/examples/git.rs index fc8fd01f79e..718ea3f329e 100644 --- a/examples/git.rs +++ b/examples/git.rs @@ -1,7 +1,7 @@ use std::ffi::OsString; use std::path::PathBuf; -use clap::{arg, Command}; +use clap::{arg, text_provider::DEFAULT_TEXT_PROVIDER, Command}; fn cli() -> Command { Command::new("git") @@ -58,7 +58,7 @@ fn push_args() -> Vec { } fn main() { - let matches = cli().get_matches(); + let matches = cli().get_matches(&*DEFAULT_TEXT_PROVIDER); match matches.subcommand() { Some(("clone", sub_matches)) => { diff --git a/examples/multicall-busybox.rs b/examples/multicall-busybox.rs index 9260e1d70e8..9fe38511651 100644 --- a/examples/multicall-busybox.rs +++ b/examples/multicall-busybox.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use std::process::exit; -use clap::{value_parser, Arg, ArgAction, Command}; +use clap::{text_provider::DEFAULT_TEXT_PROVIDER, value_parser, Arg, ArgAction, Command}; fn applet_commands() -> [Command; 2] { [ @@ -31,7 +31,7 @@ fn main() { ) .subcommands(applet_commands()); - let matches = cmd.get_matches(); + let matches = cmd.get_matches(&*DEFAULT_TEXT_PROVIDER); let mut subcommand = matches.subcommand(); if let Some(("busybox", cmd)) = subcommand { if cmd.contains_id("install") { diff --git a/examples/multicall-hostname.rs b/examples/multicall-hostname.rs index b57680a5c11..aba359bd4b8 100644 --- a/examples/multicall-hostname.rs +++ b/examples/multicall-hostname.rs @@ -1,4 +1,4 @@ -use clap::Command; +use clap::{text_provider::DEFAULT_TEXT_PROVIDER, Command}; fn main() { let cmd = Command::new(env!("CARGO_CRATE_NAME")) @@ -9,7 +9,7 @@ fn main() { .subcommand(Command::new("hostname").about("show hostname part of FQDN")) .subcommand(Command::new("dnsdomainname").about("show domain name part of FQDN")); - match cmd.get_matches().subcommand_name() { + match cmd.get_matches(&*DEFAULT_TEXT_PROVIDER).subcommand_name() { Some("hostname") => println!("www"), Some("dnsdomainname") => println!("example.com"), _ => unreachable!("parser should ensure only valid subcommand names are used"), diff --git a/examples/pacman.rs b/examples/pacman.rs index 07c14013b02..7fd3be3a7e9 100644 --- a/examples/pacman.rs +++ b/examples/pacman.rs @@ -1,4 +1,4 @@ -use clap::{Arg, ArgAction, Command}; +use clap::{text_provider::DEFAULT_TEXT_PROVIDER, Arg, ArgAction, Command}; fn main() { let matches = Command::new("pacman") @@ -66,7 +66,7 @@ fn main() { .num_args(1..), ), ) - .get_matches(); + .get_matches(&*DEFAULT_TEXT_PROVIDER); match matches.subcommand() { Some(("sync", sync_matches)) => { diff --git a/examples/tutorial_builder/02_apps.rs b/examples/tutorial_builder/02_apps.rs index b085e0edc21..1a05810bb23 100644 --- a/examples/tutorial_builder/02_apps.rs +++ b/examples/tutorial_builder/02_apps.rs @@ -1,4 +1,4 @@ -use clap::{arg, Command}; +use clap::{arg, text_provider::DEFAULT_TEXT_PROVIDER, Command}; fn main() { let matches = Command::new("MyApp") @@ -6,7 +6,7 @@ fn main() { .about("Does awesome things") .arg(arg!(--two ).required(true)) .arg(arg!(--one ).required(true)) - .get_matches(); + .get_matches(&*DEFAULT_TEXT_PROVIDER); println!( "two: {:?}", diff --git a/examples/tutorial_derive/04_04_custom.rs b/examples/tutorial_derive/04_04_custom.rs index 4acf7652974..d3018dd348b 100644 --- a/examples/tutorial_derive/04_04_custom.rs +++ b/examples/tutorial_derive/04_04_custom.rs @@ -1,4 +1,5 @@ use clap::error::ErrorKind; +use clap::text_provider::DEFAULT_TEXT_PROVIDER; use clap::{CommandFactory, Parser}; #[derive(Parser)] @@ -47,7 +48,7 @@ fn main() { ErrorKind::ArgumentConflict, "Can't do relative and absolute version change", ) - .exit(); + .exit(&*DEFAULT_TEXT_PROVIDER); } ver.to_string() } else { @@ -63,7 +64,7 @@ fn main() { ErrorKind::ArgumentConflict, "Can only modify one version field", ) - .exit(); + .exit(&*DEFAULT_TEXT_PROVIDER); } }; format!("{major}.{minor}.{patch}") @@ -84,7 +85,7 @@ fn main() { ErrorKind::MissingRequiredArgument, "INPUT_FILE or --spec-in is required when using --config", ) - .exit() + .exit(&*DEFAULT_TEXT_PROVIDER) }); println!("Doing work using input {input} and config {config}"); } diff --git a/src/bin/stdio-fixture.rs b/src/bin/stdio-fixture.rs index 03ecd1fde9c..d539baa93d7 100644 --- a/src/bin/stdio-fixture.rs +++ b/src/bin/stdio-fixture.rs @@ -1,3 +1,5 @@ +use clap::text_provider::DEFAULT_TEXT_PROVIDER; + fn main() { #[allow(unused_mut)] let mut cmd = clap::Command::new("stdio-fixture") @@ -22,5 +24,5 @@ fn main() { .placeholder(styling::AnsiColor::Cyan.on_default()); cmd = cmd.styles(STYLES); } - cmd.get_matches(); + cmd.get_matches(&*DEFAULT_TEXT_PROVIDER); } diff --git a/tests/builder/app_settings.rs b/tests/builder/app_settings.rs index 7e5d2264bf5..6a21d278c34 100644 --- a/tests/builder/app_settings.rs +++ b/tests/builder/app_settings.rs @@ -137,6 +137,9 @@ fn arg_required_else_help_with_default() { ); } +// TODO: this test fails because the error isn't actually printed, so the texts are not interpolated +// Need to consider how to handle this case -- unless users are explicitly using the i18n feature, +// they should not have to worry about this #[test] fn arg_required_else_help_error_message() { static ARG_REQUIRED_ELSE_HELP: &str = "\ @@ -158,6 +161,7 @@ Options: .long("info") .action(ArgAction::SetTrue), ); + utils::assert_output( cmd, "test", diff --git a/tests/builder/utils.rs b/tests/builder/utils.rs index c16ad55e9a1..ca1c9871bc6 100644 --- a/tests/builder/utils.rs +++ b/tests/builder/utils.rs @@ -22,7 +22,8 @@ pub(crate) fn assert_output( ) { let mut buf = Cursor::new(Vec::with_capacity(50)); let res = l.try_get_matches_from(args.split(' ').collect::>()); - let err = res.unwrap_err(); + let err: clap::error::Error = res.unwrap_err(); + write!(&mut buf, "{err}").unwrap(); let actual = buf.into_inner(); let actual = String::from_utf8(actual).unwrap(); diff --git a/tests/derive/generic.rs b/tests/derive/generic.rs index c49d60a24b3..b629db7de9a 100644 --- a/tests/derive/generic.rs +++ b/tests/derive/generic.rs @@ -1,4 +1,4 @@ -use clap::{Args, Parser}; +use clap::{text_provider::DEFAULT_TEXT_PROVIDER, Args, Parser}; #[test] fn generic_struct_flatten() { @@ -17,7 +17,7 @@ fn generic_struct_flatten() { Outer { inner: Inner { answer: 42 } }, - Outer::parse_from(["--answer", "42"]) + Outer::parse_from(["--answer", "42"], &*DEFAULT_TEXT_PROVIDER) ); } @@ -41,7 +41,7 @@ fn generic_struct_flatten_w_where_clause() { Outer { inner: Inner { answer: 42 } }, - Outer::parse_from(["--answer", "42"]) + Outer::parse_from(["--answer", "42"], &*DEFAULT_TEXT_PROVIDER) ); } @@ -60,7 +60,7 @@ fn generic_enum() { assert_eq!( GenericEnum::Start(Inner { answer: 42 }), - GenericEnum::parse_from(["test", "start", "42"]) + GenericEnum::parse_from(["test", "start", "42"], &*DEFAULT_TEXT_PROVIDER) ); } @@ -82,7 +82,7 @@ fn generic_enum_w_where_clause() { assert_eq!( GenericEnum::Start(Inner { answer: 42 }), - GenericEnum::parse_from(["test", "start", "42"]) + GenericEnum::parse_from(["test", "start", "42"], &*DEFAULT_TEXT_PROVIDER) ); } @@ -101,7 +101,7 @@ fn generic_w_fromstr_trait_bound() { assert_eq!( Opt:: { answer: 42 }, - Opt::::parse_from(["--answer", "42"]) + Opt::::parse_from(["--answer", "42"], &*DEFAULT_TEXT_PROVIDER) ); } @@ -121,7 +121,7 @@ fn generic_wo_trait_bound() { answer: 42, took: None }, - Opt::::parse_from(["--answer", "42"]) + Opt::::parse_from(["--answer", "42"], &*DEFAULT_TEXT_PROVIDER) ); } @@ -140,6 +140,6 @@ fn generic_where_clause_w_trailing_comma() { assert_eq!( Opt:: { answer: 42 }, - Opt::::parse_from(["--answer", "42"]) + Opt::::parse_from(["--answer", "42"], &*DEFAULT_TEXT_PROVIDER) ); } From d2435e9918977beab48a149f48adf10256d23426 Mon Sep 17 00:00:00 2001 From: Brian Heise Date: Sun, 22 Dec 2024 14:26:14 +0900 Subject: [PATCH 6/6] add example --- Cargo.lock | 223 +++++++++++++++++++++++++++++++++-- Cargo.toml | 8 ++ examples/i18n/locales/en.yml | 28 +++++ examples/i18n/locales/jp.yml | 28 +++++ examples/i18n/main.rs | 117 ++++++++++++++++++ 5 files changed, 396 insertions(+), 8 deletions(-) create mode 100644 examples/i18n/locales/en.yml create mode 100644 examples/i18n/locales/jp.yml create mode 100644 examples/i18n/main.rs diff --git a/Cargo.lock b/Cargo.lock index 1016b046b79..4d8cf32d365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,6 +139,12 @@ version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arrayvec" version = "0.5.2" @@ -183,6 +189,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base62" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fa474cf7492f9a299ba6019fb99ec673e1739556d48e8a90eabaea282ef0e4" + [[package]] name = "base64" version = "0.21.7" @@ -260,6 +272,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ada7f35ca622a86a4d6c27be2633fc6c243ecc834859628fcce0681d8e76e1c8" +[[package]] +name = "bstr" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -423,7 +445,9 @@ dependencies = [ "clap_builder 4.5.23", "clap_derive", "humantime", + "rust-i18n", "rustversion", + "serde_yml 0.0.12", "shlex", "snapbox", "trybuild", @@ -471,7 +495,7 @@ dependencies = [ "clap_lex 0.7.4", "color-print", "lazy_static", - "serde_yml", + "serde_yml 0.0.12", "static_assertions", "strsim", "terminal_size 0.4.0", @@ -1111,6 +1135,30 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "globset" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + [[package]] name = "hamcrest2" version = "0.3.0" @@ -1240,6 +1288,22 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -1483,6 +1547,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libyml" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64804cc6a5042d4f05379909ba25b503ec04e2c082151d62122d5dcaa274b961" + [[package]] name = "libyml" version = "0.0.5" @@ -1718,6 +1788,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "normpath" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "notify" version = "6.1.1" @@ -1924,7 +2003,7 @@ dependencies = [ "tabled", "terminal_size 0.3.0", "titlecase", - "toml", + "toml 0.8.14", "umask", "unicode-segmentation", "ureq", @@ -2447,7 +2526,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ - "siphasher", + "siphasher 0.3.11", ] [[package]] @@ -2748,6 +2827,60 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-i18n" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039f57d22229db401af3458ca939300178e99e88b938573cea12b7c2b0f09724" +dependencies = [ + "globwalk", + "once_cell", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde5c022360a2e54477882843d56b6f9bcb4bc62f504b651a2f497f0028d174f" +dependencies = [ + "glob", + "once_cell", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "serde_yml 0.0.11", + "syn", +] + +[[package]] +name = "rust-i18n-support" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75d2844d36f62b5d6b66f9cf8f8cbdbbbdcdb5fd37a473a9cc2fb45fdcf485d2" +dependencies = [ + "arc-swap", + "base62", + "globwalk", + "itertools 0.11.0", + "lazy_static", + "normpath", + "once_cell", + "proc-macro2", + "regex", + "serde", + "serde_json", + "serde_yml 0.0.11", + "siphasher 1.0.1", + "toml 0.7.8", + "triomphe", +] + [[package]] name = "rust_decimal" version = "1.35.0" @@ -2907,6 +3040,23 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serde_yml" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e76bab63c3fd98d27c17f9cbce177f64a91f5e69ac04cafe04e1bb25d1dc3c" +dependencies = [ + "indexmap", + "itoa", + "libyml 0.0.4", + "log", + "memchr", + "ryu", + "serde", + "serde_json", + "tempfile", +] + [[package]] name = "serde_yml" version = "0.0.12" @@ -2915,7 +3065,7 @@ checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" dependencies = [ "indexmap", "itoa", - "libyml", + "libyml 0.0.5", "memchr", "ryu", "serde", @@ -2998,6 +3148,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "smallvec" version = "1.13.2" @@ -3042,6 +3198,12 @@ dependencies = [ "anstream", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -3314,6 +3476,18 @@ dependencies = [ "regex", ] +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + [[package]] name = "toml" version = "0.8.14" @@ -3323,7 +3497,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.22.15", ] [[package]] @@ -3335,6 +3509,19 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.22.15" @@ -3345,7 +3532,18 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.13", +] + +[[package]] +name = "triomphe" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", ] [[package]] @@ -3360,7 +3558,7 @@ dependencies = [ "serde_json", "target-triple", "termcolor", - "toml", + "toml 0.8.14", ] [[package]] @@ -3378,7 +3576,7 @@ dependencies = [ "serde", "shlex", "snapbox", - "toml_edit", + "toml_edit 0.22.15", ] [[package]] @@ -4057,6 +4255,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.6.13" diff --git a/Cargo.toml b/Cargo.toml index 263e27996e9..a0e3f3e5e31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -189,6 +189,8 @@ snapbox = "0.6.16" shlex = "1.3.0" automod = "1.0.14" clap-cargo = { version = "0.14.1", default-features = false } +rust-i18n = "3.1.2" +serde_yml = "0.0.12" [[example]] name = "demo" @@ -505,6 +507,12 @@ path = "examples/derive_ref/flatten_hand_args.rs" required-features = ["derive"] doc-scrape-examples = true +[[example]] +name = "i18n" +path = "examples/i18n/main.rs" +required-features = ["derive"] +doc-scrape-examples = true + [profile.test] opt-level = 1 diff --git a/examples/i18n/locales/en.yml b/examples/i18n/locales/en.yml new file mode 100644 index 00000000000..ce4e30c623a --- /dev/null +++ b/examples/i18n/locales/en.yml @@ -0,0 +1,28 @@ +usage: Usage +options: Options +default_sub_value_name: COMMAND +name: NAME +file: FILE +test-help-text: Does testing things +default-help-msg: Print this message or the help of the given subcommand(s) +name-help-text: Please enter your name +config-help-text: Enter the path to a configuration file +debug-help-text: Execute in debug mode +print-help: Display the help menue +print-version: Display the version +command: COMMAND +commands-list-heading: Commands +arguments-list-heading: Arguments +options-list-heading: Options + +# Clap Texts +clap.usage.header-text: Usage +clap.usage.options: OPTIONS +clap.help.command.value-name: COMMAND +clap.default-sub-value-name: COMMAND +clap.commands-list-header: Commands +clap.help.about: Print this message or the help of the given subcommand(s) +clap.arguments-list-header: Arguments +clap.options-list-header: Options +clap.help.short-help: Print help +clap.version.short-help: Print version \ No newline at end of file diff --git a/examples/i18n/locales/jp.yml b/examples/i18n/locales/jp.yml new file mode 100644 index 00000000000..d45471dfcf0 --- /dev/null +++ b/examples/i18n/locales/jp.yml @@ -0,0 +1,28 @@ +usage: 使用方法 +options: オプション +default_sub_value_name: コマンド +name: 名前 +file: ファイル +test-help-text: テストを実行します +default-help-msg: このメッセージまたは指定されたサブコマンドのヘルプを表示します +name-help-text: 名前を入力してください +config-help-text: 設定ファイルのパスを入力してください +debug-help-text: デバッグモードで実行します +print-help: ヘルプメニューを表示します +print-version: バージョンを表示します +command: コマンド +commands-list-heading: コマンド +arguments-list-heading: 引数 +options-list-heading: オプション + +# Clap Texts +clap.usage.header-text: 使用方法 +clap.usage.options: オプション +clap.help.command.value-name: コマンド +clap.default-sub-value-name: コマンド +clap.commands-list-header: コマンド +clap.help.about: このメッセージまたは指定されたサブコマンドのヘルプを表示します +clap.arguments-list-header: 引数 +clap.options-list-header: オプション +clap.help.short-help: ヘルプを表示 +clap.version.short-help: バージョンを表示 diff --git a/examples/i18n/main.rs b/examples/i18n/main.rs new file mode 100644 index 00000000000..10262e33362 --- /dev/null +++ b/examples/i18n/main.rs @@ -0,0 +1,117 @@ +use std::collections::HashMap; +use std::env; +use std::fs::read_to_string; +use clap::text_provider::TextProvider; +use rust_i18n::set_locale; +use rust_i18n::t; +use rust_i18n::Backend; +use std::path::PathBuf; +use clap::{Parser, Subcommand}; + +pub struct RemoteI18n { + pub trs: HashMap>, +} + +impl RemoteI18n { + fn new() -> Self { + let en = read_to_string("./examples/i18n/locales/en.yml").unwrap(); + let jp = read_to_string("./examples/i18n/locales/jp.yml").unwrap(); + let en_trs = serde_yml::from_str::>(&en).unwrap(); + let jp_trs = serde_yml::from_str::>(&jp).unwrap(); + let trs = HashMap::from([("en".to_owned(), en_trs), ("jp".to_owned(), jp_trs)]); + + Self { + trs + } + } +} + +impl Backend for RemoteI18n { + fn available_locales(&self) -> Vec<&str> { + self.trs.keys().map(|k| k.as_str()).collect() + } + + fn translate(&self, locale: &str, key: &str) -> Option<&str> { + self.trs.get(locale)?.get(key).map(|k| k.as_str()) + } +} + +rust_i18n::i18n!("locales", backend = RemoteI18n::new()); + + +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct Cli { + /// {name-help-text} + #[arg(value_name = "{name}")] + name: Option, + + /// {config-help-text} + #[arg(short, long, value_name = "{file}")] + config: Option, + + /// {debug-help-text} + #[arg(short, long, action = clap::ArgAction::Count)] + debug: u8, + + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + /// {test-help-text} + Test { + /// {list.help-text} + #[arg(short, long)] + list: bool, + }, +} + +struct I18n; + +impl TextProvider for I18n { + fn get(&self, key: &str) -> impl AsRef { + t!(key) + } +} + +fn main() { + let locale = env::var("LOCALE").unwrap_or("en".into()); + set_locale(&locale); + let cli = Cli::parse_with_texts(&I18n); + // You can check the value provided by positional arguments, or option arguments + if let Some(name) = cli.name.as_deref() { + println!("Value for name: {name}"); + } + + if let Some(config_path) = cli.config.as_deref() { + println!("Value for config: {}", config_path.display()); + } + + // You can see how many times a particular flag or argument occurred + // Note, only flags can have multiple occurrences + match cli.debug { + 0 => println!("Debug mode is off"), + 1 => println!("Debug mode is kind of on"), + 2 => println!("Debug mode is on"), + _ => println!("Don't be crazy"), + } + + // You can check for the existence of subcommands, and if found use their + // matches just as you would the top level cmd + match &cli.command { + Some(Commands::Test { list }) => { + if *list { + println!("Printing testing lists..."); + } else { + println!("Not printing testing lists..."); + } + } + None => {} + } + + // Continued program logic goes here... +} + +