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))) +})