From a8c60d109a94bab9fb639a03d00b62b2717f492e Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Mon, 8 Jun 2026 16:42:38 -0400 Subject: [PATCH 1/7] Print help on missing required commands --- NEWS.md | 3 ++ R/app.R | 13 ++++- R/args.R | 10 ++++ README.md | 17 +++--- inst/examples/todo.R | 2 +- tests/testthat/test-subcommands.R | 89 +++++++++++++++++++++++++++++++ tests/testthat/test-todo.R | 8 +++ 7 files changed, 134 insertions(+), 8 deletions(-) diff --git a/NEWS.md b/NEWS.md index 38fcb9d..750cdbb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -13,6 +13,9 @@ ## New features +- Apps that declare a required command with `switch("")` or + `switch(command <- NULL, ...)` now print scoped help when the command + is omitted (#21). - `#| examples` annotations now add usage examples to `--help` output (#23). diff --git a/R/app.R b/R/app.R index e5a0af9..79186f2 100644 --- a/R/app.R +++ b/R/app.R @@ -81,6 +81,15 @@ is_command_switch <- function(e) { typeof(switch_expr) == "character" || is_simple_assignment_call(switch_expr) } +command_switch_help_on_missing <- function(switch_expr) { + if (is.character(switch_expr)) { + return(identical(switch_expr, "")) + } + + stopifnot(is_simple_assignment_call(switch_expr)) + is.null(switch_expr[[3L]]) +} + .simple_call_syms <- c("+", "-", "c", "character", "integer", "double", "numeric") @@ -117,6 +126,7 @@ get_app_inputs <- function(app, exprs = app$exprs, pos = integer()) { if (length(commands)) { stop("Only one app command switch() block allowed per expression level") } + switch_expr <- e[[2L]] branches <- as.list(e)[-(1:2)] if (".val_pos_in_exprs" %in% names(branches)) { stop('command name ".val_pos_in_exprs" not permitted.') @@ -136,9 +146,10 @@ get_app_inputs <- function(app, exprs = app$exprs, pos = integer()) { } ) names(commands) <- gsub("_", "-", names(commands), fixed = TRUE) - switch_expr <- e[[2L]] commands$.val_pos_in_exprs <- c(pos, i, 2L, if (is.call(switch_expr)) 3L) + attr(commands, "help_on_missing_command") <- + command_switch_help_on_missing(switch_expr) next } diff --git a/R/args.R b/R/args.R index 4e6ea20..c33b1e0 100644 --- a/R/args.R +++ b/R/args.R @@ -168,6 +168,16 @@ process_args <- function(args, app) { app$exprs[[spec$.val_pos_in_exprs]] <- val } + command_names <- setdiff(names(app_commands), ".val_pos_in_exprs") + if ( + !length(positional_args) && + length(command_names) && + isTRUE(attr(app_commands, "help_on_missing_command")) + ) { + print_app_help(app, command_path = command_path, yaml = FALSE) + return(FALSE) + } + if (length(positional_args) || length(app_args)) { # we've parsed all the command line args, # we can now match positional args diff --git a/README.md b/README.md index de55430..2ba58a6 100644 --- a/README.md +++ b/README.md @@ -77,12 +77,15 @@ arguments, and commands. The sections below cover the supported patterns. ### Help -All Rapps comes with built-in flags for help. +All Rapps come with built-in flags for help. - `--help` shows usage, description, and options for the app (and for subcommands when used after a command, e.g., `todo list --help`). - `--help-yaml` prints machine-readable metadata for the app as YAML. +If an app or command requires a subcommand, omitting the subcommand prints +the same help as `--help`. + ### Options Simple assignments of scalar (length-1) literals at the top level of the @@ -212,11 +215,13 @@ This changes the usage to `Usage: greet []` (with brackets). ### Commands Use a `switch()` statement whose first argument is either a character -scalar or an assignment (for example `switch("")` or -`switch(command <- "", ...)`) to declare commands. The corresponding -branch runs when the matching command is supplied on the command line. -Declare command specific options and positional arguments with the same -rules inside the branch. +scalar or an assignment to declare commands. Use `switch("")` or +`switch(command <- NULL, ...)` when a command is required; if no command +is supplied, Rapp prints help for the current command level. Use +`switch(command <- "", ...)` when running without a command is allowed. +The corresponding branch runs when the matching command is supplied on +the command line. Declare command specific options and positional +arguments with the same rules inside the branch. ``` r #!/usr/bin/env Rapp diff --git a/inst/examples/todo.R b/inst/examples/todo.R index ddcf4cc..2239ace 100644 --- a/inst/examples/todo.R +++ b/inst/examples/todo.R @@ -11,7 +11,7 @@ store <- ".todo.yml" switch( - command <- "", + command <- NULL, #| title: Display the todos #| description: Print the contents of the todo list. diff --git a/tests/testthat/test-subcommands.R b/tests/testthat/test-subcommands.R index 7abba89..973206d 100644 --- a/tests/testthat/test-subcommands.R +++ b/tests/testthat/test-subcommands.R @@ -16,6 +16,95 @@ test_that("simple app uses defaults without args", { expect_identical(env$global_opt, "global_opt_default") }) +test_that("missing literal command switch prints help", { + app_path <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| name: required-command-test", + "#| description: Exercise missing command help.", + "", + "switch('',", + " #| title: List entries", + " list = { cat('list called\\n') }", + ")" + ), + prefix = "rapp-required-command-" + ) + + lines <- capture.output(result <- Rapp::run(app_path, character())) + + expect_null(result) + expect_true(any(grepl( + "Usage: required-command-test ", + lines, + fixed = TRUE + ))) + expect_true(any(grepl("Commands:", lines, fixed = TRUE))) + expect_true(any(grepl("list", lines, fixed = TRUE))) +}) + +test_that("missing NULL command assignment prints help", { + app_path <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| name: null-command-test", + "#| description: Exercise missing command help.", + "", + "switch(command <- NULL,", + " #| title: List entries", + " list = { cat(command, '\\n', sep = '') }", + ")" + ), + prefix = "rapp-null-command-" + ) + + lines <- capture.output(result <- Rapp::run(app_path, character())) + + expect_null(result) + expect_true(any(grepl( + "Usage: null-command-test ", + lines, + fixed = TRUE + ))) + expect_true(any(grepl("Commands:", lines, fixed = TRUE))) + expect_true(any(grepl("list", lines, fixed = TRUE))) + + run_lines <- capture.output(env <- Rapp::run(app_path, "list")) + expect_identical(run_lines, "list") + expect_identical(env$command, "list") +}) + +test_that("missing nested command prints scoped help", { + app_path <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| name: nested-required-command-test", + "", + "switch('',", + " #| title: Parent command", + " parent = {", + " switch('',", + " #| title: Child command", + " child = { cat('child called\\n') }", + " )", + " }", + ")" + ), + prefix = "rapp-nested-required-command-" + ) + + lines <- capture.output(result <- Rapp::run(app_path, "parent")) + + expect_null(result) + expect_true(any(grepl( + "Usage: nested-required-command-test parent ", + lines, + fixed = TRUE + ))) + expect_true(any(grepl("Commands:", lines, fixed = TRUE))) + expect_true(any(grepl("child", lines, fixed = TRUE))) +}) + test_that("global option is recognised before and after a command", { env_pre <- capture_simple_env(c("--global-opt", "override", "cmd1")) env_post <- capture_simple_env(c("cmd1", "--global-opt", "late")) diff --git a/tests/testthat/test-todo.R b/tests/testthat/test-todo.R index 396d1f7..a2e009b 100644 --- a/tests/testthat/test-todo.R +++ b/tests/testthat/test-todo.R @@ -33,6 +33,14 @@ test_that("todo help output", { ) }) +test_that("todo without a command prints help", { + lines <- run_todo_app() + + expect_true(any(grepl("Todo manager", lines, fixed = TRUE))) + expect_true(any(grepl("Usage: todo [OPTIONS] ", lines, fixed = TRUE))) + expect_true(any(grepl("Commands:", lines, fixed = TRUE))) +}) + test_that("todo commands update the store", { store <- tempfile(fileext = ".yml") on.exit(unlink(store), add = TRUE) From 7a6f4927b2e07c57bb981528199cc5ec952469c6 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Mon, 8 Jun 2026 17:02:54 -0400 Subject: [PATCH 2/7] Snapshot missing command help as YAML --- tests/testthat/_snaps/subcommands.md | 85 ++++++++++++++++++++++++++++ tests/testthat/test-subcommands.R | 35 ++++++++++-- 2 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 tests/testthat/_snaps/subcommands.md diff --git a/tests/testthat/_snaps/subcommands.md b/tests/testthat/_snaps/subcommands.md new file mode 100644 index 0000000..a881f75 --- /dev/null +++ b/tests/testthat/_snaps/subcommands.md @@ -0,0 +1,85 @@ +# missing literal command switch prints help + + Code + yaml12::write_yaml(snapshot) + Output + --- + app: |- + #!/usr/bin/env Rapp + #| name: required-command-test + #| description: Exercise missing command help. + + switch('', + #| title: List entries + list = { cat('list called\n') } + ) + invocation: $ required-command-test + output: |- + Usage: required-command-test + + Exercise missing command help. + + Commands: + list List entries + + For help with a specific command, run: `required-command-test --help`. + ... + +# missing NULL command assignment prints help + + Code + yaml12::write_yaml(snapshot) + Output + --- + app: |- + #!/usr/bin/env Rapp + #| name: null-command-test + #| description: Exercise missing command help. + + switch(command <- NULL, + #| title: List entries + list = { cat(command, '\n', sep = '') } + ) + invocation: $ null-command-test + output: |- + Usage: null-command-test + + Exercise missing command help. + + Commands: + list List entries + + For help with a specific command, run: `null-command-test --help`. + ... + +# missing nested command prints scoped help + + Code + yaml12::write_yaml(snapshot) + Output + --- + app: |- + #!/usr/bin/env Rapp + #| name: nested-required-command-test + + switch('', + #| title: Parent command + parent = { + switch('', + #| title: Child command + child = { cat('child called\n') } + ) + } + ) + invocation: $ nested-required-command-test parent + output: |- + Parent command + + Usage: nested-required-command-test parent + + Commands: + child Child command + + For help with a specific command, run: `nested-required-command-test parent --help`. + ... + diff --git a/tests/testthat/test-subcommands.R b/tests/testthat/test-subcommands.R index 973206d..ee5b039 100644 --- a/tests/testthat/test-subcommands.R +++ b/tests/testthat/test-subcommands.R @@ -10,6 +10,20 @@ capture_nested_env <- function(args = character()) { capture_app_env(nested_app, args) } +snapshot_missing_command_help <- function(app_path, args, invocation) { + output <- capture.output(result <- Rapp::run(app_path, args)) + expect_null(result) + + snapshot <- list( + app = paste(readLines(app_path), collapse = "\n"), + invocation = paste0("$ ", invocation), + output = paste(output, collapse = "\n") + ) + expect_snapshot(yaml12::write_yaml(snapshot)) + + output +} + test_that("simple app uses defaults without args", { env <- capture_simple_env() expect_identical(env$cmd, "") @@ -31,9 +45,12 @@ test_that("missing literal command switch prints help", { prefix = "rapp-required-command-" ) - lines <- capture.output(result <- Rapp::run(app_path, character())) + lines <- snapshot_missing_command_help( + app_path, + character(), + "required-command-test" + ) - expect_null(result) expect_true(any(grepl( "Usage: required-command-test ", lines, @@ -58,9 +75,12 @@ test_that("missing NULL command assignment prints help", { prefix = "rapp-null-command-" ) - lines <- capture.output(result <- Rapp::run(app_path, character())) + lines <- snapshot_missing_command_help( + app_path, + character(), + "null-command-test" + ) - expect_null(result) expect_true(any(grepl( "Usage: null-command-test ", lines, @@ -93,9 +113,12 @@ test_that("missing nested command prints scoped help", { prefix = "rapp-nested-required-command-" ) - lines <- capture.output(result <- Rapp::run(app_path, "parent")) + lines <- snapshot_missing_command_help( + app_path, + "parent", + "nested-required-command-test parent" + ) - expect_null(result) expect_true(any(grepl( "Usage: nested-required-command-test parent ", lines, From 84ace8d0b702231edc6baa70f728f784f7c64fa5 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 09:09:21 -0400 Subject: [PATCH 3/7] Require command switches by default --- NEWS.md | 6 +- R/app.R | 12 +- README.md | 20 +-- inst/examples/todo.R | 2 +- tests/testthat/_snaps/subcommands.md | 172 +++++++++++++++++--------- tests/testthat/apps/kitchen-sink.R | 1 + tests/testthat/apps/nested-commands.R | 1 + tests/testthat/apps/simple-commands.R | 1 + tests/testthat/test-subcommands.R | 87 +++++++++++-- 9 files changed, 210 insertions(+), 92 deletions(-) diff --git a/NEWS.md b/NEWS.md index 750cdbb..0178b7c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,12 +10,12 @@ types instead of coercing them. Integer options no longer accept float or logical values such as `10.2` or `true`; float options still accept integer values (#18). +- Command switches are now required by default. When a command is + omitted, Rapp prints scoped help; add `#| required: false` above the + `switch()` to allow running without a command (#21). ## New features -- Apps that declare a required command with `switch("")` or - `switch(command <- NULL, ...)` now print scoped help when the command - is omitted (#21). - `#| examples` annotations now add usage examples to `--help` output (#23). diff --git a/R/app.R b/R/app.R index 79186f2..90d0c9c 100644 --- a/R/app.R +++ b/R/app.R @@ -81,13 +81,8 @@ is_command_switch <- function(e) { typeof(switch_expr) == "character" || is_simple_assignment_call(switch_expr) } -command_switch_help_on_missing <- function(switch_expr) { - if (is.character(switch_expr)) { - return(identical(switch_expr, "")) - } - - stopifnot(is_simple_assignment_call(switch_expr)) - is.null(switch_expr[[3L]]) +command_switch_help_on_missing <- function(anno) { + !isFALSE((anno %||% list())[["required"]]) } .simple_call_syms <- @@ -127,6 +122,7 @@ get_app_inputs <- function(app, exprs = app$exprs, pos = integer()) { stop("Only one app command switch() block allowed per expression level") } switch_expr <- e[[2L]] + switch_anno <- parse_expr_anno(getSrcLineNo(exprs[i]), lines, is_hashpipe) branches <- as.list(e)[-(1:2)] if (".val_pos_in_exprs" %in% names(branches)) { stop('command name ".val_pos_in_exprs" not permitted.') @@ -149,7 +145,7 @@ get_app_inputs <- function(app, exprs = app$exprs, pos = integer()) { commands$.val_pos_in_exprs <- c(pos, i, 2L, if (is.call(switch_expr)) 3L) attr(commands, "help_on_missing_command") <- - command_switch_help_on_missing(switch_expr) + command_switch_help_on_missing(switch_anno) next } diff --git a/README.md b/README.md index 2ba58a6..0390989 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,8 @@ All Rapps come with built-in flags for help. when used after a command, e.g., `todo list --help`). - `--help-yaml` prints machine-readable metadata for the app as YAML. -If an app or command requires a subcommand, omitting the subcommand prints -the same help as `--help`. +When a command is missing, Rapp automatically prints the same help as +`--help`. ### Options @@ -215,13 +215,13 @@ This changes the usage to `Usage: greet []` (with brackets). ### Commands Use a `switch()` statement whose first argument is either a character -scalar or an assignment to declare commands. Use `switch("")` or -`switch(command <- NULL, ...)` when a command is required; if no command -is supplied, Rapp prints help for the current command level. Use -`switch(command <- "", ...)` when running without a command is allowed. -The corresponding branch runs when the matching command is supplied on -the command line. Declare command specific options and positional -arguments with the same rules inside the branch. +scalar or an assignment to declare commands. Command switches are +required by default; if no command is supplied, Rapp prints help for the +current command level. To allow running without a command, add +`#| required: false` above the `switch()`. The corresponding branch runs +when the matching command is supplied on the command line. Declare +command specific options and positional arguments with the same rules +inside the branch. ``` r #!/usr/bin/env Rapp @@ -363,7 +363,7 @@ command line arguments. | Assignment of `TRUE` or `FALSE`
`foo <- TRUE` | 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` | +| Switch with string literal
`switch("", cmd1 = {}, cmd2 = {})` | Required commands
`APP --help`
`APP cmd1 --help`
`APP cmd2 --help` | ### Running interactively diff --git a/inst/examples/todo.R b/inst/examples/todo.R index 2239ace..ddcf4cc 100644 --- a/inst/examples/todo.R +++ b/inst/examples/todo.R @@ -11,7 +11,7 @@ store <- ".todo.yml" switch( - command <- NULL, + command <- "", #| title: Display the todos #| description: Print the contents of the todo list. diff --git a/tests/testthat/_snaps/subcommands.md b/tests/testthat/_snaps/subcommands.md index a881f75..af16d55 100644 --- a/tests/testthat/_snaps/subcommands.md +++ b/tests/testthat/_snaps/subcommands.md @@ -4,52 +4,72 @@ yaml12::write_yaml(snapshot) Output --- - app: |- - #!/usr/bin/env Rapp - #| name: required-command-test - #| description: Exercise missing command help. - - switch('', - #| title: List entries - list = { cat('list called\n') } - ) + app: + - "#!/usr/bin/env Rapp" + - "#| name: required-command-test" + - "#| description: Exercise missing command help." + - "" + - "switch(''," + - " #| title: List entries" + - " list = { cat('list called\\n') }" + - ) invocation: $ required-command-test - output: |- - Usage: required-command-test - - Exercise missing command help. - - Commands: - list List entries - - For help with a specific command, run: `required-command-test --help`. + output: + - "Usage: required-command-test " + - "" + - Exercise missing command help. + - "" + - "Commands:" + - " list List entries" + - "" + - "For help with a specific command, run: `required-command-test --help`." ... -# missing NULL command assignment prints help +# missing command assignment prints help by default Code yaml12::write_yaml(snapshot) Output --- - app: |- - #!/usr/bin/env Rapp - #| name: null-command-test - #| description: Exercise missing command help. - - switch(command <- NULL, - #| title: List entries - list = { cat(command, '\n', sep = '') } - ) - invocation: $ null-command-test - output: |- - Usage: null-command-test - - Exercise missing command help. - - Commands: - list List entries - - For help with a specific command, run: `null-command-test --help`. + app: + - "#!/usr/bin/env Rapp" + - "#| name: assigned-command-test" + - "#| description: Exercise missing command help." + - "" + - "switch(command <- ''," + - " #| title: List entries" + - " list = { cat(command, '\\n', sep = '') }" + - ) + invocation: $ assigned-command-test + output: + - "Usage: assigned-command-test " + - "" + - Exercise missing command help. + - "" + - "Commands:" + - " list List entries" + - "" + - "For help with a specific command, run: `assigned-command-test --help`." + ... + +# required false command switch allows missing command + + Code + yaml12::write_yaml(snapshot) + Output + --- + app: + - "#!/usr/bin/env Rapp" + - "#| name: optional-command-test" + - "" + - "#| required: false" + - "switch(command <- ''," + - " #| title: List entries" + - " list = { cat(command, '\\n', sep = '') }" + - ) + - "cat('no command\\n')" + invocation: $ optional-command-test + output: no command ... # missing nested command prints scoped help @@ -58,28 +78,60 @@ yaml12::write_yaml(snapshot) Output --- - app: |- - #!/usr/bin/env Rapp - #| name: nested-required-command-test - - switch('', - #| title: Parent command - parent = { - switch('', - #| title: Child command - child = { cat('child called\n') } - ) - } - ) + app: + - "#!/usr/bin/env Rapp" + - "#| name: nested-required-command-test" + - "" + - "switch(''," + - " #| title: Parent command" + - " parent = {" + - " switch(''," + - " #| title: Child command" + - " child = { cat('child called\\n') }" + - " )" + - " }" + - ) invocation: $ nested-required-command-test parent - output: |- - Parent command - - Usage: nested-required-command-test parent - - Commands: - child Child command - - For help with a specific command, run: `nested-required-command-test parent --help`. + output: + - Parent command + - "" + - "Usage: nested-required-command-test parent " + - "" + - "Commands:" + - " child Child command" + - "" + - "For help with a specific command, run: `nested-required-command-test parent --help`." + ... + +# optional parent command preserves required child help + + Code + yaml12::write_yaml(snapshot) + Output + --- + app: + - "#!/usr/bin/env Rapp" + - "#| name: optional-parent-required-child-test" + - "" + - "#| required: false" + - "switch(parent_cmd <- ''," + - " #| title: Parent command" + - " parent = {" + - " switch(child_cmd <- NULL," + - " #| title: Child command" + - " child = { cat('child called\\n') }" + - " )" + - " }" + - ) + invocation: $ optional-parent-required-child-test parent + output: + - Parent command + - "" + - "Usage: optional-parent-required-child-test parent " + - "" + - "Commands:" + - " child Child command" + - "" + - "For help with a specific command, run: `optional-parent-required-child-test parent --help`." ... diff --git a/tests/testthat/apps/kitchen-sink.R b/tests/testthat/apps/kitchen-sink.R index 3111404..1b9fe22 100644 --- a/tests/testthat/apps/kitchen-sink.R +++ b/tests/testthat/apps/kitchen-sink.R @@ -35,6 +35,7 @@ optional_positional <- NULL #| required: false optional_positional_default <- "foo" +#| required: false switch( mode <- "", #| title: Summary Mode diff --git a/tests/testthat/apps/nested-commands.R b/tests/testthat/apps/nested-commands.R index 686456e..59e8989 100644 --- a/tests/testthat/apps/nested-commands.R +++ b/tests/testthat/apps/nested-commands.R @@ -8,6 +8,7 @@ switch( parent_opt <- "parent-default" parent_switch <- TRUE + #| required: false switch( child_cmd <- "", child1 = { diff --git a/tests/testthat/apps/simple-commands.R b/tests/testthat/apps/simple-commands.R index 2002715..0d4a8f8 100644 --- a/tests/testthat/apps/simple-commands.R +++ b/tests/testthat/apps/simple-commands.R @@ -2,6 +2,7 @@ global_opt <- "global_opt_default" +#| required: false switch( cmd <- "", diff --git a/tests/testthat/test-subcommands.R b/tests/testthat/test-subcommands.R index ee5b039..c6700dc 100644 --- a/tests/testthat/test-subcommands.R +++ b/tests/testthat/test-subcommands.R @@ -11,17 +11,23 @@ capture_nested_env <- function(args = character()) { } snapshot_missing_command_help <- function(app_path, args, invocation) { + run <- snapshot_command_run(app_path, args, invocation) + expect_null(run$result) + + run$output +} + +snapshot_command_run <- function(app_path, args, invocation) { output <- capture.output(result <- Rapp::run(app_path, args)) - expect_null(result) snapshot <- list( - app = paste(readLines(app_path), collapse = "\n"), + app = readLines(app_path), invocation = paste0("$ ", invocation), - output = paste(output, collapse = "\n") + output = output ) expect_snapshot(yaml12::write_yaml(snapshot)) - output + list(output = output, result = result) } test_that("simple app uses defaults without args", { @@ -60,29 +66,29 @@ test_that("missing literal command switch prints help", { expect_true(any(grepl("list", lines, fixed = TRUE))) }) -test_that("missing NULL command assignment prints help", { +test_that("missing command assignment prints help by default", { app_path <- local_rapp_app( c( "#!/usr/bin/env Rapp", - "#| name: null-command-test", + "#| name: assigned-command-test", "#| description: Exercise missing command help.", "", - "switch(command <- NULL,", + "switch(command <- '',", " #| title: List entries", " list = { cat(command, '\\n', sep = '') }", ")" ), - prefix = "rapp-null-command-" + prefix = "rapp-assigned-command-" ) lines <- snapshot_missing_command_help( app_path, character(), - "null-command-test" + "assigned-command-test" ) expect_true(any(grepl( - "Usage: null-command-test ", + "Usage: assigned-command-test ", lines, fixed = TRUE ))) @@ -94,6 +100,32 @@ test_that("missing NULL command assignment prints help", { expect_identical(env$command, "list") }) +test_that("required false command switch allows missing command", { + app_path <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| name: optional-command-test", + "", + "#| required: false", + "switch(command <- '',", + " #| title: List entries", + " list = { cat(command, '\\n', sep = '') }", + ")", + "cat('no command\\n')" + ), + prefix = "rapp-optional-command-" + ) + + run <- snapshot_command_run( + app_path, + character(), + "optional-command-test" + ) + + expect_identical(run$output, "no command") + expect_identical(run$result$command, "") +}) + test_that("missing nested command prints scoped help", { app_path <- local_rapp_app( c( @@ -128,6 +160,41 @@ test_that("missing nested command prints scoped help", { expect_true(any(grepl("child", lines, fixed = TRUE))) }) +test_that("optional parent command preserves required child help", { + app_path <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| name: optional-parent-required-child-test", + "", + "#| required: false", + "switch(parent_cmd <- '',", + " #| title: Parent command", + " parent = {", + " switch(child_cmd <- NULL,", + " #| title: Child command", + " child = { cat('child called\\n') }", + " )", + " }", + ")" + ), + prefix = "rapp-optional-parent-required-child-" + ) + + lines <- snapshot_missing_command_help( + app_path, + "parent", + "optional-parent-required-child-test parent" + ) + + expect_true(any(grepl( + "Usage: optional-parent-required-child-test parent ", + lines, + fixed = TRUE + ))) + expect_true(any(grepl("Commands:", lines, fixed = TRUE))) + expect_true(any(grepl("child", lines, fixed = TRUE))) +}) + test_that("global option is recognised before and after a command", { env_pre <- capture_simple_env(c("--global-opt", "override", "cmd1")) env_post <- capture_simple_env(c("cmd1", "--global-opt", "late")) From 37307d2956baf7d12a0f3f23467bc4f10a5bdcbd Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 10:08:54 -0400 Subject: [PATCH 4/7] Enforce missing command help with positionals --- R/args.R | 3 +- tests/testthat/_snaps/subcommands.md | 111 +++++---------------------- tests/testthat/test-subcommands.R | 37 ++++++++- 3 files changed, 56 insertions(+), 95 deletions(-) diff --git a/R/args.R b/R/args.R index c33b1e0..304a7da 100644 --- a/R/args.R +++ b/R/args.R @@ -170,8 +170,7 @@ process_args <- function(args, app) { command_names <- setdiff(names(app_commands), ".val_pos_in_exprs") if ( - !length(positional_args) && - length(command_names) && + length(command_names) && isTRUE(attr(app_commands, "help_on_missing_command")) ) { print_app_help(app, command_path = command_path, yaml = FALSE) diff --git a/tests/testthat/_snaps/subcommands.md b/tests/testthat/_snaps/subcommands.md index af16d55..feb6bc5 100644 --- a/tests/testthat/_snaps/subcommands.md +++ b/tests/testthat/_snaps/subcommands.md @@ -4,25 +4,9 @@ yaml12::write_yaml(snapshot) Output --- - app: - - "#!/usr/bin/env Rapp" - - "#| name: required-command-test" - - "#| description: Exercise missing command help." - - "" - - "switch(''," - - " #| title: List entries" - - " list = { cat('list called\\n') }" - - ) + app: "#!/usr/bin/env Rapp\\n#| name: required-command-test\\n#| description: Exercise missing command help.\\n\\nswitch('',\\n #| title: List entries\\n list = { cat('list called\\n') }\\n)" invocation: $ required-command-test - output: - - "Usage: required-command-test " - - "" - - Exercise missing command help. - - "" - - "Commands:" - - " list List entries" - - "" - - "For help with a specific command, run: `required-command-test --help`." + output: "Usage: required-command-test \\n\\nExercise missing command help.\\n\\nCommands:\\n list List entries\\n\\nFor help with a specific command, run: `required-command-test --help`." ... # missing command assignment prints help by default @@ -31,25 +15,9 @@ yaml12::write_yaml(snapshot) Output --- - app: - - "#!/usr/bin/env Rapp" - - "#| name: assigned-command-test" - - "#| description: Exercise missing command help." - - "" - - "switch(command <- ''," - - " #| title: List entries" - - " list = { cat(command, '\\n', sep = '') }" - - ) + app: "#!/usr/bin/env Rapp\\n#| name: assigned-command-test\\n#| description: Exercise missing command help.\\n\\nswitch(command <- '',\\n #| title: List entries\\n list = { cat(command, '\\n', sep = '') }\\n)" invocation: $ assigned-command-test - output: - - "Usage: assigned-command-test " - - "" - - Exercise missing command help. - - "" - - "Commands:" - - " list List entries" - - "" - - "For help with a specific command, run: `assigned-command-test --help`." + output: "Usage: assigned-command-test \\n\\nExercise missing command help.\\n\\nCommands:\\n list List entries\\n\\nFor help with a specific command, run: `assigned-command-test --help`." ... # required false command switch allows missing command @@ -58,49 +26,31 @@ yaml12::write_yaml(snapshot) Output --- - app: - - "#!/usr/bin/env Rapp" - - "#| name: optional-command-test" - - "" - - "#| required: false" - - "switch(command <- ''," - - " #| title: List entries" - - " list = { cat(command, '\\n', sep = '') }" - - ) - - "cat('no command\\n')" + app: "#!/usr/bin/env Rapp\\n#| name: optional-command-test\\n\\n#| required: false\\nswitch(command <- '',\\n #| title: List entries\\n list = { cat(command, '\\n', sep = '') }\\n)\\ncat('no command\\n')" invocation: $ optional-command-test output: no command ... +# missing command prints help before matching positionals + + Code + yaml12::write_yaml(snapshot) + Output + --- + app: "#!/usr/bin/env Rapp\\n#| name: command-with-positional-test\\n\\n#| description: Input path.\\ninput <- NULL\\n\\nswitch('',\\n #| title: Run command\\n run = { cat('run ', input, '\\n', sep = '') }\\n)\\ncat('no command\\n')" + invocation: $ command-with-positional-test data.csv + output: "Usage: command-with-positional-test \\n\\ncommand-with-positional-test\\n\\nCommands:\\n run Run command\\n\\nArguments:\\n Input path.\\n\\nFor help with a specific command, run: `command-with-positional-test --help`." + ... + # missing nested command prints scoped help Code yaml12::write_yaml(snapshot) Output --- - app: - - "#!/usr/bin/env Rapp" - - "#| name: nested-required-command-test" - - "" - - "switch(''," - - " #| title: Parent command" - - " parent = {" - - " switch(''," - - " #| title: Child command" - - " child = { cat('child called\\n') }" - - " )" - - " }" - - ) + app: "#!/usr/bin/env Rapp\\n#| name: nested-required-command-test\\n\\nswitch('',\\n #| title: Parent command\\n parent = {\\n switch('',\\n #| title: Child command\\n child = { cat('child called\\n') }\\n )\\n }\\n)" invocation: $ nested-required-command-test parent - output: - - Parent command - - "" - - "Usage: nested-required-command-test parent " - - "" - - "Commands:" - - " child Child command" - - "" - - "For help with a specific command, run: `nested-required-command-test parent --help`." + output: "Parent command\\n\\nUsage: nested-required-command-test parent \\n\\nCommands:\\n child Child command\\n\\nFor help with a specific command, run: `nested-required-command-test parent --help`." ... # optional parent command preserves required child help @@ -109,29 +59,8 @@ yaml12::write_yaml(snapshot) Output --- - app: - - "#!/usr/bin/env Rapp" - - "#| name: optional-parent-required-child-test" - - "" - - "#| required: false" - - "switch(parent_cmd <- ''," - - " #| title: Parent command" - - " parent = {" - - " switch(child_cmd <- NULL," - - " #| title: Child command" - - " child = { cat('child called\\n') }" - - " )" - - " }" - - ) + app: "#!/usr/bin/env Rapp\\n#| name: optional-parent-required-child-test\\n\\n#| required: false\\nswitch(parent_cmd <- '',\\n #| title: Parent command\\n parent = {\\n switch(child_cmd <- NULL,\\n #| title: Child command\\n child = { cat('child called\\n') }\\n )\\n }\\n)" invocation: $ optional-parent-required-child-test parent - output: - - Parent command - - "" - - "Usage: optional-parent-required-child-test parent " - - "" - - "Commands:" - - " child Child command" - - "" - - "For help with a specific command, run: `optional-parent-required-child-test parent --help`." + output: "Parent command\\n\\nUsage: optional-parent-required-child-test parent \\n\\nCommands:\\n child Child command\\n\\nFor help with a specific command, run: `optional-parent-required-child-test parent --help`." ... diff --git a/tests/testthat/test-subcommands.R b/tests/testthat/test-subcommands.R index c6700dc..34a2364 100644 --- a/tests/testthat/test-subcommands.R +++ b/tests/testthat/test-subcommands.R @@ -21,9 +21,9 @@ snapshot_command_run <- function(app_path, args, invocation) { output <- capture.output(result <- Rapp::run(app_path, args)) snapshot <- list( - app = readLines(app_path), + app = paste(readLines(app_path), collapse = "\\n"), invocation = paste0("$ ", invocation), - output = output + output = paste(output, collapse = "\\n") ) expect_snapshot(yaml12::write_yaml(snapshot)) @@ -126,6 +126,39 @@ test_that("required false command switch allows missing command", { expect_identical(run$result$command, "") }) +test_that("missing command prints help before matching positionals", { + app_path <- local_rapp_app( + c( + "#!/usr/bin/env Rapp", + "#| name: command-with-positional-test", + "", + "#| description: Input path.", + "input <- NULL", + "", + "switch('',", + " #| title: Run command", + " run = { cat('run ', input, '\\n', sep = '') }", + ")", + "cat('no command\\n')" + ), + prefix = "rapp-command-with-positional-" + ) + + lines <- snapshot_missing_command_help( + app_path, + "data.csv", + "command-with-positional-test data.csv" + ) + + expect_true(any(grepl( + "Usage: command-with-positional-test ", + lines, + fixed = TRUE + ))) + expect_true(any(grepl("Commands:", lines, fixed = TRUE))) + expect_true(any(grepl("run", lines, fixed = TRUE))) +}) + test_that("missing nested command prints scoped help", { app_path <- local_rapp_app( c( From 5858e08e6a9ff5aeda404f39251d2af53e1d6382 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 10:23:03 -0400 Subject: [PATCH 5/7] Fix unknown command typo handling --- R/args.R | 21 +++++++++++++++------ tests/testthat/test-todo.R | 7 +++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/R/args.R b/R/args.R index 304a7da..9e0b4be 100644 --- a/R/args.R +++ b/R/args.R @@ -169,13 +169,9 @@ process_args <- function(args, app) { } command_names <- setdiff(names(app_commands), ".val_pos_in_exprs") - if ( + missing_required_command <- length(command_names) && isTRUE(attr(app_commands, "help_on_missing_command")) - ) { - print_app_help(app, command_path = command_path, yaml = FALSE) - return(FALSE) - } if (length(positional_args) || length(app_args)) { # we've parsed all the command line args, @@ -213,12 +209,20 @@ process_args <- function(args, app) { } if (length(specs) < length(positional_args)) { + unrecognized_args <- positional_args[ + seq.int(length(specs) + 1L, length(positional_args)) + ] stop( "Arguments not recognized: ", - paste0(positional_args[-seq_along(specs)], collapse = " ") + paste0(unrecognized_args, collapse = " ") ) } + if (missing_required_command) { + print_app_help(app, command_path = command_path, yaml = FALSE) + return(FALSE) + } + if (length(specs) != length(positional_args)) { for (i in rev(seq_along(specs))) { if (isFALSE(specs[[i]][["required"]])) { @@ -248,6 +252,11 @@ process_args <- function(args, app) { } } + if (missing_required_command) { + print_app_help(app, command_path = command_path, yaml = FALSE) + return(FALSE) + } + TRUE } diff --git a/tests/testthat/test-todo.R b/tests/testthat/test-todo.R index a2e009b..63590e0 100644 --- a/tests/testthat/test-todo.R +++ b/tests/testthat/test-todo.R @@ -41,6 +41,13 @@ test_that("todo without a command prints help", { expect_true(any(grepl("Commands:", lines, fixed = TRUE))) }) +test_that("todo rejects unknown command tokens", { + expect_error( + run_todo_app("lisst", capture = FALSE), + "Arguments not recognized: lisst" + ) +}) + test_that("todo commands update the store", { store <- tempfile(fileext = ".yml") on.exit(unlink(store), add = TRUE) From a0acdc53d358a8417d743ff97649f9ae764071c7 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 10:36:16 -0400 Subject: [PATCH 6/7] Render optional commands as optional in help --- R/help.R | 12 ++++++++++-- tests/testthat/test-subcommands.R | 8 ++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/R/help.R b/R/help.R index 01f88bf..050f7f5 100644 --- a/R/help.R +++ b/R/help.R @@ -453,8 +453,16 @@ print_app_help <- function(app, yaml = TRUE, command_path = character()) { if (any_opts) { usage_components <- c(usage_components, "[OPTIONS]") } - if (length(setdiff(names(current_commands), ".val_pos_in_exprs"))) { - usage_components <- c(usage_components, "") + command_names <- setdiff(names(current_commands), ".val_pos_in_exprs") + if (length(command_names)) { + command_usage <- if ( + isFALSE(attr(current_commands, "help_on_missing_command")) + ) { + "[]" + } else { + "" + } + usage_components <- c(usage_components, command_usage) } usage_components <- c(usage_components, build_usage_args(current_args)) usage_line <- paste("Usage:", paste(usage_components, collapse = " ")) diff --git a/tests/testthat/test-subcommands.R b/tests/testthat/test-subcommands.R index 34a2364..ce107e1 100644 --- a/tests/testthat/test-subcommands.R +++ b/tests/testthat/test-subcommands.R @@ -124,6 +124,14 @@ test_that("required false command switch allows missing command", { expect_identical(run$output, "no command") expect_identical(run$result$command, "") + + help_output <- capture.output(help_result <- Rapp::run(app_path, "--help")) + expect_null(help_result) + expect_true(any(grepl( + "Usage: optional-command-test []", + help_output, + fixed = TRUE + ))) }) test_that("missing command prints help before matching positionals", { From cfec596dcaf5d38f0ba2d1817eed316582f56c7a Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 10:59:29 -0400 Subject: [PATCH 7/7] Group subcommand snapshots by app and invocation --- tests/testthat/_snaps/subcommands.md | 172 ++++++++++++++++++++++++--- tests/testthat/test-subcommands.R | 119 ++++++++++++------ 2 files changed, 237 insertions(+), 54 deletions(-) diff --git a/tests/testthat/_snaps/subcommands.md b/tests/testthat/_snaps/subcommands.md index feb6bc5..f9df528 100644 --- a/tests/testthat/_snaps/subcommands.md +++ b/tests/testthat/_snaps/subcommands.md @@ -4,9 +4,28 @@ yaml12::write_yaml(snapshot) Output --- - app: "#!/usr/bin/env Rapp\\n#| name: required-command-test\\n#| description: Exercise missing command help.\\n\\nswitch('',\\n #| title: List entries\\n list = { cat('list called\\n') }\\n)" - invocation: $ required-command-test - output: "Usage: required-command-test \\n\\nExercise missing command help.\\n\\nCommands:\\n list List entries\\n\\nFor help with a specific command, run: `required-command-test --help`." + app: |- + #!/usr/bin/env Rapp + #| name: required-command-test + #| description: Exercise missing command help. + + switch('', + #| title: List entries + list = { cat('list called\n') } + ) + invocation: + - usage: $ required-command-test + output: |- + Usage: required-command-test + + Exercise missing command help. + + Commands: + list List entries + + For help with a specific command, run: `required-command-test --help`. + - usage: $ required-command-test list + output: list called ... # missing command assignment prints help by default @@ -15,9 +34,28 @@ yaml12::write_yaml(snapshot) Output --- - app: "#!/usr/bin/env Rapp\\n#| name: assigned-command-test\\n#| description: Exercise missing command help.\\n\\nswitch(command <- '',\\n #| title: List entries\\n list = { cat(command, '\\n', sep = '') }\\n)" - invocation: $ assigned-command-test - output: "Usage: assigned-command-test \\n\\nExercise missing command help.\\n\\nCommands:\\n list List entries\\n\\nFor help with a specific command, run: `assigned-command-test --help`." + app: |- + #!/usr/bin/env Rapp + #| name: assigned-command-test + #| description: Exercise missing command help. + + switch(command <- '', + #| title: List entries + list = { cat(command, '\n', sep = '') } + ) + invocation: + - usage: $ assigned-command-test + output: |- + Usage: assigned-command-test + + Exercise missing command help. + + Commands: + list List entries + + For help with a specific command, run: `assigned-command-test --help`. + - usage: $ assigned-command-test list + output: list ... # required false command switch allows missing command @@ -26,9 +64,33 @@ yaml12::write_yaml(snapshot) Output --- - app: "#!/usr/bin/env Rapp\\n#| name: optional-command-test\\n\\n#| required: false\\nswitch(command <- '',\\n #| title: List entries\\n list = { cat(command, '\\n', sep = '') }\\n)\\ncat('no command\\n')" - invocation: $ optional-command-test - output: no command + app: |- + #!/usr/bin/env Rapp + #| name: optional-command-test + + #| required: false + switch(command <- '', + #| title: List entries + list = { cat(command, '\n', sep = '') } + ) + cat('no command\n') + invocation: + - usage: $ optional-command-test + output: no command + - usage: $ optional-command-test list + output: |- + list + no command + - usage: $ optional-command-test --help + output: |- + Usage: optional-command-test [] + + optional-command-test + + Commands: + list List entries + + For help with a specific command, run: `optional-command-test --help`. ... # missing command prints help before matching positionals @@ -37,9 +99,36 @@ yaml12::write_yaml(snapshot) Output --- - app: "#!/usr/bin/env Rapp\\n#| name: command-with-positional-test\\n\\n#| description: Input path.\\ninput <- NULL\\n\\nswitch('',\\n #| title: Run command\\n run = { cat('run ', input, '\\n', sep = '') }\\n)\\ncat('no command\\n')" - invocation: $ command-with-positional-test data.csv - output: "Usage: command-with-positional-test \\n\\ncommand-with-positional-test\\n\\nCommands:\\n run Run command\\n\\nArguments:\\n Input path.\\n\\nFor help with a specific command, run: `command-with-positional-test --help`." + app: |- + #!/usr/bin/env Rapp + #| name: command-with-positional-test + + #| description: Input path. + input <- NULL + + switch('', + #| title: Run command + run = { cat('run ', input, '\n', sep = '') } + ) + cat('no command\n') + invocation: + - usage: $ command-with-positional-test data.csv + output: |- + Usage: command-with-positional-test + + command-with-positional-test + + Commands: + run Run command + + Arguments: + Input path. + + For help with a specific command, run: `command-with-positional-test --help`. + - usage: $ command-with-positional-test run data.csv + output: |- + run data.csv + no command ... # missing nested command prints scoped help @@ -48,9 +137,32 @@ yaml12::write_yaml(snapshot) Output --- - app: "#!/usr/bin/env Rapp\\n#| name: nested-required-command-test\\n\\nswitch('',\\n #| title: Parent command\\n parent = {\\n switch('',\\n #| title: Child command\\n child = { cat('child called\\n') }\\n )\\n }\\n)" - invocation: $ nested-required-command-test parent - output: "Parent command\\n\\nUsage: nested-required-command-test parent \\n\\nCommands:\\n child Child command\\n\\nFor help with a specific command, run: `nested-required-command-test parent --help`." + app: |- + #!/usr/bin/env Rapp + #| name: nested-required-command-test + + switch('', + #| title: Parent command + parent = { + switch('', + #| title: Child command + child = { cat('child called\n') } + ) + } + ) + invocation: + - usage: $ nested-required-command-test parent + output: |- + Parent command + + Usage: nested-required-command-test parent + + Commands: + child Child command + + For help with a specific command, run: `nested-required-command-test parent --help`. + - usage: $ nested-required-command-test parent child + output: child called ... # optional parent command preserves required child help @@ -59,8 +171,32 @@ yaml12::write_yaml(snapshot) Output --- - app: "#!/usr/bin/env Rapp\\n#| name: optional-parent-required-child-test\\n\\n#| required: false\\nswitch(parent_cmd <- '',\\n #| title: Parent command\\n parent = {\\n switch(child_cmd <- NULL,\\n #| title: Child command\\n child = { cat('child called\\n') }\\n )\\n }\\n)" - invocation: $ optional-parent-required-child-test parent - output: "Parent command\\n\\nUsage: optional-parent-required-child-test parent \\n\\nCommands:\\n child Child command\\n\\nFor help with a specific command, run: `optional-parent-required-child-test parent --help`." + app: |- + #!/usr/bin/env Rapp + #| name: optional-parent-required-child-test + + #| required: false + switch(parent_cmd <- '', + #| title: Parent command + parent = { + switch(child_cmd <- NULL, + #| title: Child command + child = { cat('child called\n') } + ) + } + ) + invocation: + - usage: $ optional-parent-required-child-test parent + output: |- + Parent command + + Usage: optional-parent-required-child-test parent + + Commands: + child Child command + + For help with a specific command, run: `optional-parent-required-child-test parent --help`. + - usage: $ optional-parent-required-child-test parent child + output: child called ... diff --git a/tests/testthat/test-subcommands.R b/tests/testthat/test-subcommands.R index ce107e1..3f1f0cd 100644 --- a/tests/testthat/test-subcommands.R +++ b/tests/testthat/test-subcommands.R @@ -10,24 +10,39 @@ capture_nested_env <- function(args = character()) { capture_app_env(nested_app, args) } -snapshot_missing_command_help <- function(app_path, args, invocation) { - run <- snapshot_command_run(app_path, args, invocation) - expect_null(run$result) +command_invocation <- function(usage, args = character()) { + stopifnot(is.character(usage), length(usage) == 1) + stopifnot(is.character(args)) - run$output + list(usage = usage, args = args) } -snapshot_command_run <- function(app_path, args, invocation) { - output <- capture.output(result <- Rapp::run(app_path, args)) +snapshot_command_runs <- function(app_path, ...) { + invocations <- list(...) + stopifnot(length(invocations) > 0) + + runs <- lapply(invocations, function(invocation) { + output <- capture.output(result <- Rapp::run(app_path, invocation$args)) + + list( + usage = paste0("$ ", invocation$usage), + output = output, + result = result + ) + }) snapshot <- list( - app = paste(readLines(app_path), collapse = "\\n"), - invocation = paste0("$ ", invocation), - output = paste(output, collapse = "\\n") + app = paste(readLines(app_path), collapse = "\n"), + invocation = lapply(runs, function(run) { + list( + usage = run$usage, + output = paste(run$output, collapse = "\n") + ) + }) ) expect_snapshot(yaml12::write_yaml(snapshot)) - list(output = output, result = result) + runs } test_that("simple app uses defaults without args", { @@ -51,11 +66,15 @@ test_that("missing literal command switch prints help", { prefix = "rapp-required-command-" ) - lines <- snapshot_missing_command_help( + runs <- snapshot_command_runs( app_path, - character(), - "required-command-test" + command_invocation("required-command-test"), + command_invocation("required-command-test list", "list") ) + lines <- runs[[1]]$output + + expect_null(runs[[1]]$result) + expect_identical(runs[[2]]$output, "list called") expect_true(any(grepl( "Usage: required-command-test ", @@ -81,11 +100,16 @@ test_that("missing command assignment prints help by default", { prefix = "rapp-assigned-command-" ) - lines <- snapshot_missing_command_help( + runs <- snapshot_command_runs( app_path, - character(), - "assigned-command-test" + command_invocation("assigned-command-test"), + command_invocation("assigned-command-test list", "list") ) + lines <- runs[[1]]$output + + expect_null(runs[[1]]$result) + expect_identical(runs[[2]]$output, "list") + expect_identical(runs[[2]]$result$command, "list") expect_true(any(grepl( "Usage: assigned-command-test ", @@ -95,9 +119,6 @@ test_that("missing command assignment prints help by default", { expect_true(any(grepl("Commands:", lines, fixed = TRUE))) expect_true(any(grepl("list", lines, fixed = TRUE))) - run_lines <- capture.output(env <- Rapp::run(app_path, "list")) - expect_identical(run_lines, "list") - expect_identical(env$command, "list") }) test_that("required false command switch allows missing command", { @@ -116,20 +137,22 @@ test_that("required false command switch allows missing command", { prefix = "rapp-optional-command-" ) - run <- snapshot_command_run( + runs <- snapshot_command_runs( app_path, - character(), - "optional-command-test" + command_invocation("optional-command-test"), + command_invocation("optional-command-test list", "list"), + command_invocation("optional-command-test --help", "--help") ) - expect_identical(run$output, "no command") - expect_identical(run$result$command, "") + expect_identical(runs[[1]]$output, "no command") + expect_identical(runs[[1]]$result$command, "") + expect_identical(runs[[2]]$output, c("list", "no command")) + expect_identical(runs[[2]]$result$command, "list") - help_output <- capture.output(help_result <- Rapp::run(app_path, "--help")) - expect_null(help_result) + expect_null(runs[[3]]$result) expect_true(any(grepl( "Usage: optional-command-test []", - help_output, + runs[[3]]$output, fixed = TRUE ))) }) @@ -152,11 +175,18 @@ test_that("missing command prints help before matching positionals", { prefix = "rapp-command-with-positional-" ) - lines <- snapshot_missing_command_help( + runs <- snapshot_command_runs( app_path, - "data.csv", - "command-with-positional-test data.csv" + command_invocation("command-with-positional-test data.csv", "data.csv"), + command_invocation( + "command-with-positional-test run data.csv", + c("run", "data.csv") + ) ) + lines <- runs[[1]]$output + + expect_null(runs[[1]]$result) + expect_identical(runs[[2]]$output, c("run data.csv", "no command")) expect_true(any(grepl( "Usage: command-with-positional-test ", @@ -186,11 +216,18 @@ test_that("missing nested command prints scoped help", { prefix = "rapp-nested-required-command-" ) - lines <- snapshot_missing_command_help( + runs <- snapshot_command_runs( app_path, - "parent", - "nested-required-command-test parent" + command_invocation("nested-required-command-test parent", "parent"), + command_invocation( + "nested-required-command-test parent child", + c("parent", "child") + ) ) + lines <- runs[[1]]$output + + expect_null(runs[[1]]$result) + expect_identical(runs[[2]]$output, "child called") expect_true(any(grepl( "Usage: nested-required-command-test parent ", @@ -221,11 +258,21 @@ test_that("optional parent command preserves required child help", { prefix = "rapp-optional-parent-required-child-" ) - lines <- snapshot_missing_command_help( + runs <- snapshot_command_runs( app_path, - "parent", - "optional-parent-required-child-test parent" + command_invocation( + "optional-parent-required-child-test parent", + "parent" + ), + command_invocation( + "optional-parent-required-child-test parent child", + c("parent", "child") + ) ) + lines <- runs[[1]]$output + + expect_null(runs[[1]]$result) + expect_identical(runs[[2]]$output, "child called") expect_true(any(grepl( "Usage: optional-parent-required-child-test parent ",