From 117106be5df711997476990671b368bcf94c7ecc Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Sat, 6 Jun 2026 15:41:49 -0400 Subject: [PATCH 01/12] Add opt-out for negative boolean aliases --- NEWS.md | 4 ++- R/args.R | 12 +++++-- R/help.R | 11 ++++-- R/utils.R | 2 ++ README.md | 11 ++++++ tests/testthat/test-regressions.R | 57 +++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 6 deletions(-) diff --git a/NEWS.md b/NEWS.md index 9be5fda..fa57c16 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,8 @@ # Rapp (development version) +- Boolean switches annotated with `#| negative_alias: false` no longer include + or accept a generated `--no-*` alias (#24). + # Rapp 0.3.0 ## Breaking changes @@ -26,4 +29,3 @@ # Rapp 0.1.0 - Initial release - diff --git a/R/args.R b/R/args.R index df32cdf..5368d8a 100644 --- a/R/args.R +++ b/R/args.R @@ -91,8 +91,9 @@ process_args <- function(args, app) { # if flag not known, maybe this is a switch flag if (is.null(spec) && startsWith(a, "--no-")) { alt_name <- str_drop_prefix(name, "no_") - spec <- app_opts[[alt_name]] - if (!is.null(spec)) { + alt_spec <- app_opts[[alt_name]] + if (!is.null(alt_spec) && has_negative_alias(alt_spec)) { + spec <- alt_spec val <- "false" name <- alt_name } @@ -198,9 +199,14 @@ process_args <- function(args, app) { } if (length(specs) < length(positional_args)) { + unknown_args <- if (length(specs)) { + positional_args[-seq_along(specs)] + } else { + positional_args + } stop( "Arguments not recognized: ", - paste0(positional_args[-seq_along(specs)], collapse = " ") + paste0(unknown_args, collapse = " ") ) } diff --git a/R/help.R b/R/help.R index 66858f3..cd808de 100644 --- a/R/help.R +++ b/R/help.R @@ -177,8 +177,13 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { } else if (identical(opt$arg_type, "switch")) { default_value <- format_default_value(opt$default) toggle_flag <- paste0("--no-", cli_name) + show_negative <- has_negative_alias(opt) toggle_note <- if (isTRUE(opt$default)) { - sprintf("Disable with `%s`.", toggle_flag) + if (show_negative) { + sprintf("Disable with `%s`.", toggle_flag) + } else { + character() + } } else { sprintf("Enable with `%s`.", paste0("--", cli_name)) } @@ -186,7 +191,9 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { details <- c(details, sprintf("[default: %s]", default_value)) } details <- c(details, toggle_note) - flag <- paste(flag, "/", toggle_flag) + if (show_negative) { + flag <- paste(flag, "/", toggle_flag) + } } if (identical(opt$action, "append")) { diff --git a/R/utils.R b/R/utils.R index 1ba4261..d6253e6 100644 --- a/R/utils.R +++ b/R/utils.R @@ -68,6 +68,8 @@ compact <- function(x) x[lengths(x) > 0] `%||%` <- function(x, y) if (is.null(x)) y else x `subtract<-` <- function(x, value) x - value +has_negative_alias <- function(opt) !isFALSE(opt[["negative_alias"]]) + `append<-` <- function(x, after = NULL, value) { if (is.null(after)) c(x, value) else append(x, value, after) } diff --git a/README.md b/README.md index ca50adc..3b941e9 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,15 @@ my-app --echo=false # FALSE my-app --echo=0 # FALSE ``` +To omit the generated `--no-*` alias for a boolean switch, add +`#| negative_alias: false` above the assignment: + +``` r +#| description: Print version and exit. +#| negative_alias: false +version <- FALSE +``` + Assigning `c()` or `list()` declares an option that can be supplied multiple times. Use `c()` when you want to keep the exact strings provided on the command line, and `list()` when you want Rapp to attempt @@ -338,6 +347,8 @@ Other YAML fields you can supply to change the behavior of Rapp - `arg_type`: how the input appears on the CLI (`option`, `switch`, `positional`). - `action`: whether values replace or accumulate (`replace` vs `append` for repeatable options and collectors). +- `negative_alias`: whether boolean switches include a generated `--no-*` + alias. ## Summary diff --git a/tests/testthat/test-regressions.R b/tests/testthat/test-regressions.R index 4ac86a0..ecf8912 100644 --- a/tests/testthat/test-regressions.R +++ b/tests/testthat/test-regressions.R @@ -98,3 +98,60 @@ test_that("leading variadic positional collectors accumulate args", { expect_output(Rapp::run(app_path, c("alpha", "beta", "gamma")), "ok") }) + +test_that("boolean switches can disable negative aliases", { + app_path <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| name: version-app", + "#| description: Version printer.", + "", + "#| description: Print version and exit.", + "#| negative_alias: false", + "version <- FALSE", + "", + "if (version) cat('version-app 1.0.0\\n')" + ), + prefix = "rapp-no-negative-switch-" + ) + + expect_output(Rapp::run(app_path, "--version"), "version-app 1.0.0") + + help <- capture.output(Rapp::run(app_path, "--help")) + expect_true(any(grepl("--version", help, fixed = TRUE))) + expect_false(any(grepl("--no-version", help, fixed = TRUE))) + expect_error( + Rapp::run(app_path, "--no-version"), + "Arguments not recognized: --no-version", + fixed = TRUE + ) + + true_default_app <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| description: Keep output wrapped.", + "#| negative_alias: false", + "wrap <- TRUE" + ), + prefix = "rapp-no-negative-default-true-" + ) + true_default_help <- capture.output(Rapp::run(true_default_app, "--help")) + expect_false(any(grepl("--no-wrap", true_default_help, fixed = TRUE))) + expect_false(any(grepl( + "Enable with `--wrap`.", + true_default_help, + fixed = TRUE + ))) + + negative_app <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| description: Legacy spelling is ignored.", + "#| negative: false", + "legacy <- FALSE" + ), + prefix = "rapp-negative-ignored-" + ) + negative_help <- capture.output(Rapp::run(negative_app, "--help")) + expect_true(any(grepl("--no-legacy", negative_help, fixed = TRUE))) +}) From 4433165a172004d3e46c63e0c8330c6e766d5e7e Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Sat, 6 Jun 2026 17:32:09 -0400 Subject: [PATCH 02/12] Adjust boolean switch aliases to follow logical defaults --- NEWS.md | 4 ++ R/app.R | 4 +- R/args.R | 31 +++++++++++++- R/help.R | 28 ++++++++++--- R/utils.R | 26 +++++++++++- README.md | 33 ++++++++------- tests/testthat/_snaps/basics.md | 2 +- tests/testthat/_snaps/help-snapshots.md | 12 +++--- tests/testthat/test-regressions.R | 54 ++++++++++++++++++++++++- 9 files changed, 157 insertions(+), 37 deletions(-) diff --git a/NEWS.md b/NEWS.md index fa57c16..1a26792 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # Rapp (development version) +- Boolean switch aliases now follow logical defaults: `FALSE` exposes the + positive flag, `TRUE` exposes the generated `--no-*` flag, and `NA` + exposes both. + - Boolean switches annotated with `#| negative_alias: false` no longer include or accept a generated `--no-*` alias (#24). diff --git a/R/app.R b/R/app.R index 7aa510f..928471c 100644 --- a/R/app.R +++ b/R/app.R @@ -98,7 +98,7 @@ get_app_inputs <- function(app, exprs = app$exprs, pos = integer()) { e <- exprs[[i]] # foo <- NULL default positional arg `APP ` - # foo <- default switch `APP --foo` or `APP --no-foo` + # foo <- default switch, with aliases from default # foo <- default opt `APP --foo val` # foo <- default opt with action: append `APP --foo val1 --foo val2` # @@ -217,7 +217,7 @@ get_app_inputs <- function(app, exprs = app$exprs, pos = integer()) { "NULL" = "string" ), - arg_type = if (identical(default, TRUE) || identical(default, FALSE)) { + arg_type = if (is_bool_switch_default(default)) { "switch" } else if (is.null(default)) { "positional" diff --git a/R/args.R b/R/args.R index 5368d8a..7884b16 100644 --- a/R/args.R +++ b/R/args.R @@ -14,7 +14,16 @@ process_args <- function(args, app) { short <- str_drop_prefix(short_opt, "-") for (i in seq_along(app_opts)) { if (identical(short, app_opts[[i]]$short)) { - return(paste0("--", names(app_opts)[[i]])) + if (identical(app_opts[[i]]$arg_type, "switch")) { + if (has_positive_alias(app_opts[[i]])) { + return(paste0("--", names(app_opts)[[i]])) + } + if (has_negative_alias(app_opts[[i]])) { + return(paste0("--no-", names(app_opts)[[i]])) + } + } else { + return(paste0("--", names(app_opts)[[i]])) + } } } } @@ -81,18 +90,36 @@ process_args <- function(args, app) { name <- gsub("-", "_", name, fixed = TRUE) val <- str_drop_prefix(a, equals_idx) spec <- app_opts[[name]] + if ( + !is.null(spec) && + identical(spec$arg_type, "switch") && + !has_positive_alias(spec) + ) { + spec <- NULL + } } else { # --name name <- str_drop_prefix(a, "--") name <- gsub("-", "_", name, fixed = TRUE) spec <- app_opts[[name]] + if ( + !is.null(spec) && + identical(spec$arg_type, "switch") && + !has_positive_alias(spec) + ) { + spec <- NULL + } # if flag not known, maybe this is a switch flag if (is.null(spec) && startsWith(a, "--no-")) { alt_name <- str_drop_prefix(name, "no_") alt_spec <- app_opts[[alt_name]] - if (!is.null(alt_spec) && has_negative_alias(alt_spec)) { + if ( + !is.null(alt_spec) && + identical(alt_spec$arg_type, "switch") && + has_negative_alias(alt_spec) + ) { spec <- alt_spec val <- "false" name <- alt_name diff --git a/R/help.R b/R/help.R index cd808de..0de9436 100644 --- a/R/help.R +++ b/R/help.R @@ -176,24 +176,40 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { } } else if (identical(opt$arg_type, "switch")) { default_value <- format_default_value(opt$default) - toggle_flag <- paste0("--no-", cli_name) + positive_flag <- paste0("--", cli_name) + negative_flag <- paste0("--no-", cli_name) + show_positive <- has_positive_alias(opt) show_negative <- has_negative_alias(opt) + flags <- c( + if (show_positive) positive_flag, + if (show_negative) negative_flag + ) + flag <- paste(flags, collapse = " / ") + if (!is.null(short_flag) && nzchar(short_flag)) { + flag <- paste0("-", short_flag, ", ", flag) + } toggle_note <- if (isTRUE(opt$default)) { if (show_negative) { - sprintf("Disable with `%s`.", toggle_flag) + sprintf("Disable with `%s`.", negative_flag) + } else { + character() + } + } else if (isFALSE(opt$default)) { + if (show_positive) { + sprintf("Enable with `%s`.", positive_flag) } else { character() } } else { - sprintf("Enable with `%s`.", paste0("--", cli_name)) + c( + if (show_positive) sprintf("Enable with `%s`.", positive_flag), + if (show_negative) sprintf("Disable with `%s`.", negative_flag) + ) } if (!is.null(default_value)) { details <- c(details, sprintf("[default: %s]", default_value)) } details <- c(details, toggle_note) - if (show_negative) { - flag <- paste(flag, "/", toggle_flag) - } } if (identical(opt$action, "append")) { diff --git a/R/utils.R b/R/utils.R index d6253e6..e5bd7d1 100644 --- a/R/utils.R +++ b/R/utils.R @@ -68,7 +68,31 @@ compact <- function(x) x[lengths(x) > 0] `%||%` <- function(x, y) if (is.null(x)) y else x `subtract<-` <- function(x, value) x - value -has_negative_alias <- function(opt) !isFALSE(opt[["negative_alias"]]) +is_bool_switch_default <- function(x) is.logical(x) && length(x) == 1L + +has_positive_alias <- function(opt) { + stopifnot(identical(opt[["arg_type"]], "switch")) + + default <- opt[["default"]] + stopifnot(is_bool_switch_default(default)) + + isFALSE(default) || + isTRUE(is.na(default)) || + isFALSE(opt[["negative_alias"]]) +} + +has_negative_alias <- function(opt) { + stopifnot(identical(opt[["arg_type"]], "switch")) + + default <- opt[["default"]] + stopifnot(is_bool_switch_default(default)) + + if (!is.null(opt[["negative_alias"]])) { + return(!isFALSE(opt[["negative_alias"]])) + } + + isTRUE(default) || isTRUE(is.na(default)) +} `append<-` <- function(x, after = NULL, value) { if (is.null(after)) c(x, value) else append(x, value, after) diff --git a/README.md b/README.md index 3b941e9..6c4cf69 100644 --- a/README.md +++ b/README.md @@ -108,35 +108,32 @@ flip-coin --n=1 flip-coin --n 1 ``` -Bool options, (that is, assignments of `TRUE` or `FALSE` in an R app) -are a little different. They support usage as switches or toggles at the -command line. For example in an R script: +Bool options, (that is, assignments of `TRUE`, `FALSE`, or `NA` in an R +app) are a little different. They support usage as switches or toggles +at the command line, and the default controls which aliases are exposed. +For example in an R script: ``` r echo <- TRUE ``` -means that at the command line the following are supported: +means that at the command line the negative alias is supported: ``` r -my-app --echo # TRUE -my-app --echo=yes # TRUE -my-app --echo=true # TRUE -my-app --echo=1 # TRUE - my-app --no-echo # FALSE -my-app --echo=no # FALSE -my-app --echo=false # FALSE -my-app --echo=0 # FALSE ``` +With `echo <- FALSE`, the positive alias `--echo` is supported. With +`echo <- NA`, both `--echo` and `--no-echo` are supported. + To omit the generated `--no-*` alias for a boolean switch, add -`#| negative_alias: false` above the assignment: +`#| negative_alias: false` above the assignment. This is most useful for +`NA` defaults: ``` r #| description: Print version and exit. #| negative_alias: false -version <- FALSE +version <- NA ``` Assigning `c()` or `list()` declares an option that can be supplied @@ -347,8 +344,8 @@ Other YAML fields you can supply to change the behavior of Rapp - `arg_type`: how the input appears on the CLI (`option`, `switch`, `positional`). - `action`: whether values replace or accumulate (`replace` vs `append` for repeatable options and collectors). -- `negative_alias`: whether boolean switches include a generated `--no-*` - alias. +- `negative_alias`: override whether boolean switches include a generated + `--no-*` alias. ## Summary @@ -359,7 +356,9 @@ command line arguments. |----|----| | Assignment of scalar literal
`foo <- ""` | Option
`APP --foo value` | | Assignment of `NULL`
`foo <- NULL` | Positional Arg
`APP foo-value` | -| Assignment of `TRUE` or `FALSE`
`foo <- TRUE` | Boolean switch
`APP --foo` or `APP --no-foo` | +| Assignment of `FALSE`
`foo <- FALSE` | Boolean switch
`APP --foo` | +| Assignment of `TRUE`
`foo <- TRUE` | Boolean switch
`APP --no-foo` | +| Assignment of `NA`
`foo <- NA` | Boolean switch
`APP --foo` or `APP --no-foo` | | Assignment of `c()` or `list()`
`foo <- c()` | Repeatable option
`APP --foo val1 --foo val2` | | Assignment of `NULL` to name with `...`
`args... <- NULL` | Positional Arg Collector
`APP foo bar baz` | | Switch with string literal
`switch("", cmd1 = {}, cmd2 = {})` | Commands
`APP cmd1 --help`
`APP cmd2 --help` | diff --git a/tests/testthat/_snaps/basics.md b/tests/testthat/_snaps/basics.md index 4b80bba..e786a2b 100644 --- a/tests/testthat/_snaps/basics.md +++ b/tests/testthat/_snaps/basics.md @@ -10,6 +10,6 @@ Options: -n, --flips Number of coin flips [default: 1] [type: integer] --sep [default: " "] [type: string] - --wrap / --no-wrap [default: true] Disable with `--no-wrap`. + --no-wrap [default: true] Disable with `--no-wrap`. --seed [default: NA] [type: integer] diff --git a/tests/testthat/_snaps/help-snapshots.md b/tests/testthat/_snaps/help-snapshots.md index b9d72f2..0e1fc77 100644 --- a/tests/testthat/_snaps/help-snapshots.md +++ b/tests/testthat/_snaps/help-snapshots.md @@ -10,7 +10,7 @@ Options: -n, --flips Number of coin flips [default: 1] [type: integer] --sep [default: " "] [type: string] - --wrap / --no-wrap [default: true] Disable with `--no-wrap`. + --no-wrap [default: true] Disable with `--no-wrap`. --seed [default: NA] [type: integer] --- @@ -102,14 +102,12 @@ child2 command Options: - --child2-opt [default: "child2-default"] [type: string] - --child2-switch / --no-child2-switch [default: false] - Enable with `--child2-switch`. + --child2-opt [default: "child2-default"] [type: string] + --child2-switch [default: false] Enable with `--child2-switch`. Parent options: - --parent-opt [default: "parent-default"] [type: string] - --parent-switch / --no-parent-switch [default: true] - Disable with `--no-parent-switch`. + --parent-opt [default: "parent-default"] [type: string] + --no-parent-switch [default: true] Disable with `--no-parent-switch`. Global options: --top-opt [default: "top-default"] [type: string] diff --git a/tests/testthat/test-regressions.R b/tests/testthat/test-regressions.R index ecf8912..52b6f4f 100644 --- a/tests/testthat/test-regressions.R +++ b/tests/testthat/test-regressions.R @@ -148,10 +148,62 @@ test_that("boolean switches can disable negative aliases", { "#!/usr/bin/env Rapp", "#| description: Legacy spelling is ignored.", "#| negative: false", - "legacy <- FALSE" + "legacy <- NA" ), prefix = "rapp-negative-ignored-" ) negative_help <- capture.output(Rapp::run(negative_app, "--help")) expect_true(any(grepl("--no-legacy", negative_help, fixed = TRUE))) }) + +test_that("boolean switch aliases follow logical defaults", { + app_path <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "disabled <- FALSE", + "enabled <- TRUE", + "unknown <- NA", + "cat(", + " sprintf(", + " 'disabled=%s enabled=%s unknown=%s\\n',", + " disabled,", + " enabled,", + " unknown", + " )", + ")" + ), + prefix = "rapp-bool-default-aliases-" + ) + + expect_output( + Rapp::run(app_path, c("--disabled", "--no-enabled", "--unknown")), + "disabled=TRUE enabled=FALSE unknown=TRUE" + ) + expect_output( + Rapp::run(app_path, c("--no-unknown")), + "disabled=FALSE enabled=TRUE unknown=FALSE" + ) + + help <- capture.output(Rapp::run(app_path, "--help")) + expect_true(any(grepl("--disabled", help, fixed = TRUE))) + expect_false(any(grepl("--no-disabled", help, fixed = TRUE))) + expect_false(any(grepl("--enabled", help, fixed = TRUE))) + expect_true(any(grepl("--no-enabled", help, fixed = TRUE))) + expect_true(any(grepl("--unknown / --no-unknown", help, fixed = TRUE))) + + expect_error( + Rapp::run(app_path, "--no-disabled"), + "Arguments not recognized: --no-disabled", + fixed = TRUE + ) + expect_error( + Rapp::run(app_path, "--enabled"), + "Arguments not recognized: --enabled", + fixed = TRUE + ) + expect_error( + Rapp::run(app_path, "--enabled=false"), + "Arguments not recognized: --enabled=false", + fixed = TRUE + ) +}) From a93860c5cca9dd173b0695cc542ff5048b7a44bf Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Sat, 6 Jun 2026 17:56:50 -0400 Subject: [PATCH 03/12] Keep NA boolean options and trim switch aliases --- NEWS.md | 5 +++-- R/app.R | 2 +- R/utils.R | 8 +++----- README.md | 21 +++++++++++---------- tests/testthat/test-regressions.R | 20 +++++++++++++++----- 5 files changed, 33 insertions(+), 23 deletions(-) diff --git a/NEWS.md b/NEWS.md index 1a26792..e453da5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,8 +1,9 @@ # Rapp (development version) - Boolean switch aliases now follow logical defaults: `FALSE` exposes the - positive flag, `TRUE` exposes the generated `--no-*` flag, and `NA` - exposes both. + positive flag and `TRUE` exposes the generated `--no-*` flag. `NA` remains + a value-taking boolean option, so it can still indicate that an option was + not supplied. - Boolean switches annotated with `#| negative_alias: false` no longer include or accept a generated `--no-*` alias (#24). diff --git a/R/app.R b/R/app.R index 928471c..474aa0d 100644 --- a/R/app.R +++ b/R/app.R @@ -98,7 +98,7 @@ get_app_inputs <- function(app, exprs = app$exprs, pos = integer()) { e <- exprs[[i]] # foo <- NULL default positional arg `APP ` - # foo <- default switch, with aliases from default + # foo <- default switch, with aliases from default # foo <- default opt `APP --foo val` # foo <- default opt with action: append `APP --foo val1 --foo val2` # diff --git a/R/utils.R b/R/utils.R index e5bd7d1..d2b3adb 100644 --- a/R/utils.R +++ b/R/utils.R @@ -68,7 +68,7 @@ compact <- function(x) x[lengths(x) > 0] `%||%` <- function(x, y) if (is.null(x)) y else x `subtract<-` <- function(x, value) x - value -is_bool_switch_default <- function(x) is.logical(x) && length(x) == 1L +is_bool_switch_default <- function(x) identical(x, TRUE) || identical(x, FALSE) has_positive_alias <- function(opt) { stopifnot(identical(opt[["arg_type"]], "switch")) @@ -76,9 +76,7 @@ has_positive_alias <- function(opt) { default <- opt[["default"]] stopifnot(is_bool_switch_default(default)) - isFALSE(default) || - isTRUE(is.na(default)) || - isFALSE(opt[["negative_alias"]]) + isFALSE(default) || isFALSE(opt[["negative_alias"]]) } has_negative_alias <- function(opt) { @@ -91,7 +89,7 @@ has_negative_alias <- function(opt) { return(!isFALSE(opt[["negative_alias"]])) } - isTRUE(default) || isTRUE(is.na(default)) + isTRUE(default) } `append<-` <- function(x, after = NULL, value) { diff --git a/README.md b/README.md index 6c4cf69..d5d81e6 100644 --- a/README.md +++ b/README.md @@ -108,10 +108,10 @@ flip-coin --n=1 flip-coin --n 1 ``` -Bool options, (that is, assignments of `TRUE`, `FALSE`, or `NA` in an R -app) are a little different. They support usage as switches or toggles -at the command line, and the default controls which aliases are exposed. -For example in an R script: +Assignments of `TRUE` or `FALSE` are a little different from other +options. They support usage as switches or toggles at the command line, +and the default controls which aliases are exposed. For example in an R +script: ``` r echo <- TRUE @@ -123,17 +123,18 @@ means that at the command line the negative alias is supported: my-app --no-echo # FALSE ``` -With `echo <- FALSE`, the positive alias `--echo` is supported. With -`echo <- NA`, both `--echo` and `--no-echo` are supported. +With `echo <- FALSE`, the positive alias `--echo` is supported. +Assignments of `NA` remain boolean options that take a value, so omitting +`echo <- NA` leaves `echo` as `NA` and supplying `--echo true` or +`--echo false` records the caller's choice. To omit the generated `--no-*` alias for a boolean switch, add -`#| negative_alias: false` above the assignment. This is most useful for -`NA` defaults: +`#| negative_alias: false` above the assignment: ``` r #| description: Print version and exit. #| negative_alias: false -version <- NA +version <- TRUE ``` Assigning `c()` or `list()` declares an option that can be supplied @@ -358,7 +359,7 @@ command line arguments. | Assignment of `NULL`
`foo <- NULL` | Positional Arg
`APP foo-value` | | Assignment of `FALSE`
`foo <- FALSE` | Boolean switch
`APP --foo` | | Assignment of `TRUE`
`foo <- TRUE` | Boolean switch
`APP --no-foo` | -| Assignment of `NA`
`foo <- NA` | Boolean switch
`APP --foo` or `APP --no-foo` | +| Assignment of `NA`
`foo <- NA` | Boolean option
`APP --foo true` or `APP --foo false` | | Assignment of `c()` or `list()`
`foo <- c()` | Repeatable option
`APP --foo val1 --foo val2` | | Assignment of `NULL` to name with `...`
`args... <- NULL` | Positional Arg Collector
`APP foo bar baz` | | Switch with string literal
`switch("", cmd1 = {}, cmd2 = {})` | Commands
`APP cmd1 --help`
`APP cmd2 --help` | diff --git a/tests/testthat/test-regressions.R b/tests/testthat/test-regressions.R index 52b6f4f..2189432 100644 --- a/tests/testthat/test-regressions.R +++ b/tests/testthat/test-regressions.R @@ -148,7 +148,7 @@ test_that("boolean switches can disable negative aliases", { "#!/usr/bin/env Rapp", "#| description: Legacy spelling is ignored.", "#| negative: false", - "legacy <- NA" + "legacy <- TRUE" ), prefix = "rapp-negative-ignored-" ) @@ -156,7 +156,7 @@ test_that("boolean switches can disable negative aliases", { expect_true(any(grepl("--no-legacy", negative_help, fixed = TRUE))) }) -test_that("boolean switch aliases follow logical defaults", { +test_that("boolean aliases follow logical defaults without consuming NA", { app_path <- local_rapp_app( c( "#!/usr/bin/env Rapp", @@ -176,11 +176,15 @@ test_that("boolean switch aliases follow logical defaults", { ) expect_output( - Rapp::run(app_path, c("--disabled", "--no-enabled", "--unknown")), + Rapp::run(app_path, character()), + "disabled=FALSE enabled=TRUE unknown=NA" + ) + expect_output( + Rapp::run(app_path, c("--disabled", "--no-enabled", "--unknown", "true")), "disabled=TRUE enabled=FALSE unknown=TRUE" ) expect_output( - Rapp::run(app_path, c("--no-unknown")), + Rapp::run(app_path, "--unknown=false"), "disabled=FALSE enabled=TRUE unknown=FALSE" ) @@ -189,7 +193,8 @@ test_that("boolean switch aliases follow logical defaults", { expect_false(any(grepl("--no-disabled", help, fixed = TRUE))) expect_false(any(grepl("--enabled", help, fixed = TRUE))) expect_true(any(grepl("--no-enabled", help, fixed = TRUE))) - expect_true(any(grepl("--unknown / --no-unknown", help, fixed = TRUE))) + expect_true(any(grepl("--unknown ", help, fixed = TRUE))) + expect_false(any(grepl("--no-unknown", help, fixed = TRUE))) expect_error( Rapp::run(app_path, "--no-disabled"), @@ -206,4 +211,9 @@ test_that("boolean switch aliases follow logical defaults", { "Arguments not recognized: --enabled=false", fixed = TRUE ) + expect_error( + Rapp::run(app_path, "--no-unknown"), + "Arguments not recognized: --no-unknown", + fixed = TRUE + ) }) From a5bd05551c7bbede2a92daacc19ea990cd976278 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 12:47:13 -0400 Subject: [PATCH 04/12] Fix boolean switch alias edge cases --- R/app.R | 2 +- R/args.R | 21 ++++++++---- R/utils.R | 25 ++++++++++++--- tests/testthat/test-regressions.R | 53 +++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 13 deletions(-) diff --git a/R/app.R b/R/app.R index 09ef208..fbbc8c8 100644 --- a/R/app.R +++ b/R/app.R @@ -228,7 +228,7 @@ get_app_inputs <- function(app, exprs = app$exprs, pos = integer()) { "NULL" = "string" ), - arg_type = if (is_bool_switch_default(default)) { + arg_type = if (is_auto_bool_switch_default(default)) { "switch" } else if (is.null(default)) { "positional" diff --git a/R/args.R b/R/args.R index 9afa8be..dafedca 100644 --- a/R/args.R +++ b/R/args.R @@ -10,6 +10,10 @@ process_args <- function(args, app) { on.exit(close(args)) } + stop_unrecognized_arg <- function(arg) { + stop("Arguments not recognized: ", arg, call. = FALSE) + } + short_opt_to_long_opt <- function(short_opt) { short <- str_drop_prefix(short_opt, "-") for (i in seq_along(app_opts)) { @@ -95,7 +99,7 @@ process_args <- function(args, app) { identical(spec$arg_type, "switch") && !has_positive_alias(spec) ) { - spec <- NULL + stop_unrecognized_arg(a) } } else { # --name @@ -108,7 +112,7 @@ process_args <- function(args, app) { identical(spec$arg_type, "switch") && !has_positive_alias(spec) ) { - spec <- NULL + stop_unrecognized_arg(a) } # if flag not known, maybe this is a switch flag @@ -117,12 +121,15 @@ process_args <- function(args, app) { alt_spec <- app_opts[[alt_name]] if ( !is.null(alt_spec) && - identical(alt_spec$arg_type, "switch") && - has_negative_alias(alt_spec) + identical(alt_spec$arg_type, "switch") ) { - spec <- alt_spec - val <- "false" - name <- alt_name + if (has_negative_alias(alt_spec)) { + spec <- alt_spec + val <- "false" + name <- alt_name + } else { + stop_unrecognized_arg(a) + } } } } diff --git a/R/utils.R b/R/utils.R index d2b3adb..aee6929 100644 --- a/R/utils.R +++ b/R/utils.R @@ -68,28 +68,43 @@ compact <- function(x) x[lengths(x) > 0] `%||%` <- function(x, y) if (is.null(x)) y else x `subtract<-` <- function(x, value) x - value -is_bool_switch_default <- function(x) identical(x, TRUE) || identical(x, FALSE) +is_auto_bool_switch_default <- function(x) { + identical(x, TRUE) || identical(x, FALSE) +} + +is_bool_switch_default <- function(x) is.logical(x) && length(x) == 1L + +check_bool_switch_default <- function(default) { + if (!is_bool_switch_default(default)) { + stop( + "Boolean switches must have a TRUE, FALSE, or NA default.", + call. = FALSE + ) + } +} has_positive_alias <- function(opt) { stopifnot(identical(opt[["arg_type"]], "switch")) default <- opt[["default"]] - stopifnot(is_bool_switch_default(default)) + check_bool_switch_default(default) - isFALSE(default) || isFALSE(opt[["negative_alias"]]) + isFALSE(default) || + isTRUE(is.na(default)) || + isFALSE(opt[["negative_alias"]]) } has_negative_alias <- function(opt) { stopifnot(identical(opt[["arg_type"]], "switch")) default <- opt[["default"]] - stopifnot(is_bool_switch_default(default)) + check_bool_switch_default(default) if (!is.null(opt[["negative_alias"]])) { return(!isFALSE(opt[["negative_alias"]])) } - isTRUE(default) + isTRUE(default) || isTRUE(is.na(default)) } `append<-` <- function(x, after = NULL, value) { diff --git a/tests/testthat/test-regressions.R b/tests/testthat/test-regressions.R index cabe662..0959796 100644 --- a/tests/testthat/test-regressions.R +++ b/tests/testthat/test-regressions.R @@ -218,6 +218,59 @@ test_that("boolean aliases follow logical defaults without consuming NA", { ) }) +test_that("annotated NA boolean switches use explicit switch aliases", { + app_path <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| arg_type: switch", + "flag <- NA", + "cat(sprintf('flag=%s\\n', flag))" + ), + prefix = "rapp-annotated-na-switch-" + ) + + help <- capture.output(Rapp::run(app_path, "--help")) + expect_true(any(grepl("--flag / --no-flag", help, fixed = TRUE))) + + expect_output(Rapp::run(app_path, character()), "flag=NA") + expect_output(Rapp::run(app_path, "--flag"), "flag=TRUE") + expect_output(Rapp::run(app_path, "--no-flag"), "flag=FALSE") +}) + +test_that("disabled boolean aliases are rejected before positional matching", { + true_default_app <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "flag <- TRUE", + "file <- NULL", + "cat(sprintf('flag=%s file=%s\\n', flag, file))" + ), + prefix = "rapp-disabled-positive-alias-positional-" + ) + + expect_error( + Rapp::run(true_default_app, "--flag=false"), + "Arguments not recognized: --flag=false", + fixed = TRUE + ) + + false_default_app <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "flag <- FALSE", + "file <- NULL", + "cat(sprintf('flag=%s file=%s\\n', flag, file))" + ), + prefix = "rapp-disabled-negative-alias-positional-" + ) + + expect_error( + Rapp::run(false_default_app, "--no-flag"), + "Arguments not recognized: --no-flag", + fixed = TRUE + ) +}) + test_that("YAML 1.2 strings are preserved in parsed option values", { app_path <- local_rapp_app( c( From 311a3c4b686d40d5df697d2e9e9f3e01cccb7892 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 15:18:40 -0400 Subject: [PATCH 05/12] Fix boolean switch alias edge cases --- tests/testthat/_snaps/basics.new.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/testthat/_snaps/basics.new.md diff --git a/tests/testthat/_snaps/basics.new.md b/tests/testthat/_snaps/basics.new.md new file mode 100644 index 0000000..cbc617e --- /dev/null +++ b/tests/testthat/_snaps/basics.new.md @@ -0,0 +1,19 @@ +# examples work + + Code + writeLines(run_app("flip-coin.R --help")) + Output + Usage: flip-coin [OPTIONS] + + Flip a coin. + + Options: + -n, --flips Number of coin flips [default: 1] [type: integer] + --sep [default: " "] [type: string] + --wrap / --no-wrap [default: true] Disable with `--no-wrap`. + --seed [default: NA] [type: integer] + + Examples: + flip-coin --flips 3 + flip-coin -n 30 --no-wrap + From 4f4aa8724dd5cea2c052ad92604340aecff82602 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 15:19:47 -0400 Subject: [PATCH 06/12] Remove generated snapshot artifact --- tests/testthat/_snaps/basics.new.md | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 tests/testthat/_snaps/basics.new.md diff --git a/tests/testthat/_snaps/basics.new.md b/tests/testthat/_snaps/basics.new.md deleted file mode 100644 index cbc617e..0000000 --- a/tests/testthat/_snaps/basics.new.md +++ /dev/null @@ -1,19 +0,0 @@ -# examples work - - Code - writeLines(run_app("flip-coin.R --help")) - Output - Usage: flip-coin [OPTIONS] - - Flip a coin. - - Options: - -n, --flips Number of coin flips [default: 1] [type: integer] - --sep [default: " "] [type: string] - --wrap / --no-wrap [default: true] Disable with `--no-wrap`. - --seed [default: NA] [type: integer] - - Examples: - flip-coin --flips 3 - flip-coin -n 30 --no-wrap - From cbc5784f54ea278efb3702306e6eef2b4c87223a Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 15:31:09 -0400 Subject: [PATCH 07/12] Address boolean alias review findings --- R/args.R | 11 +++++++++++ README.md | 2 +- tests/testthat/test-regressions.R | 5 +++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/R/args.R b/R/args.R index dafedca..df87093 100644 --- a/R/args.R +++ b/R/args.R @@ -101,6 +101,17 @@ process_args <- function(args, app) { ) { stop_unrecognized_arg(a) } + if (is.null(spec) && startsWith(a, "--no-")) { + alt_name <- str_drop_prefix(name, "no_") + alt_spec <- app_opts[[alt_name]] + if ( + !is.null(alt_spec) && + identical(alt_spec$arg_type, "switch") && + !has_negative_alias(alt_spec) + ) { + stop_unrecognized_arg(a) + } + } } else { # --name name <- str_drop_prefix(a, "--") diff --git a/README.md b/README.md index 3e49d2d..6338e45 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ To omit the generated `--no-*` alias for a boolean switch, add ``` r #| description: Print version and exit. #| negative_alias: false -version <- TRUE +version <- FALSE ``` Rapp parses option values as YAML 1.2, where bare `yes` and `no` are diff --git a/tests/testthat/test-regressions.R b/tests/testthat/test-regressions.R index 0959796..d14435b 100644 --- a/tests/testthat/test-regressions.R +++ b/tests/testthat/test-regressions.R @@ -269,6 +269,11 @@ test_that("disabled boolean aliases are rejected before positional matching", { "Arguments not recognized: --no-flag", fixed = TRUE ) + expect_error( + Rapp::run(false_default_app, "--no-flag=false"), + "Arguments not recognized: --no-flag=false", + fixed = TRUE + ) }) test_that("YAML 1.2 strings are preserved in parsed option values", { From 2d00c46f7fe725fd315c402c1748c7bcb93bde97 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 17:24:30 -0400 Subject: [PATCH 08/12] Relax boolean switch parsing --- NEWS.md | 8 +-- R/app.R | 2 +- R/args.R | 41 ++++++++++-- R/help.R | 65 +++++++++++-------- R/utils.R | 28 ++++++--- README.md | 14 +++-- docs/boolean-options.md | 26 ++++++++ tests/testthat/test-regressions.R | 101 ++++++++++++++++++++++-------- 8 files changed, 209 insertions(+), 76 deletions(-) create mode 100644 docs/boolean-options.md diff --git a/NEWS.md b/NEWS.md index 5e37e03..05d2c6b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,10 +2,10 @@ ## Breaking changes -- Boolean switch aliases now follow logical defaults: `FALSE` exposes the - positive flag and `TRUE` exposes the generated `--no-*` flag. `NA` remains - a value-taking boolean option, so it can still indicate that an option was - not supplied. +- Boolean switch aliases now follow logical defaults in help output: `FALSE` + exposes the positive flag, `TRUE` exposes the generated `--no-*` flag, and + `NA` exposes both as a tri-state switch. Boolean switches also accept + explicit values such as `--foo=false`. - Rapp now parses YAML with YAML 1.2 semantics. Bare `yes` and `no` non-bool option values are strings, not boolean aliases. Declared bool options still accept YAML 1.1 bool aliases such as `yes`, `no`, diff --git a/R/app.R b/R/app.R index fbbc8c8..09ef208 100644 --- a/R/app.R +++ b/R/app.R @@ -228,7 +228,7 @@ get_app_inputs <- function(app, exprs = app$exprs, pos = integer()) { "NULL" = "string" ), - arg_type = if (is_auto_bool_switch_default(default)) { + arg_type = if (is_bool_switch_default(default)) { "switch" } else if (is.null(default)) { "positional" diff --git a/R/args.R b/R/args.R index df87093..54f34dd 100644 --- a/R/args.R +++ b/R/args.R @@ -19,12 +19,13 @@ process_args <- function(args, app) { for (i in seq_along(app_opts)) { if (identical(short, app_opts[[i]][["short"]])) { if (identical(app_opts[[i]][["arg_type"]], "switch")) { - if (has_positive_alias(app_opts[[i]])) { + if (show_positive_alias(app_opts[[i]])) { return(paste0("--", names(app_opts)[[i]])) } - if (has_negative_alias(app_opts[[i]])) { + if (show_negative_alias(app_opts[[i]])) { return(paste0("--no-", names(app_opts)[[i]])) } + return(paste0("--", names(app_opts)[[i]])) } else { return(paste0("--", names(app_opts)[[i]])) } @@ -86,6 +87,7 @@ process_args <- function(args, app) { # resolve these values in this block name <- val <- spec <- NULL + positive_switch <- FALSE # --name=val equals_idx <- regexpr("=", a) @@ -101,13 +103,15 @@ process_args <- function(args, app) { ) { stop_unrecognized_arg(a) } + if (!is.null(spec) && identical(spec$arg_type, "switch")) { + positive_switch <- TRUE + } if (is.null(spec) && startsWith(a, "--no-")) { alt_name <- str_drop_prefix(name, "no_") alt_spec <- app_opts[[alt_name]] if ( !is.null(alt_spec) && - identical(alt_spec$arg_type, "switch") && - !has_negative_alias(alt_spec) + identical(alt_spec$arg_type, "switch") ) { stop_unrecognized_arg(a) } @@ -125,6 +129,9 @@ process_args <- function(args, app) { ) { stop_unrecognized_arg(a) } + if (!is.null(spec) && identical(spec$arg_type, "switch")) { + positive_switch <- TRUE + } # if flag not known, maybe this is a switch flag if (is.null(spec) && startsWith(a, "--no-")) { @@ -154,10 +161,28 @@ process_args <- function(args, app) { if (is.null(val)) { if (identical(spec$arg_type, "switch")) { - val <- "true" + if (positive_switch) { + next_arg <- readLines(args, 1L) + if (length(next_arg)) { + if (is_bool_cli_value(next_arg)) { + val <- next_arg + } else { + pushBack(next_arg, args) + } + } + } + if (is.null(val)) { + val <- "true" + } } else { # arg_type == "option" val <- readLines(args, 1L) + if (!length(val)) { + stop( + sprintf("Missing value for --%s.", to_kebab_case(name)), + call. = FALSE + ) + } } } @@ -328,5 +353,11 @@ normalize_bool_cli_value <- function(val) { ) } +is_bool_cli_value <- function(val) { + is.character(val) && + length(val) == 1L && + normalize_bool_cli_value(val) %in% c("true", "false") +} + to_snake_case <- function(x) gsub("-", "_", x, fixed = TRUE) to_kebab_case <- function(x) gsub("_", "-", x, fixed = TRUE) diff --git a/R/help.R b/R/help.R index 08481c8..cc381cf 100644 --- a/R/help.R +++ b/R/help.R @@ -194,38 +194,49 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { default_value <- format_default_value(opt$default) positive_flag <- paste0("--", cli_name) negative_flag <- paste0("--no-", cli_name) - show_positive <- has_positive_alias(opt) - show_negative <- has_negative_alias(opt) - flags <- c( - if (show_positive) positive_flag, - if (show_negative) negative_flag - ) - flag <- paste(flags, collapse = " / ") - if (!is.null(short_flag) && nzchar(short_flag)) { - flag <- paste0("-", short_flag, ", ", flag) - } - toggle_note <- if (isTRUE(opt$default)) { - if (show_negative) { - sprintf("Disable with `%s`.", negative_flag) - } else { - character() + show_positive <- show_positive_alias(opt) + show_negative <- show_negative_alias(opt) + if (!show_positive && !show_negative) { + flag <- paste(positive_flag, format_placeholder(name)) + if (!is.null(short_flag) && nzchar(short_flag)) { + flag <- paste0("-", short_flag, ", ", flag) } - } else if (isFALSE(opt$default)) { - if (show_positive) { - sprintf("Enable with `%s`.", positive_flag) - } else { - character() + if (!is.null(default_value)) { + details <- c(details, sprintf("[default: %s]", default_value)) } + details <- c(details, "[type: bool]") } else { - c( - if (show_positive) sprintf("Enable with `%s`.", positive_flag), - if (show_negative) sprintf("Disable with `%s`.", negative_flag) + flags <- c( + if (show_positive) positive_flag, + if (show_negative) negative_flag ) + flag <- paste(flags, collapse = " / ") + if (!is.null(short_flag) && nzchar(short_flag)) { + flag <- paste0("-", short_flag, ", ", flag) + } + toggle_note <- if (isTRUE(opt$default)) { + if (show_negative) { + sprintf("Disable with `%s`.", negative_flag) + } else { + character() + } + } else if (isFALSE(opt$default)) { + if (show_positive) { + sprintf("Enable with `%s`.", positive_flag) + } else { + character() + } + } else { + c( + if (show_positive) sprintf("Enable with `%s`.", positive_flag), + if (show_negative) sprintf("Disable with `%s`.", negative_flag) + ) + } + if (!is.null(default_value)) { + details <- c(details, sprintf("[default: %s]", default_value)) + } + details <- c(details, toggle_note) } - if (!is.null(default_value)) { - details <- c(details, sprintf("[default: %s]", default_value)) - } - details <- c(details, toggle_note) } if (identical(opt$action, "append")) { diff --git a/R/utils.R b/R/utils.R index aee6929..d7bf079 100644 --- a/R/utils.R +++ b/R/utils.R @@ -68,10 +68,6 @@ compact <- function(x) x[lengths(x) > 0] `%||%` <- function(x, y) if (is.null(x)) y else x `subtract<-` <- function(x, value) x - value -is_auto_bool_switch_default <- function(x) { - identical(x, TRUE) || identical(x, FALSE) -} - is_bool_switch_default <- function(x) is.logical(x) && length(x) == 1L check_bool_switch_default <- function(default) { @@ -89,9 +85,7 @@ has_positive_alias <- function(opt) { default <- opt[["default"]] check_bool_switch_default(default) - isFALSE(default) || - isTRUE(is.na(default)) || - isFALSE(opt[["negative_alias"]]) + TRUE } has_negative_alias <- function(opt) { @@ -104,7 +98,25 @@ has_negative_alias <- function(opt) { return(!isFALSE(opt[["negative_alias"]])) } - isTRUE(default) || isTRUE(is.na(default)) + TRUE +} + +show_positive_alias <- function(opt) { + stopifnot(identical(opt[["arg_type"]], "switch")) + + default <- opt[["default"]] + check_bool_switch_default(default) + + isFALSE(default) || isTRUE(is.na(default)) +} + +show_negative_alias <- function(opt) { + stopifnot(identical(opt[["arg_type"]], "switch")) + + default <- opt[["default"]] + check_bool_switch_default(default) + + has_negative_alias(opt) && (isTRUE(default) || isTRUE(is.na(default))) } `append<-` <- function(x, after = NULL, value) { diff --git a/README.md b/README.md index 6338e45..464753e 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ flip-coin --n=1 flip-coin --n 1 ``` -Assignments of `TRUE` or `FALSE` are a little different from other +Assignments of `TRUE`, `FALSE`, or `NA` are a little different from other options. They support usage as switches or toggles at the command line, and the default controls which aliases are exposed. For example in an R script: @@ -129,9 +129,10 @@ my-app --no-echo # FALSE ``` With `echo <- FALSE`, the positive alias `--echo` is supported. -Assignments of `NA` remain boolean options that take a value, so omitting -`echo <- NA` leaves `echo` as `NA` and supplying `--echo true` or -`--echo false` records the caller's choice. +Assignments of `NA` are tri-state boolean switches: omitting the flag +leaves the value as `NA`, `--echo` sets it to `TRUE`, and `--no-echo` +sets it to `FALSE`. Boolean switches also accept explicit values, such +as `--echo=true`, `--echo=false`, `--echo true`, and `--echo false`. To omit the generated `--no-*` alias for a boolean switch, add `#| negative_alias: false` above the assignment: @@ -147,6 +148,9 @@ strings rather than boolean aliases for non-bool values. For declared bool options, Rapp also accepts YAML 1.1 bool aliases such as `yes`, `no`, `y`, `n`, `on`, and `off` for backward compatibility. +See [Boolean option behavior](docs/boolean-options.md) for a full table +of boolean defaults, annotations, and accepted command-line forms. + Assigning `c()` or `list()` declares an option that can be supplied multiple times. Use `c()` when you want to keep the exact strings provided on the command line, and `list()` when you want Rapp to attempt @@ -373,7 +377,7 @@ command line arguments. | Assignment of `NULL`
`foo <- NULL` | Positional Arg
`APP foo-value` | | Assignment of `FALSE`
`foo <- FALSE` | Boolean switch
`APP --foo` | | Assignment of `TRUE`
`foo <- TRUE` | Boolean switch
`APP --no-foo` | -| Assignment of `NA`
`foo <- NA` | Boolean option
`APP --foo true` or `APP --foo false` | +| Assignment of `NA`
`foo <- NA` | Tri-state boolean switch
`APP --foo` or `APP --no-foo` | | Assignment of `c()` or `list()`
`foo <- c()` | Repeatable option
`APP --foo val1 --foo val2` | | Assignment of `NULL` to name with `...`
`args... <- NULL` | Positional Arg Collector
`APP foo bar baz` | | Switch with string literal
`switch("", cmd1 = {}, cmd2 = {})` | Required commands
`APP --help`
`APP cmd1 --help`
`APP cmd2 --help` | diff --git a/docs/boolean-options.md b/docs/boolean-options.md new file mode 100644 index 0000000..cbfa31b --- /dev/null +++ b/docs/boolean-options.md @@ -0,0 +1,26 @@ +# Boolean Option Behavior + +Logical defaults become boolean command-line inputs. Help output shows the +most useful spelling for changing the default, while parsing also accepts +explicit boolean values for convenience. + +In the table below, `TRUE`, `FALSE`, and `NA` are the resulting R values. +`reject` means the command-line form is not accepted. + +| R definition | Help shows | Omitted | `--foo` | `--no-foo` | `--foo=true` | `--foo=false` | `--foo true` | `--foo false` | +|---|---|---:|---:|---:|---:|---:|---:|---:| +| `foo <- FALSE` | `--foo` | `FALSE` | `TRUE` | `FALSE` | `TRUE` | `FALSE` | `TRUE` | `FALSE` | +| `foo <- TRUE` | `--no-foo` | `TRUE` | `TRUE` | `FALSE` | `TRUE` | `FALSE` | `TRUE` | `FALSE` | +| `foo <- NA` | `--foo / --no-foo` | `NA` | `TRUE` | `FALSE` | `TRUE` | `FALSE` | `TRUE` | `FALSE` | +| `#| negative_alias: false`
`foo <- FALSE` | `--foo` | `FALSE` | `TRUE` | reject | `TRUE` | `FALSE` | `TRUE` | `FALSE` | +| `#| negative_alias: false`
`foo <- TRUE` | `--foo ` | `TRUE` | `TRUE` | reject | `TRUE` | `FALSE` | `TRUE` | `FALSE` | +| `#| negative_alias: false`
`foo <- NA` | `--foo` | `NA` | `TRUE` | reject | `TRUE` | `FALSE` | `TRUE` | `FALSE` | +| `#| arg_type: option`
`foo <- NA` | `--foo ` | `NA` | reject | reject | `TRUE` | `FALSE` | `TRUE` | `FALSE` | + +Bare `--foo` and `--no-foo` count as supplied values for boolean switches. +Use `#| arg_type: option` when a boolean input should require an explicit +value after `--foo`. + +`#| negative_alias: false` only disables the generated `--no-foo` spelling. +It does not disable the positive spelling or explicit value forms such as +`--foo=false`. diff --git a/tests/testthat/test-regressions.R b/tests/testthat/test-regressions.R index d14435b..1f34fdb 100644 --- a/tests/testthat/test-regressions.R +++ b/tests/testthat/test-regressions.R @@ -156,7 +156,7 @@ test_that("boolean switches can disable negative aliases", { expect_true(any(grepl("--no-legacy", negative_help, fixed = TRUE))) }) -test_that("boolean aliases follow logical defaults without consuming NA", { +test_that("boolean switches accept default-driven and explicit value forms", { app_path <- local_rapp_app( c( "#!/usr/bin/env Rapp", @@ -180,11 +180,23 @@ test_that("boolean aliases follow logical defaults without consuming NA", { "disabled=FALSE enabled=TRUE unknown=NA" ) expect_output( - Rapp::run(app_path, c("--disabled", "--no-enabled", "--unknown", "true")), + Rapp::run(app_path, c("--disabled", "--no-enabled", "--unknown")), "disabled=TRUE enabled=FALSE unknown=TRUE" ) expect_output( - Rapp::run(app_path, "--unknown=false"), + Rapp::run(app_path, c("--disabled", "false", "--enabled", "false")), + "disabled=FALSE enabled=FALSE unknown=NA" + ) + expect_output( + Rapp::run(app_path, c("--disabled=false", "--enabled=false")), + "disabled=FALSE enabled=FALSE unknown=NA" + ) + expect_output( + Rapp::run(app_path, c("--no-disabled", "--enabled", "--unknown=false")), + "disabled=FALSE enabled=TRUE unknown=FALSE" + ) + expect_output( + Rapp::run(app_path, c("--unknown", "false")), "disabled=FALSE enabled=TRUE unknown=FALSE" ) @@ -193,28 +205,25 @@ test_that("boolean aliases follow logical defaults without consuming NA", { expect_false(any(grepl("--no-disabled", help, fixed = TRUE))) expect_false(any(grepl("--enabled", help, fixed = TRUE))) expect_true(any(grepl("--no-enabled", help, fixed = TRUE))) - expect_true(any(grepl("--unknown ", help, fixed = TRUE))) - expect_false(any(grepl("--no-unknown", help, fixed = TRUE))) + expect_true(any(grepl("--unknown / --no-unknown", help, fixed = TRUE))) +}) - expect_error( - Rapp::run(app_path, "--no-disabled"), - "Arguments not recognized: --no-disabled", - fixed = TRUE - ) - expect_error( - Rapp::run(app_path, "--enabled"), - "Arguments not recognized: --enabled", - fixed = TRUE - ) - expect_error( - Rapp::run(app_path, "--enabled=false"), - "Arguments not recognized: --enabled=false", - fixed = TRUE +test_that("short aliases follow the default-driven help spelling", { + app_path <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| short: d", + "disabled <- FALSE", + "#| short: e", + "enabled <- TRUE", + "cat(sprintf('disabled=%s enabled=%s\\n', disabled, enabled))" + ), + prefix = "rapp-bool-short-aliases-" ) - expect_error( - Rapp::run(app_path, "--no-unknown"), - "Arguments not recognized: --no-unknown", - fixed = TRUE + + expect_output( + Rapp::run(app_path, c("-d", "-e")), + "disabled=TRUE enabled=FALSE" ) }) @@ -237,10 +246,11 @@ test_that("annotated NA boolean switches use explicit switch aliases", { expect_output(Rapp::run(app_path, "--no-flag"), "flag=FALSE") }) -test_that("disabled boolean aliases are rejected before positional matching", { +test_that("negative_alias false only rejects negative aliases", { true_default_app <- local_rapp_app( c( "#!/usr/bin/env Rapp", + "#| negative_alias: false", "flag <- TRUE", "file <- NULL", "cat(sprintf('flag=%s file=%s\\n', flag, file))" @@ -248,15 +258,29 @@ test_that("disabled boolean aliases are rejected before positional matching", { prefix = "rapp-disabled-positive-alias-positional-" ) + expect_output( + Rapp::run(true_default_app, c("--flag=false", "input")), + "flag=FALSE file=input" + ) + expect_output( + Rapp::run(true_default_app, c("--flag", "false", "input")), + "flag=FALSE file=input" + ) + expect_output( + Rapp::run(true_default_app, c("--flag", "input")), + "flag=TRUE file=input" + ) + expect_error( - Rapp::run(true_default_app, "--flag=false"), - "Arguments not recognized: --flag=false", + Rapp::run(true_default_app, "--no-flag"), + "Arguments not recognized: --no-flag", fixed = TRUE ) false_default_app <- local_rapp_app( c( "#!/usr/bin/env Rapp", + "#| negative_alias: false", "flag <- FALSE", "file <- NULL", "cat(sprintf('flag=%s file=%s\\n', flag, file))" @@ -276,6 +300,31 @@ test_that("disabled boolean aliases are rejected before positional matching", { ) }) +test_that("boolean options can require explicit values", { + app_path <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| arg_type: option", + "flag <- NA", + "cat(sprintf('flag=%s\\n', flag))" + ), + prefix = "rapp-explicit-bool-option-" + ) + + expect_output(Rapp::run(app_path, c("--flag", "false")), "flag=FALSE") + expect_output(Rapp::run(app_path, "--flag=false"), "flag=FALSE") + expect_error( + Rapp::run(app_path, "--flag"), + "Missing value for --flag.", + fixed = TRUE + ) + expect_error( + Rapp::run(app_path, "--no-flag"), + "Arguments not recognized: --no-flag", + fixed = TRUE + ) +}) + test_that("YAML 1.2 strings are preserved in parsed option values", { app_path <- local_rapp_app( c( From 0ed5abba1c82260f8d7baf984d78d58fbe58e1f4 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 20:53:32 -0400 Subject: [PATCH 09/12] Clarify boolean switch alias helpers --- R/args.R | 10 +++++----- R/help.R | 4 ++-- R/utils.R | 12 +++++++----- docs/boolean-options.md | 7 ++++++- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/R/args.R b/R/args.R index 54f34dd..f2c50b6 100644 --- a/R/args.R +++ b/R/args.R @@ -19,10 +19,10 @@ process_args <- function(args, app) { for (i in seq_along(app_opts)) { if (identical(short, app_opts[[i]][["short"]])) { if (identical(app_opts[[i]][["arg_type"]], "switch")) { - if (show_positive_alias(app_opts[[i]])) { + if (shows_positive_alias(app_opts[[i]])) { return(paste0("--", names(app_opts)[[i]])) } - if (show_negative_alias(app_opts[[i]])) { + if (shows_negative_alias(app_opts[[i]])) { return(paste0("--no-", names(app_opts)[[i]])) } return(paste0("--", names(app_opts)[[i]])) @@ -99,7 +99,7 @@ process_args <- function(args, app) { if ( !is.null(spec) && identical(spec$arg_type, "switch") && - !has_positive_alias(spec) + !accepts_positive_alias(spec) ) { stop_unrecognized_arg(a) } @@ -125,7 +125,7 @@ process_args <- function(args, app) { if ( !is.null(spec) && identical(spec$arg_type, "switch") && - !has_positive_alias(spec) + !accepts_positive_alias(spec) ) { stop_unrecognized_arg(a) } @@ -141,7 +141,7 @@ process_args <- function(args, app) { !is.null(alt_spec) && identical(alt_spec$arg_type, "switch") ) { - if (has_negative_alias(alt_spec)) { + if (accepts_negative_alias(alt_spec)) { spec <- alt_spec val <- "false" name <- alt_name diff --git a/R/help.R b/R/help.R index cc381cf..b0031ae 100644 --- a/R/help.R +++ b/R/help.R @@ -194,8 +194,8 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { default_value <- format_default_value(opt$default) positive_flag <- paste0("--", cli_name) negative_flag <- paste0("--no-", cli_name) - show_positive <- show_positive_alias(opt) - show_negative <- show_negative_alias(opt) + show_positive <- shows_positive_alias(opt) + show_negative <- shows_negative_alias(opt) if (!show_positive && !show_negative) { flag <- paste(positive_flag, format_placeholder(name)) if (!is.null(short_flag) && nzchar(short_flag)) { diff --git a/R/utils.R b/R/utils.R index d7bf079..94800c9 100644 --- a/R/utils.R +++ b/R/utils.R @@ -79,7 +79,9 @@ check_bool_switch_default <- function(default) { } } -has_positive_alias <- function(opt) { +# Parsing is intentionally more permissive than help. accepts_* controls +# command-line forms; shows_* controls the concise help surface. +accepts_positive_alias <- function(opt) { stopifnot(identical(opt[["arg_type"]], "switch")) default <- opt[["default"]] @@ -88,7 +90,7 @@ has_positive_alias <- function(opt) { TRUE } -has_negative_alias <- function(opt) { +accepts_negative_alias <- function(opt) { stopifnot(identical(opt[["arg_type"]], "switch")) default <- opt[["default"]] @@ -101,7 +103,7 @@ has_negative_alias <- function(opt) { TRUE } -show_positive_alias <- function(opt) { +shows_positive_alias <- function(opt) { stopifnot(identical(opt[["arg_type"]], "switch")) default <- opt[["default"]] @@ -110,13 +112,13 @@ show_positive_alias <- function(opt) { isFALSE(default) || isTRUE(is.na(default)) } -show_negative_alias <- function(opt) { +shows_negative_alias <- function(opt) { stopifnot(identical(opt[["arg_type"]], "switch")) default <- opt[["default"]] check_bool_switch_default(default) - has_negative_alias(opt) && (isTRUE(default) || isTRUE(is.na(default))) + accepts_negative_alias(opt) && (isTRUE(default) || isTRUE(is.na(default))) } `append<-` <- function(x, after = NULL, value) { diff --git a/docs/boolean-options.md b/docs/boolean-options.md index cbfa31b..524d51f 100644 --- a/docs/boolean-options.md +++ b/docs/boolean-options.md @@ -4,6 +4,10 @@ Logical defaults become boolean command-line inputs. Help output shows the most useful spelling for changing the default, while parsing also accepts explicit boolean values for convenience. +Help output is intentionally narrower than parsing. For example, `foo <- TRUE` +normally shows `--no-foo`, but `--foo=false` is still accepted because it is an +explicit boolean value for `foo`. + In the table below, `TRUE`, `FALSE`, and `NA` are the resulting R values. `reject` means the command-line form is not accepted. @@ -23,4 +27,5 @@ value after `--foo`. `#| negative_alias: false` only disables the generated `--no-foo` spelling. It does not disable the positive spelling or explicit value forms such as -`--foo=false`. +`--foo=false`. With `foo <- TRUE`, that leaves no default-changing bare alias, +so help shows `--foo ` as an explicit-value form. From 16dd0bf96979d4e49bd96f998eaaad83a6fcd43e Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 21:11:13 -0400 Subject: [PATCH 10/12] Clarify boolean switch defaults in help --- R/help.R | 37 ++++++++----------------- tests/testthat/_snaps/basics.md | 2 +- tests/testthat/_snaps/help-snapshots.md | 6 ++-- tests/testthat/test-help-annotations.R | 4 +-- tests/testthat/test-regressions.R | 5 ++-- 5 files changed, 20 insertions(+), 34 deletions(-) diff --git a/R/help.R b/R/help.R index b0031ae..108d6e3 100644 --- a/R/help.R +++ b/R/help.R @@ -170,6 +170,15 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { } deparse1(value) } + format_switch_default <- function(default) { + if (isTRUE(default)) { + "[enabled by default]" + } else if (isFALSE(default)) { + "[disabled by default]" + } else { + "[unset by default]" + } + } format_option_entry <- function(opt, name) { cli_name <- format_cli_name(name) short_flag <- opt[["short"]] @@ -191,7 +200,6 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { details <- c(details, sprintf("[type: %s]", opt$val_type)) } } else if (identical(opt$arg_type, "switch")) { - default_value <- format_default_value(opt$default) positive_flag <- paste0("--", cli_name) negative_flag <- paste0("--no-", cli_name) show_positive <- shows_positive_alias(opt) @@ -201,9 +209,7 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { if (!is.null(short_flag) && nzchar(short_flag)) { flag <- paste0("-", short_flag, ", ", flag) } - if (!is.null(default_value)) { - details <- c(details, sprintf("[default: %s]", default_value)) - } + details <- c(details, format_switch_default(opt$default)) details <- c(details, "[type: bool]") } else { flags <- c( @@ -214,28 +220,7 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { if (!is.null(short_flag) && nzchar(short_flag)) { flag <- paste0("-", short_flag, ", ", flag) } - toggle_note <- if (isTRUE(opt$default)) { - if (show_negative) { - sprintf("Disable with `%s`.", negative_flag) - } else { - character() - } - } else if (isFALSE(opt$default)) { - if (show_positive) { - sprintf("Enable with `%s`.", positive_flag) - } else { - character() - } - } else { - c( - if (show_positive) sprintf("Enable with `%s`.", positive_flag), - if (show_negative) sprintf("Disable with `%s`.", negative_flag) - ) - } - if (!is.null(default_value)) { - details <- c(details, sprintf("[default: %s]", default_value)) - } - details <- c(details, toggle_note) + details <- c(details, format_switch_default(opt$default)) } } diff --git a/tests/testthat/_snaps/basics.md b/tests/testthat/_snaps/basics.md index 772e4d4..d39b097 100644 --- a/tests/testthat/_snaps/basics.md +++ b/tests/testthat/_snaps/basics.md @@ -10,7 +10,7 @@ Options: -n, --flips Number of coin flips [default: 1] [type: integer] --sep [default: " "] [type: string] - --no-wrap [default: true] Disable with `--no-wrap`. + --no-wrap [enabled by default] --seed [default: NA] [type: integer] Examples: diff --git a/tests/testthat/_snaps/help-snapshots.md b/tests/testthat/_snaps/help-snapshots.md index 331aeba..0247bf7 100644 --- a/tests/testthat/_snaps/help-snapshots.md +++ b/tests/testthat/_snaps/help-snapshots.md @@ -10,7 +10,7 @@ Options: -n, --flips Number of coin flips [default: 1] [type: integer] --sep [default: " "] [type: string] - --no-wrap [default: true] Disable with `--no-wrap`. + --no-wrap [enabled by default] --seed [default: NA] [type: integer] Examples: @@ -117,11 +117,11 @@ Options: --child2-opt [default: "child2-default"] [type: string] - --child2-switch [default: false] Enable with `--child2-switch`. + --child2-switch [disabled by default] Parent options: --parent-opt [default: "parent-default"] [type: string] - --no-parent-switch [default: true] Disable with `--no-parent-switch`. + --no-parent-switch [enabled by default] Global options: --top-opt [default: "top-default"] [type: string] diff --git a/tests/testthat/test-help-annotations.R b/tests/testthat/test-help-annotations.R index 3e22145..0d68978 100644 --- a/tests/testthat/test-help-annotations.R +++ b/tests/testthat/test-help-annotations.R @@ -229,14 +229,14 @@ test_that("help output lists option defaults, types, and toggle hints", { "#| description: Wrap output.", "wrap <- TRUE" ), - patterns = c("[default: true]", "Disable with `--no-wrap`.") + patterns = c("[enabled by default]") ), switch_false = list( option = c( "#| description: Verbose output.", "verbose <- FALSE" ), - patterns = c("[default: false]", "Enable with `--verbose`.") + patterns = c("[disabled by default]") ) ) diff --git a/tests/testthat/test-regressions.R b/tests/testthat/test-regressions.R index 1f34fdb..d9d182d 100644 --- a/tests/testthat/test-regressions.R +++ b/tests/testthat/test-regressions.R @@ -137,8 +137,9 @@ test_that("boolean switches can disable negative aliases", { ) true_default_help <- capture.output(Rapp::run(true_default_app, "--help")) expect_false(any(grepl("--no-wrap", true_default_help, fixed = TRUE))) - expect_false(any(grepl( - "Enable with `--wrap`.", + expect_true(any(grepl("--wrap ", true_default_help, fixed = TRUE))) + expect_true(any(grepl( + "[enabled by default]", true_default_help, fixed = TRUE ))) From b49d9ee41b41f56ca2bb710131b575da7481b773 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 22:22:32 -0400 Subject: [PATCH 11/12] Reject negative aliases for explicit bool options --- R/args.R | 12 +++++++++++- tests/testthat/test-regressions.R | 18 +++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/R/args.R b/R/args.R index f2c50b6..e177cf9 100644 --- a/R/args.R +++ b/R/args.R @@ -13,6 +13,11 @@ process_args <- function(args, app) { stop_unrecognized_arg <- function(arg) { stop("Arguments not recognized: ", arg, call. = FALSE) } + is_bool_option_spec <- function(spec) { + !is.null(spec) && + identical(spec$arg_type, "option") && + identical(spec$val_type, "bool") + } short_opt_to_long_opt <- function(short_opt) { short <- str_drop_prefix(short_opt, "-") @@ -111,7 +116,10 @@ process_args <- function(args, app) { alt_spec <- app_opts[[alt_name]] if ( !is.null(alt_spec) && - identical(alt_spec$arg_type, "switch") + ( + identical(alt_spec$arg_type, "switch") || + is_bool_option_spec(alt_spec) + ) ) { stop_unrecognized_arg(a) } @@ -148,6 +156,8 @@ process_args <- function(args, app) { } else { stop_unrecognized_arg(a) } + } else if (is_bool_option_spec(alt_spec)) { + stop_unrecognized_arg(a) } } } diff --git a/tests/testthat/test-regressions.R b/tests/testthat/test-regressions.R index d9d182d..7ad22af 100644 --- a/tests/testthat/test-regressions.R +++ b/tests/testthat/test-regressions.R @@ -307,13 +307,20 @@ test_that("boolean options can require explicit values", { "#!/usr/bin/env Rapp", "#| arg_type: option", "flag <- NA", - "cat(sprintf('flag=%s\\n', flag))" + "file <- NULL", + "cat(sprintf('flag=%s file=%s\\n', flag, file))" ), prefix = "rapp-explicit-bool-option-" ) - expect_output(Rapp::run(app_path, c("--flag", "false")), "flag=FALSE") - expect_output(Rapp::run(app_path, "--flag=false"), "flag=FALSE") + expect_output( + Rapp::run(app_path, c("--flag", "false", "input")), + "flag=FALSE file=input" + ) + expect_output( + Rapp::run(app_path, c("--flag=false", "input")), + "flag=FALSE file=input" + ) expect_error( Rapp::run(app_path, "--flag"), "Missing value for --flag.", @@ -324,6 +331,11 @@ test_that("boolean options can require explicit values", { "Arguments not recognized: --no-flag", fixed = TRUE ) + expect_error( + Rapp::run(app_path, "--no-flag=false"), + "Arguments not recognized: --no-flag=false", + fixed = TRUE + ) }) test_that("YAML 1.2 strings are preserved in parsed option values", { From 5a81d85723b1c97ffbfcf789a218fe03cb109711 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 22:27:13 -0400 Subject: [PATCH 12/12] Omit generated switch defaults from help --- R/help.R | 13 +------------ tests/testthat/_snaps/basics.md | 2 +- tests/testthat/_snaps/help-snapshots.md | 6 +++--- tests/testthat/test-help-annotations.R | 18 +++++++++++++++--- tests/testthat/test-regressions.R | 5 +++++ 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/R/help.R b/R/help.R index 108d6e3..ee37eee 100644 --- a/R/help.R +++ b/R/help.R @@ -170,15 +170,6 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { } deparse1(value) } - format_switch_default <- function(default) { - if (isTRUE(default)) { - "[enabled by default]" - } else if (isFALSE(default)) { - "[disabled by default]" - } else { - "[unset by default]" - } - } format_option_entry <- function(opt, name) { cli_name <- format_cli_name(name) short_flag <- opt[["short"]] @@ -209,7 +200,6 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { if (!is.null(short_flag) && nzchar(short_flag)) { flag <- paste0("-", short_flag, ", ", flag) } - details <- c(details, format_switch_default(opt$default)) details <- c(details, "[type: bool]") } else { flags <- c( @@ -220,7 +210,6 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { if (!is.null(short_flag) && nzchar(short_flag)) { flag <- paste0("-", short_flag, ", ", flag) } - details <- c(details, format_switch_default(opt$default)) } } @@ -272,7 +261,7 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { ctx <- label_context(entry$label, indent, label_width) text <- entry$text if (!length(text)) { - out <- c(out, ctx$initial) + out <- c(out, paste0(strrep(" ", indent), entry$label)) next } for (i in seq_along(text)) { diff --git a/tests/testthat/_snaps/basics.md b/tests/testthat/_snaps/basics.md index d39b097..ce7b15e 100644 --- a/tests/testthat/_snaps/basics.md +++ b/tests/testthat/_snaps/basics.md @@ -10,7 +10,7 @@ Options: -n, --flips Number of coin flips [default: 1] [type: integer] --sep [default: " "] [type: string] - --no-wrap [enabled by default] + --no-wrap --seed [default: NA] [type: integer] Examples: diff --git a/tests/testthat/_snaps/help-snapshots.md b/tests/testthat/_snaps/help-snapshots.md index 0247bf7..30271ac 100644 --- a/tests/testthat/_snaps/help-snapshots.md +++ b/tests/testthat/_snaps/help-snapshots.md @@ -10,7 +10,7 @@ Options: -n, --flips Number of coin flips [default: 1] [type: integer] --sep [default: " "] [type: string] - --no-wrap [enabled by default] + --no-wrap --seed [default: NA] [type: integer] Examples: @@ -117,11 +117,11 @@ Options: --child2-opt [default: "child2-default"] [type: string] - --child2-switch [disabled by default] + --child2-switch Parent options: --parent-opt [default: "parent-default"] [type: string] - --no-parent-switch [enabled by default] + --no-parent-switch Global options: --top-opt [default: "top-default"] [type: string] diff --git a/tests/testthat/test-help-annotations.R b/tests/testthat/test-help-annotations.R index 0d68978..da2dad8 100644 --- a/tests/testthat/test-help-annotations.R +++ b/tests/testthat/test-help-annotations.R @@ -188,7 +188,7 @@ test_that("required-like annotation keys do not partially match", { ) }) -test_that("help output lists option defaults, types, and toggle hints", { +test_that("help output lists option defaults, types, and switch descriptions", { build_help <- function(option_block, prefix) { help_lines_from_script( c( @@ -229,14 +229,16 @@ test_that("help output lists option defaults, types, and toggle hints", { "#| description: Wrap output.", "wrap <- TRUE" ), - patterns = c("[enabled by default]") + patterns = c("Wrap output."), + absent_patterns = c("[default: true]", "[enabled by default]") ), switch_false = list( option = c( "#| description: Verbose output.", "verbose <- FALSE" ), - patterns = c("[disabled by default]") + patterns = c("Verbose output."), + absent_patterns = c("[default: false]", "[disabled by default]") ) ) @@ -252,6 +254,16 @@ test_that("help output lists option defaults, types, and toggle hints", { info = sprintf("Missing pattern '%s' for case '%s'", pattern, case_name) ) } + for (pattern in case[["absent_patterns"]] %||% character()) { + expect_false( + any(grepl(pattern, lines, fixed = TRUE)), + info = sprintf( + "Unexpected pattern '%s' for case '%s'", + pattern, + case_name + ) + ) + } } }) diff --git a/tests/testthat/test-regressions.R b/tests/testthat/test-regressions.R index 7ad22af..2aecee0 100644 --- a/tests/testthat/test-regressions.R +++ b/tests/testthat/test-regressions.R @@ -139,6 +139,11 @@ test_that("boolean switches can disable negative aliases", { expect_false(any(grepl("--no-wrap", true_default_help, fixed = TRUE))) expect_true(any(grepl("--wrap ", true_default_help, fixed = TRUE))) expect_true(any(grepl( + "Keep output wrapped.", + true_default_help, + fixed = TRUE + ))) + expect_false(any(grepl( "[enabled by default]", true_default_help, fixed = TRUE