Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save marcusadair/e06dca2f97386f4680abf6e9686b52c4 to your computer and use it in GitHub Desktop.

Select an option

Save marcusadair/e06dca2f97386f4680abf6e9686b52c4 to your computer and use it in GitHub Desktop.

Clap 4 enum options interacting with boolean flags

  • Demo two-way effects between options and flags
  • Avoid breaking changes when a CLI args gets more complex

Short help

Clap 4 enum options interacting with boolean flags

* Demo two-way effects between options and flags
* Avoid breaking changes when a CLI args gets more complex


Usage: demo [OPTIONS]

Options:
      --keyword-case <KEYWORD_CASE>  Case setting for reserved keywords [default: none] 
                                     [possible values: none, uppercase, lowercase]
      --uppercase                    Equivalent to `--keyword-case uppercase`
      --lowercase                    Equivalent to `--keyword-case lowercase`
      --no-uppercase                 Equivalent to `--keyword-case none` (deprecated)
      --no-lowercase                 Equivalent to `--keyword-case none` (deprecated)
  -h, --help                         Print help (see more with '--help')
  -V, --version                      Print version

Long help

Clap 4 enum options interacting with boolean flags

* Demo two-way effects between options and flags
* Avoid breaking changes when a CLI args gets more complex


Usage: demo [OPTIONS]

Options:
      --keyword-case <KEYWORD_CASE>
          Case setting for reserved keywords
          
          Overrides earlier instances and `--lowercase`, `--uppercase` flags.
          
          [default: none]

          Possible values:
          - none:      Leave reserved keywords as is (default)
          - uppercase: Convert reserved keywords to uppercase
          - lowercase: Convert reserved keywords to lowercase

      --uppercase
          Equivalent to `--keyword-case uppercase`

      --lowercase
          Equivalent to `--keyword-case lowercase`

      --no-uppercase
          Equivalent to `--keyword-case none` (deprecated)

      --no-lowercase
          Equivalent to `--keyword-case none` (deprecated)

  -h, --help
          Print help (see a summary with '-h')

  -V, --version
          Print version

