diff --git a/NEWS.md b/NEWS.md index 1c649cf..05d2c6b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,10 @@ ## Breaking changes +- 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`, @@ -21,6 +25,8 @@ ## Bug fixes +- Boolean switches annotated with `#| negative_alias: false` no longer include + or accept a generated `--no-*` alias (#24). - On macOS, `install_pkg_cli_apps()` now adds the default `~/.local/bin` install directory to the user's zsh profile when it is not already on `PATH`, respecting `ZDOTDIR` and warning rather than failing if diff --git a/R/app.R b/R/app.R index 5dce2e6..09ef208 100644 --- a/R/app.R +++ b/R/app.R @@ -106,7 +106,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` # @@ -228,7 +228,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 9e0b4be..e177cf9 100644 --- a/R/args.R +++ b/R/args.R @@ -10,11 +10,30 @@ process_args <- function(args, app) { on.exit(close(args)) } + 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, "-") 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 (shows_positive_alias(app_opts[[i]])) { + return(paste0("--", names(app_opts)[[i]])) + } + if (shows_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]])) + } } } } @@ -73,6 +92,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) @@ -81,20 +101,63 @@ 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") && + !accepts_positive_alias(spec) + ) { + 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") || + is_bool_option_spec(alt_spec) + ) + ) { + stop_unrecognized_arg(a) + } + } } 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") && + !accepts_positive_alias(spec) + ) { + 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-")) { alt_name <- str_drop_prefix(name, "no_") - spec <- app_opts[[alt_name]] - if (!is.null(spec)) { - val <- "false" - name <- alt_name + alt_spec <- app_opts[[alt_name]] + if ( + !is.null(alt_spec) && + identical(alt_spec$arg_type, "switch") + ) { + if (accepts_negative_alias(alt_spec)) { + spec <- alt_spec + val <- "false" + name <- alt_name + } else { + stop_unrecognized_arg(a) + } + } else if (is_bool_option_spec(alt_spec)) { + stop_unrecognized_arg(a) } } } @@ -108,10 +171,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 + ) + } } } @@ -282,5 +363,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 050f7f5..ee37eee 100644 --- a/R/help.R +++ b/R/help.R @@ -191,18 +191,26 @@ 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) - toggle_flag <- paste0("--no-", cli_name) - toggle_note <- if (isTRUE(opt$default)) { - sprintf("Disable with `%s`.", toggle_flag) + positive_flag <- paste0("--", cli_name) + negative_flag <- paste0("--no-", cli_name) + 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)) { + flag <- paste0("-", short_flag, ", ", flag) + } + details <- c(details, "[type: bool]") } else { - sprintf("Enable with `%s`.", paste0("--", cli_name)) - } - if (!is.null(default_value)) { - details <- c(details, sprintf("[default: %s]", default_value)) + 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) + } } - details <- c(details, toggle_note) - flag <- paste(flag, "/", toggle_flag) } if (identical(opt$action, "append")) { @@ -253,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/R/utils.R b/R/utils.R index 1ba4261..94800c9 100644 --- a/R/utils.R +++ b/R/utils.R @@ -68,6 +68,59 @@ 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 + +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 + ) + } +} + +# 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"]] + check_bool_switch_default(default) + + TRUE +} + +accepts_negative_alias <- function(opt) { + stopifnot(identical(opt[["arg_type"]], "switch")) + + default <- opt[["default"]] + check_bool_switch_default(default) + + if (!is.null(opt[["negative_alias"]])) { + return(!isFALSE(opt[["negative_alias"]])) + } + + TRUE +} + +shows_positive_alias <- function(opt) { + stopifnot(identical(opt[["arg_type"]], "switch")) + + default <- opt[["default"]] + check_bool_switch_default(default) + + isFALSE(default) || isTRUE(is.na(default)) +} + +shows_negative_alias <- function(opt) { + stopifnot(identical(opt[["arg_type"]], "switch")) + + default <- opt[["default"]] + check_bool_switch_default(default) + + accepts_negative_alias(opt) && (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 5fff9eb..464753e 100644 --- a/README.md +++ b/README.md @@ -113,24 +113,34 @@ 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: +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: ``` 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=true # TRUE -my-app --echo=1 # TRUE - my-app --no-echo # FALSE -my-app --echo=false # FALSE -my-app --echo=0 # FALSE +``` + +With `echo <- FALSE`, the positive alias `--echo` is supported. +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: + +``` r +#| description: Print version and exit. +#| negative_alias: false +version <- FALSE ``` Rapp parses option values as YAML 1.2, where bare `yes` and `no` are @@ -138,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 @@ -348,6 +361,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`: override whether boolean switches include a generated + `--no-*` alias. - `examples`: usage examples to show in `--help`. Use a YAML sequence to list multiple examples. @@ -360,7 +375,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` | 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..524d51f --- /dev/null +++ b/docs/boolean-options.md @@ -0,0 +1,31 @@ +# 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. + +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. + +| 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`. With `foo <- TRUE`, that leaves no default-changing bare alias, +so help shows `--foo ` as an explicit-value form. diff --git a/tests/testthat/_snaps/basics.md b/tests/testthat/_snaps/basics.md index cbc617e..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] - --wrap / --no-wrap [default: true] Disable with `--no-wrap`. + --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 3d01a6d..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] - --wrap / --no-wrap [default: true] Disable with `--no-wrap`. + --no-wrap --seed [default: NA] [type: integer] Examples: @@ -116,14 +116,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 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 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..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("[default: true]", "Disable with `--no-wrap`.") + patterns = c("Wrap output."), + absent_patterns = c("[default: true]", "[enabled by default]") ), switch_false = list( option = c( "#| description: Verbose output.", "verbose <- FALSE" ), - patterns = c("[default: false]", "Enable with `--verbose`.") + 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 094253b..2aecee0 100644 --- a/tests/testthat/test-regressions.R +++ b/tests/testthat/test-regressions.R @@ -99,6 +99,250 @@ 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_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 + ))) + + negative_app <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| description: Legacy spelling is ignored.", + "#| negative: false", + "legacy <- TRUE" + ), + 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 switches accept default-driven and explicit value forms", { + 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, character()), + "disabled=FALSE enabled=TRUE unknown=NA" + ) + expect_output( + Rapp::run(app_path, c("--disabled", "--no-enabled", "--unknown")), + "disabled=TRUE enabled=FALSE unknown=TRUE" + ) + expect_output( + 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" + ) + + 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))) +}) + +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_output( + Rapp::run(app_path, c("-d", "-e")), + "disabled=TRUE enabled=FALSE" + ) +}) + +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("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))" + ), + 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, "--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))" + ), + prefix = "rapp-disabled-negative-alias-positional-" + ) + + expect_error( + Rapp::run(false_default_app, "--no-flag"), + "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("boolean options can require explicit values", { + app_path <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| arg_type: option", + "flag <- NA", + "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", "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.", + fixed = TRUE + ) + expect_error( + Rapp::run(app_path, "--no-flag"), + "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", { app_path <- local_rapp_app( c(