Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
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

Expand Down
9 changes: 8 additions & 1 deletion R/app.R
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ is_command_switch <- function(e) {
typeof(switch_expr) == "character" || is_simple_assignment_call(switch_expr)
}

command_switch_help_on_missing <- function(anno) {
!isFALSE((anno %||% list())[["required"]])
}

.simple_call_syms <-
c("+", "-", "c", "character", "integer", "double", "numeric")

Expand Down Expand Up @@ -117,6 +121,8 @@ 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]]
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.')
Expand All @@ -136,9 +142,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_anno)

next
}
Expand Down
20 changes: 19 additions & 1 deletion R/args.R
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ process_args <- function(args, app) {
app$exprs[[spec$.val_pos_in_exprs]] <- val
}

command_names <- setdiff(names(app_commands), ".val_pos_in_exprs")
missing_required_command <-
length(command_names) &&
isTRUE(attr(app_commands, "help_on_missing_command"))
Comment thread
t-kalinowski marked this conversation as resolved.

if (length(positional_args) || length(app_args)) {
# we've parsed all the command line args,
# we can now match positional args
Expand Down Expand Up @@ -204,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"]])) {
Expand Down Expand Up @@ -239,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
}

Expand Down
12 changes: 10 additions & 2 deletions R/help.R
Original file line number Diff line number Diff line change
Expand Up @@ -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>")
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"))
) {
"[<COMMAND>]"
} else {
"<COMMAND>"
}
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 = " "))
Expand Down
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

When a command is missing, Rapp automatically prints the same help as
`--help`.

### Options

Simple assignments of scalar (length-1) literals at the top level of the
Expand Down Expand Up @@ -212,11 +215,13 @@ This changes the usage to `Usage: greet [<NAME>]` (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. 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
Expand Down Expand Up @@ -358,7 +363,7 @@ command line arguments.
| Assignment of `TRUE` or `FALSE`<br>`foo <- TRUE` | Boolean switch<br>`APP --foo` or `APP --no-foo` |
| Assignment of `c()` or `list()`<br>`foo <- c()` | Repeatable option<br>`APP --foo val1 --foo val2` |
| Assignment of `NULL` to name with `...`<br>`args... <- NULL` | Positional Arg Collector<br>`APP foo bar baz` |
| Switch with string literal<br>`switch("", cmd1 = {}, cmd2 = {})` | Commands<br>`APP cmd1 --help`<br>`APP cmd2 --help` |
| Switch with string literal<br>`switch("", cmd1 = {}, cmd2 = {})` | Required commands<br>`APP --help`<br>`APP cmd1 --help`<br>`APP cmd2 --help` |

### Running interactively

Expand Down
202 changes: 202 additions & 0 deletions tests/testthat/_snaps/subcommands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# 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:
- usage: $ required-command-test
output: |-
Usage: required-command-test <COMMAND>

Exercise missing command help.

Commands:
list List entries

For help with a specific command, run: `required-command-test <command> --help`.
- usage: $ required-command-test list
output: list called
...

# missing command assignment prints help by default

Code
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 = '') }
)
invocation:
- usage: $ assigned-command-test
output: |-
Usage: assigned-command-test <COMMAND>

Exercise missing command help.

Commands:
list List entries

For help with a specific command, run: `assigned-command-test <command> --help`.
- usage: $ assigned-command-test list
output: list
...

# 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:
- 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 [<COMMAND>]

optional-command-test

Commands:
list List entries

For help with a specific command, run: `optional-command-test <command> --help`.
...

# missing command prints help before matching positionals

Code
yaml12::write_yaml(snapshot)
Output
---
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> <INPUT>

command-with-positional-test

Commands:
run Run command

Arguments:
<INPUT> Input path.

For help with a specific command, run: `command-with-positional-test <command> --help`.
- usage: $ command-with-positional-test run data.csv
output: |-
run data.csv
no command
...

# 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:
- usage: $ nested-required-command-test parent
output: |-
Parent command

Usage: nested-required-command-test parent <COMMAND>

Commands:
child Child command

For help with a specific command, run: `nested-required-command-test parent <command> --help`.
- usage: $ nested-required-command-test parent child
output: child called
...

# 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:
- usage: $ optional-parent-required-child-test parent
output: |-
Parent command

Usage: optional-parent-required-child-test parent <COMMAND>

Commands:
child Child command

For help with a specific command, run: `optional-parent-required-child-test parent <command> --help`.
- usage: $ optional-parent-required-child-test parent child
output: child called
...

1 change: 1 addition & 0 deletions tests/testthat/apps/kitchen-sink.R
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ optional_positional <- NULL
#| required: false
optional_positional_default <- "foo"

#| required: false
switch(
mode <- "",
#| title: Summary Mode
Expand Down
1 change: 1 addition & 0 deletions tests/testthat/apps/nested-commands.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ switch(
parent_opt <- "parent-default"
parent_switch <- TRUE

#| required: false
switch(
child_cmd <- "",
child1 = {
Expand Down
1 change: 1 addition & 0 deletions tests/testthat/apps/simple-commands.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

global_opt <- "global_opt_default"

#| required: false
switch(
cmd <- "",

Expand Down
Loading
Loading