[package]
name = "demo"
description = """\
Clap 4 enum options interacting with boolean flags
* Demo two-way effects between options and flags
* Avoid breaking changes when a CLI args gets more complex
"""
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.5.17", features = ["derive"] }
[[bin]]
name = "demo"
path = "./demo.rs"
use clap::{ArgAction, Parser, ValueEnum};
fn main() {
let options = Opt::parse();
println!("Hello, world, we got options...");
println!("{:#?}", options);
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)]
enum KeywordCase {
/// Leave reserved keywords as is (default)
#[default]
None,
/// Convert reserved keywords to uppercase
Uppercase,
/// Convert reserved keywords to lowercase
Lowercase,
}
/// Picture this: some cli utility is evolving over time.
///
/// 1. The first release has a boolean `--uppercase` flag which if set will
/// cause keywords to be uppercased during formatting, otherwise they are
/// left as is.
///
/// We include the negation `--no-uppercase` because we are nice.
///
/// ```
/// #[derive(Parser, Debug)]
/// #[command]
/// struct Options {
///
/// #[arg(long)]
/// uppercase: bool,
///
/// #[arg(long, overrides_with = "uppercase")]
/// no_uppercase: bool,
/// }
/// ```
///
/// 2. Time passes, *lowercase* should be an option too.
///
/// Duh! We overly simplified the problem, nothing wrong with an
/// `--uppercase` affordance, but the use case indicated multiplicity
/// not than duality, and a simple `--lowercase` flag does not cut it.
/// What we need is expandable `--keyword-case <mode>` and relegate
/// `--uppercase` and `--lowercase` to ergonomics.
///
/// Clap makes this possible without a breaking change:
///
/// - Derive`ValueEnum` for the choices,
/// - Add `--keyword-case <mode>` option to the options struct,
/// - Keep `--uppercase` and `--no-uppercase` for compatibility.
/// - Add `--lowercase` and `--no-lowercase` to pretend we meant it this way.
///
/// See the implementation below.
///
/// Notes
/// * `--keyword-case` has truly taken over, no need to refer to other struct members.
/// * `uppercase` / `lowercase` flags indicate the outcome as well: continuity / consistency.
/// * Negation flags are never are vestigial, only used for effects, never become true.
/// They could though, if there is some purpose.
/// * Apparently circular dependencies are not a problem because overrides eliminate priors.
/// * It would be better without the negation flags, they complicate interactions with
/// `--keyword-case` and there is a gotcha that, `--no-uppercase` resets it to `none` regardless
/// of the current value. More on this at the keyword_case negation test.
/// ```
///
#[derive(Parser, PartialEq, Default, Debug)]
#[command(version)]
struct Opt {
/// Case setting for reserved keywords
///
/// Overrides earlier instances and `--lowercase`, `--uppercase` flags.
#[arg(long, default_value = "none",
default_value_ifs = [
("uppercase", "true", "uppercase"),
("lowercase", "true", "lowercase"),
],
overrides_with = "keyword_case",
)]
keyword_case: KeywordCase,
/// Equivalent to `--keyword-case uppercase`
#[arg(long,
overrides_with_all = ["lowercase", "uppercase", "keyword_case"],
default_value_if("keyword_case", "uppercase", "true"),
)]
uppercase: bool,
/// Equivalent to `--keyword-case lowercase`.
#[arg(long,
overrides_with_all = ["lowercase", "uppercase", "keyword_case"],
default_value_if("keyword_case", "lowercase", "true"),
)]
lowercase: bool,
/// Equivalent to `--keyword-case none` (deprecated)
#[arg(long, default_value = "false", action = ArgAction::SetFalse,
overrides_with_all = ["lowercase", "uppercase", "keyword_case"],
)]
no_uppercase: bool,
/// Equivalent to `--keyword-case none` (deprecated)
#[arg(long, default_value = "false", action = ArgAction::SetFalse,
overrides_with_all = ["lowercase", "uppercase", "keyword_case"],
)]
no_lowercase: bool,
}
#[cfg(test)]
mod tests {
mod keyword_case {
use super::super::*;
#[test]
fn default_is_none() {
assert_eq!(
Opt { keyword_case: KeywordCase::None, ..Default::default() },
Opt::parse_from(["test"])
);
}
#[test]
fn propagates() {
assert_eq!(
Opt { keyword_case: KeywordCase::Lowercase, lowercase: true, ..Default::default() },
Opt::parse_from(["test", "--keyword-case", "lowercase"])
);
assert_eq!(
Opt { keyword_case: KeywordCase::Uppercase, uppercase: true, ..Default::default() },
Opt::parse_from(["test", "--keyword-case", "uppercase"])
);
}
#[test]
fn overrides() {
assert_eq!(
Opt { keyword_case: KeywordCase::None, ..Default::default() },
Opt::parse_from(["test", "--lowercase", "--uppercase", "--keyword-case", "none"])
);
}
#[test]
fn last_wins() {
assert_eq!(
Opt { keyword_case: KeywordCase::Lowercase, lowercase: true, ..Default::default() },
Opt::parse_from([
"test",
"--keyword-case", "uppercase",
"--keyword-case", "lowercase"
])
);
}
#[test]
fn redundant_okay() {
Opt::parse_from(["test", "--keyword-case", "none", "--keyword-case", "none"]);
}
#[test]
fn negated() {
assert_eq!(
Opt { keyword_case: KeywordCase::None, ..Default::default() },
Opt::parse_from(["test", "--keyword-case", "uppercase", "--no-uppercase"])
);
assert_eq!(
Opt { keyword_case: KeywordCase::None, ..Default::default() },
Opt::parse_from(["test", "--keyword-case", "lowercase", "--no-lowercase"])
);
// But negation resets keyword_case regardless of whether its value no the
// thing being negated. I did not find a way to prevent this. Note that
// these only exist because of the progress of events in our scenario.
assert_eq!(
Opt { keyword_case: KeywordCase::None, ..Default::default() },
Opt::parse_from(["test", "--keyword-case", "uppercase", "--no-lowercase"])
);
assert_eq!(
Opt { keyword_case: KeywordCase::None, ..Default::default() },
Opt::parse_from(["test", "--keyword-case", "lowercase", "--no-uppercase"])
);
}
}
mod uppercase {
use super::super::*;
#[test]
fn default_is_false() {
let options = Opt::parse_from(["test"]);
assert!(!options.uppercase);
}
#[test]
fn propagates() {
assert_eq!(
Opt { keyword_case: KeywordCase::Uppercase, uppercase: true, ..Default::default() },
Opt::parse_from(["test", "--uppercase"])
);
}
#[test]
fn redundant_okay() {
Opt::parse_from(["test", "--uppercase", "--uppercase"]);
}
#[test]
fn negated() {
assert_eq!(
Opt { keyword_case: KeywordCase::None, ..Default::default() },
Opt::parse_from(["test", "--uppercase", "--no-uppercase"])
);
}
}
mod lowercase {
use super::super::*;
#[test]
fn default_is_false() {
assert!(!Opt::parse_from(["test"]).lowercase);
}
#[test]
fn propagates() {
assert_eq!(
Opt { keyword_case: KeywordCase::Lowercase, lowercase: true, ..Default::default() },
Opt::parse_from(["test", "--lowercase"])
);
}
#[test]
fn overrides_uppercase() {
assert!(!Opt::parse_from(["test", "--lowercase"]).uppercase);
}
#[test]
fn sets_keyword_case() {
assert_eq!(KeywordCase::Lowercase,
Opt::parse_from(["test", "--lowercase"]).keyword_case);
}
#[test]
fn redundant_okay() {
Opt::parse_from(["test", "--lowercase", "--lowercase"]);
}
#[test]
fn negated() {
assert_eq!(
Opt { keyword_case: KeywordCase::None, ..Default::default() },
Opt::parse_from(["test", "--lowercase", "--no-lowercase"])
);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment