├── .Rbuildignore ├── .gitignore ├── .travis.yml ├── DESCRIPTION ├── LICENSE ├── NAMESPACE ├── R ├── ask.R ├── style_fancy.R ├── style_plain.R └── utils.R ├── README.md ├── appveyor.yml ├── ask.Rproj ├── inst ├── README.md ├── ask-checkbox.png ├── ask-choose.png ├── ask-confirm.png ├── ask-input.png └── ask-pizza.png ├── man ├── ask.Rd ├── ask_.Rd └── questions.Rd └── tests ├── testthat.R └── testthat └── test-utils.R /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^ask.Rproj$ 2 | ^tags$ 3 | ^.travis\.yml$ 4 | ^appveyor\.yml$ 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | /tags 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | ## Sample .travis.yml file for use with metacran/r-builder 2 | ## See https://github.com/metacran/r-builder for details. 3 | 4 | language: c 5 | sudo: required 6 | 7 | before_install: 8 | - curl -OL https://raw.githubusercontent.com/metacran/r-builder/master/pkg-build.sh 9 | - chmod 755 pkg-build.sh 10 | - ./pkg-build.sh bootstrap 11 | 12 | install: 13 | - ./pkg-build.sh install_github gaborcsardi/readline 14 | - ./pkg-build.sh install_github gaborcsardi/clisymbols 15 | - ./pkg-build.sh install_deps 16 | 17 | script: 18 | - ./pkg-build.sh run_tests 19 | 20 | after_failure: 21 | - ./pkg-build.sh dump_logs 22 | 23 | notifications: 24 | email: 25 | on_success: change 26 | on_failure: change 27 | 28 | env: 29 | matrix: 30 | - RVERSION=oldrel 31 | - RVERSION=release 32 | - RVERSION=devel 33 | 34 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: ask 2 | Title: Friendly Command Line Interface Tools 3 | Version: 1.0.0 4 | Author: Gabor Csardi [aut, cre] 5 | Maintainer: Gabor Csardi 6 | Description: Ask questions to the user through the CLI and validate the answer. 7 | Several types of input are supported: yes-no question, single choice, 8 | multiple choice, etc. 9 | License: MIT + file LICENSE 10 | LazyData: true 11 | URL: https://github.com/gaborcsardi/ask 12 | BugReports: https://github.com/gaborcsardi/ask/issues 13 | Imports: 14 | lazyeval, 15 | keypress, 16 | readline, 17 | crayon (>= 1.3.0), 18 | clisymbols, 19 | stats 20 | Collate: 21 | 'ask.R' 22 | 'style_plain.R' 23 | 'style_fancy.R' 24 | 'utils.R' 25 | RoxygenNote: 5.0.1.9000 26 | Suggests: 27 | testthat 28 | Remotes: 29 | gaborcsardi/readline 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2015 2 | COPYRIGHT HOLDER: Gabor Csardi 3 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(ask) 4 | export(ask_) 5 | export(questions) 6 | importFrom(clisymbols,symbol) 7 | importFrom(crayon,blue) 8 | importFrom(crayon,bold) 9 | importFrom(crayon,combine_styles) 10 | importFrom(crayon,finish) 11 | importFrom(crayon,green) 12 | importFrom(crayon,magenta) 13 | importFrom(crayon,red) 14 | importFrom(crayon,yellow) 15 | importFrom(keypress,has_keypress_support) 16 | importFrom(keypress,keypress) 17 | importFrom(lazyeval,lazy_dots) 18 | importFrom(lazyeval,lazy_eval) 19 | importFrom(readline,read_line) 20 | importFrom(stats,start) 21 | -------------------------------------------------------------------------------- /R/ask.R: -------------------------------------------------------------------------------- 1 | 2 | #' Ask questions to the user, at the command line, and get the answers 3 | #' 4 | #' @details 5 | #' Ask a series of questions to the user, and return all 6 | #' results together in a list. 7 | #' 8 | #' The \code{ask} function takes named arguments only: 9 | #' \itemize{ 10 | #' \item Each argument corresponds to a question to the user. 11 | #' \item The name of the argument is the identifier of the 12 | #' question, the answer will have the same name in the result list. 13 | #' \item Each argument is a function call. The name of the function 14 | #' is the type of the question. See question types below. 15 | #' \item Questions are asked in the order they are given. See 16 | #' \sQuote{Conditional execution} below for more flexible workflows. 17 | #' } 18 | #' 19 | #' @section Question types: 20 | #' \describe{ 21 | #' \item{input}{One line of text input.} 22 | #' \item{confirm}{A yes-no question, \sQuote{y} and \sQuote{yes} 23 | #' are considered as positive, \sQuote{n} and \sQuote{no} as negative 24 | #' answers (case insensitively).} 25 | #' \item{choose}{Choose one item form multiple items.} 26 | #' \item{checkbox}{Select multiple values from a list.} 27 | #' \item{constant}{Not a question, it defines constants.} 28 | #' } 29 | #' 30 | #' @section \sQuote{input} type: 31 | #' \preformatted{ 32 | #' input(message, default = "", filter = NULL, validate = NULL, 33 | #' when = NULL) 34 | #' } 35 | #' \describe{ 36 | #' \item{\code{message}}{The message to print.} 37 | #' \item{\code{default}}{The default vaue to return if the user just 38 | #' presses enter.} 39 | #' \item{\code{filter}}{If not \code{NULL}, then it must be a function, 40 | #' that is called to filter the entered result.} 41 | #' \item{\code{validate}}{If not \code{NULL}, then it must be a function 42 | #' that is called to validate the input. The function must return 43 | #' \code{TRUE} for valid inputs and an error message (character scalar) 44 | #' for invalid ones.} 45 | #' \item{\code{when}}{See \sQuote{Conditional execution} below.} 46 | #' } 47 | #' 48 | #' @section \sQuote{confirm} type: 49 | #' \preformatted{ 50 | #' confirm(message, default = TRUE, when = NULL) 51 | #' } 52 | #' \describe{ 53 | #' \item{\code{message}}{The message to print.} 54 | #' \item{\code{default}}{The default answer if the user just presses 55 | #' enter.} 56 | #' \item{\code{when}}{See \sQuote{Conditional execution} below.} 57 | #' } 58 | #' 59 | #' @section \sQuote{choose} type: 60 | #' \preformatted{ 61 | #' choose(message, choices, default = NA, when = NULL) 62 | #' } 63 | #' \describe{ 64 | #' \item{\code{message}}{Message to print.} 65 | #' \item{\code{choices}}{Possible choices, character vector.} 66 | #' \item{\code{default}}{Index or value of the default choice (if the user 67 | #' hits enter, or \code{NA} for no default. Values are matched using 68 | #' partial matches via \code{pmatch}.} 69 | #' \item{\code{when}}{See \sQuote{Conditional execution} below.} 70 | #' } 71 | #' 72 | #' @section \sQuote{checkbox} type: 73 | #' \preformatted{ 74 | #' checkbox(message, choices, default = numeric(), when = NULL) 75 | #' } 76 | #' \describe{ 77 | #' \item{\code{message}}{Message to print.} 78 | #' \item{\code{choices}}{Possible choices, character vector.} 79 | #' \item{\code{default}}{Indices or values of default choices. 80 | #' values are matches using partial matches via \code{pmatch}.} 81 | #' \item{\code{when}}{See \sQuote{Conditional execution} below.} 82 | #' } 83 | #' 84 | #' @section \sQuote{constant} type: 85 | #' \preformatted{ 86 | #' constant(value = constant_value, when = NULL) 87 | #' } 88 | #' \describe{ 89 | #' \item{\code{constant_value}}{The constant value. Note that the 90 | #' argument must be named.} 91 | #' \item{\code{when}}{See \sQuote{Conditional execution} below.} 92 | #' } 93 | #' 94 | #' @section Conditional execution: 95 | #' The \code{when} argument to a question can be used for conditional 96 | #' execution of questions. If it is given (and not \code{NULL}), then 97 | #' it must be a function. It is called with the answers list up to that 98 | #' point, and it should return \code{TRUE} or \code{FALSE}. For \code{TRUE}, 99 | #' the question is shown to the user and the result is inserted into the 100 | #' answer list. For \code{FALSE}, the question is not shown, and the 101 | #' answer list is not chagned. 102 | #' 103 | #' @param ... Questions to ask, see details below. 104 | #' @param .prompt Prompt to prepend to all questions. 105 | #' @return A named list with the answers. 106 | #' 107 | #' @export 108 | #' @importFrom lazyeval lazy_dots lazy_eval 109 | #' @importFrom crayon yellow 110 | #' @importFrom clisymbols symbol 111 | #' @examples 112 | #' \dontrun{ 113 | #' ask( 114 | #' name = input("What is your name?"), 115 | #' cool = confirm("Are you cool?"), 116 | #' drink = choose("Select your poison!", c("Beer", "Wine")), 117 | #' language = checkbox("Favorite languages?", c("C", "C++", "Python", "R")) 118 | #' ) 119 | #' } 120 | 121 | ask <- function(..., .prompt = yellow(paste0(symbol$pointer, " "))) { 122 | ask_(questions(...), .prompt = .prompt) 123 | } 124 | 125 | #' Store a series of questions, to ask them later 126 | #' 127 | #' Later you can call \code{\link{ask_}} (note the trailing underscore!) 128 | #' to ask them. 129 | #' 130 | #' @param ... Questions to store. See \code{\link{ask}}. 131 | #' @return An unevaluated series of questions for future use. 132 | #' 133 | #' @export 134 | #' @examples 135 | #' \dontrun{ 136 | #' qs <- questions( 137 | #' name = input("What is your name?"), 138 | #' color = choose("Red or blue?", c("Red", "Blue")) 139 | #' ) 140 | #' ask_(qs) 141 | #'} 142 | 143 | questions <- function(...) { 144 | x <- lazy_dots(...) 145 | class(x) <- "ask_questions" 146 | x 147 | } 148 | 149 | #' Ask a series of questions, stored in an object 150 | #' 151 | #' Store questions with \code{\link{questions}}, and then ask them 152 | #' with \code{ask_}. 153 | #' 154 | #' @param questions Questions stored with \code{\link{questions}}. 155 | #' @param .prompt Prompt to prepend to all questions. 156 | #' @return A named list with the answers, see \code{\link{ask}} 157 | #' for the format. 158 | #' 159 | #' @export 160 | #' @examples 161 | #' \dontrun{ 162 | #' qs <- questions( 163 | #' name = input("What is your name?"), 164 | #' color = choose("Red or blue?", c("Red", "Blue")) 165 | #' ) 166 | #' ask_(qs) 167 | #'} 168 | 169 | ask_ <- function(questions, .prompt = yellow(paste0(symbol$pointer, " "))) { 170 | 171 | if (!interactive()) { stop("ask() can only be used in interactive mode" ) } 172 | 173 | qs_names <- names(questions) 174 | 175 | if (is.null(qs_names) || any(qs_names == "")) { 176 | stop("Questions must have names") 177 | } 178 | 179 | if (any(unlist(lapply(questions, function(x) class(x$expr))) != "call")) { 180 | stop("Questions must be function calls") 181 | } 182 | 183 | qs_fun_names <- unlist(lapply(questions, function(x) as.character(x$expr[[1]]))) 184 | 185 | question_style <- get_style() 186 | 187 | unknown_fun <- setdiff(qs_fun_names, names(question_style)) 188 | if (length(unknown_fun)) { 189 | stop("Unknown question types: ", paste(unknown_fun, collapse = ", ")) 190 | } 191 | 192 | answers <- list() 193 | 194 | question <- function(message, ..., when = NULL, type, name) { 195 | if (! type %in% names(question_style)) stop("Unknown question type"); 196 | 197 | if (!is.null(when) && ! when(answers)) return(answers) 198 | 199 | answers[[name]] <- question_style[[type]](message = .prompt %+% message, ...) 200 | answers 201 | } 202 | 203 | for (q in seq_along(questions)) { 204 | qs_call <- questions[[q]] 205 | qs_call$expr[[1]] <- as.name("question") 206 | qs_call$expr$type <- unname(qs_fun_names[[q]]) 207 | qs_call$expr$name <- unname(qs_names[[q]]) 208 | answers <- lazy_eval( 209 | qs_call, 210 | data = list(question = question, answers = answers) 211 | ) 212 | } 213 | answers 214 | } 215 | 216 | #' @importFrom keypress has_keypress_support 217 | 218 | get_style <- function() { 219 | if (can_move_cursor() && has_keypress_support()) { 220 | style_fancy 221 | } else { 222 | style_plain 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /R/style_fancy.R: -------------------------------------------------------------------------------- 1 | 2 | #' @include style_plain.R 3 | #' @importFrom clisymbols symbol 4 | 5 | style_fancy <- list() 6 | 7 | #' @importFrom keypress keypress 8 | #' @importFrom crayon blue red green 9 | 10 | style_fancy$confirm <- function(message, default = TRUE) { 11 | prompt <- c(" (y/N) ", " (Y/n) ")[default + 1] 12 | msg(message %+% prompt) 13 | repeat { 14 | ans <- keypress() 15 | ans <- tolower(ans) 16 | if (ans == 'y' || ans == 'n' || ans == 'enter') break 17 | } 18 | ans <- ans == 'y' || (default && ans == 'enter') 19 | msg(c(green(symbol$tick), red(symbol$cross))[2 - ans], "\n", sep = "") 20 | ans 21 | } 22 | 23 | #' @importFrom readline read_line 24 | 25 | style_fancy$input <- function(message, default = "", filter = NULL, 26 | nextline = TRUE, wrap = TRUE, validate = NULL) { 27 | 28 | tw <- terminal_width() 29 | 30 | orig_message <- message 31 | if (default != "") message <- message %+% " (" %+% default %+% ")" 32 | 33 | emph <- blue 34 | 35 | if (nextline) { 36 | premsg <- message %+% start(emph) %+% "\n" 37 | prompt <- " " 38 | } else { 39 | premsg <- "" 40 | prompt <- message %+% " " %+% start(emph) 41 | } 42 | 43 | on.exit(msg(finish(emph)), add = TRUE) 44 | 45 | repeat { 46 | cat(premsg) 47 | orig_result <- result <- read_line(prompt, multiline = nextline) 48 | if (result == "") { orig_result <- result; result <- default } 49 | if (is.null(validate)) break 50 | valres <- validate(result) 51 | if (identical(valres, TRUE)) break 52 | error_msg(finish(emph), valres) 53 | } 54 | 55 | if (nextline) { 56 | uplines <- ceiling((nchar(result) + 3) / tw) + 1 57 | cursor_up(uplines) 58 | spaces1 <- make_spaces(tw) 59 | spaces2 <- make_spaces(nchar(orig_result, type = "width")) 60 | msg(orig_message %+% spaces1 %+% "\r " %+% spaces2, appendLF = TRUE) 61 | 62 | cursor_up(uplines - 1) 63 | msg(green(wrap_if(result, wrap)), appendLF = TRUE) 64 | 65 | } else if (!nextline) { 66 | cursor_up(1) 67 | spaces <- make_spaces(nchar(default, type = "width") + 3) 68 | msg(orig_message %+% " " %+% green(result) %+% spaces, appendLF = TRUE) 69 | } 70 | 71 | result <- sub("^ ", "", wrap_if(result, wrap)) 72 | 73 | if (!is.null(filter)) result <- filter(result) 74 | result 75 | } 76 | 77 | 78 | #' @importFrom crayon blue 79 | 80 | style_fancy$choose <- function(message, choices, default = NA) { 81 | if (is.character(default)) default <- pmatch(default, choices) 82 | default <- as.numeric(default) 83 | stopifnot(is.na(default) || is_index(choices, default)) 84 | 85 | current <- default 86 | if (is.na(current)) current <- 1 87 | 88 | msg(message, appendLF = TRUE) 89 | 90 | draw <- function(empty = FALSE) { 91 | choices[current] <- blue(choices[current]) 92 | pointer <- blue(symbol$pointer) 93 | pr <- paste("", ifelse(seq_along(choices) == current, pointer, " "), 94 | choices, collapse = "\n") 95 | if (empty) pr <- gsub("[^\n]", " ", pr) 96 | msg(pr, appendLF = TRUE) 97 | } 98 | 99 | draw() 100 | repeat { 101 | repeat { 102 | ans <- keypress() 103 | if (ans %in% c("up", "down", "n", "p", "enter", " ")) break 104 | } 105 | if (ans %in% c("up", "p") && current != 1) { 106 | current <- current - 1 107 | cursor_up(length(choices)) 108 | draw() 109 | } else if (ans %in% c("down", "n") && current != length(choices)) { 110 | current <- current + 1 111 | cursor_up(length(choices)) 112 | draw() 113 | } else if (ans %in% c("enter", " ")) { 114 | break 115 | } 116 | } 117 | 118 | cursor_up(length(choices) + 1) 119 | msg(message, " ", green(choices[current]), appendLF = TRUE) 120 | draw(empty = TRUE) 121 | cursor_up(length(choices)) 122 | 123 | choices[current] 124 | } 125 | 126 | style_fancy$checkbox <- function(message, choices, default = numeric()) { 127 | 128 | choices <- as.character(choices) 129 | 130 | if (is.character(default)) default <- pmatch(default, choices) 131 | default <- as.numeric(default) 132 | 133 | selected <- default 134 | current <- 1 135 | 136 | msg(message, appendLF = TRUE) 137 | 138 | draw <- function(empty = FALSE) { 139 | choices[selected] <- green(choices[selected]) 140 | pointer <- blue(symbol$pointer) 141 | box_empty <- symbol$radio_off 142 | box_fill <- green(symbol$radio_on) 143 | pr <- paste( 144 | "", 145 | ifelse(seq_along(choices) == current, pointer, " "), 146 | ifelse(seq_along(choices) %in% selected, box_fill, box_empty), 147 | choices, collapse = "\n" 148 | ) 149 | if (empty) pr <- gsub("[^\n]", " ", pr) 150 | msg(pr, appendLF = TRUE) 151 | } 152 | 153 | draw() 154 | repeat { 155 | repeat { 156 | ans <- keypress() 157 | if (ans %in% c("up", "down", "n", "p", "enter", " ")) break 158 | } 159 | if (ans %in% c("up", "p") && current != 1) { 160 | current <- current - 1 161 | cursor_up(length(choices)) 162 | draw() 163 | 164 | } else if (ans %in% c("down", "n") && current != length(choices)) { 165 | current <- current + 1 166 | cursor_up(length(choices)) 167 | draw() 168 | 169 | } else if (ans == " ") { 170 | if (current %in% selected) { 171 | selected <- setdiff(selected, current) 172 | } else { 173 | selected <- c(selected, current) 174 | } 175 | cursor_up(length(choices)) 176 | draw() 177 | 178 | } else if (ans == "enter") { 179 | break 180 | } 181 | } 182 | 183 | res <- choices[sort(selected)] 184 | 185 | cursor_up(length(choices) + 1) 186 | msg(message, " ", paste(green(res), collapse = ", "), appendLF = TRUE) 187 | draw(empty = TRUE) 188 | cursor_up(length(choices)) 189 | 190 | res 191 | } 192 | 193 | style_fancy$constant <- style_plain$constant 194 | -------------------------------------------------------------------------------- /R/style_plain.R: -------------------------------------------------------------------------------- 1 | 2 | style_plain <- list() 3 | 4 | #' @importFrom clisymbols symbol 5 | #' @importFrom crayon combine_styles magenta bold finish 6 | #' @importFrom stats start 7 | 8 | style_plain$confirm <- function(message, default = TRUE) { 9 | prompt <- c(" (y/N) ", " (Y/n) ")[default + 1] 10 | emph <- combine_styles(magenta, bold) 11 | repeat { 12 | ans <- readline(prompt = bold(message) %+% prompt %+% start(emph)) 13 | res <- NA 14 | if (ans == "") res <- default 15 | if (tolower(ans) == "y" || tolower(ans) == "yes") res <- TRUE 16 | if (tolower(ans) == "n" || tolower(ans) == "no" ) res <- FALSE 17 | if (!is.na(res)) break 18 | msg(finish(emph) %+% "Sorry, did not get it.", appendLF = TRUE) 19 | } 20 | msg(finish(emph)) 21 | res 22 | } 23 | 24 | #' @importFrom crayon combine_styles magenta bold finish 25 | #' @importFrom stats start 26 | 27 | style_plain$input <- function(message, default = "", filter = NULL, 28 | nextline = TRUE, wrap = TRUE, validate = NULL) { 29 | if (default != "") message <- message %+% " (" %+% default %+% ")" 30 | 31 | emph <- combine_styles(magenta, bold) 32 | 33 | repeat { 34 | result <- readline(bold(message) %+% " " %+% start(emph)) 35 | if (is.null(validate)) break 36 | valres <- validate(result) 37 | if (identical(valres, TRUE)) break 38 | error_msg(finish(emph), valres) 39 | } 40 | if (result == "") result <- default 41 | msg(finish(emph)) 42 | 43 | if (!is.null(filter)) result <- filter(result) 44 | result 45 | } 46 | 47 | #' @importFrom crayon green magenta bold combine_styles 48 | 49 | style_plain$choose <- function(message, choices, default = NA) { 50 | if (is.character(default)) default <- pmatch(default, choices) 51 | default <- as.numeric(default) 52 | stopifnot(is.na(default) || is_index(choices, default)) 53 | emph <- combine_styles(magenta, bold) 54 | 55 | msg( 56 | bold(message), 57 | "\n", 58 | paste0(" ", seq_along(choices), ". ", choices, collapse = "\n"), 59 | "\n" 60 | ) 61 | 62 | repeat { 63 | prompt <- paste0( 64 | green(symbol$fancy_question_mark), " ", 65 | if (! is.na(default)) " (" %+% default %+% ") " else "", 66 | start(emph) 67 | ) 68 | res <- readline(prompt = prompt) 69 | msg(finish(emph)) 70 | if (res == "" && !is.na(default)) { 71 | res <- default 72 | break 73 | } 74 | suppressWarnings(res <- as.numeric(res)) 75 | if (is.na(res) || res < 1 || res > length(choices) || ! is_integerish(res)) { 76 | msg("Sorry, I did not get that.", appendLF = TRUE) 77 | } else { 78 | res <- choices[res] 79 | break 80 | } 81 | } 82 | res 83 | } 84 | 85 | #' @importFrom crayon green magenta bold combine_styles 86 | 87 | style_plain$checkbox <- function(message, choices, default = numeric()) { 88 | 89 | if (is.character(default)) default <- pmatch(default, choices) 90 | default <- as.numeric(default) 91 | 92 | choices <- as.character(choices) 93 | emph <- combine_styles(magenta, bold) 94 | 95 | msg( 96 | bold(message), 97 | "\n", 98 | paste0(" ", seq_along(choices), ". ", choices, collapse = "\n"), 99 | "\n" 100 | ) 101 | 102 | repeat { 103 | prompt <- paste0( 104 | green(symbol$fancy_question_mark), 105 | " (Commas separated numbers, dash for nothing) ", 106 | start(emph) 107 | ) 108 | 109 | res <- strtrim(strsplit(strtrim(readline(prompt = prompt)), ",")[[1]]) 110 | msg(finish(emph)) 111 | 112 | if (length(res) == 1 && res == "-") { 113 | res <- numeric() 114 | } else if (length(res) == 1 && res == "") { 115 | res <- default() 116 | } 117 | 118 | res <- suppressWarnings(res <- as.numeric(res)) 119 | if (any(is.na(res)) || any(!is_integerish(res)) || 120 | any(res < 1) || any(res > length(choices))) { 121 | msg("Sorry, I did not get that.", appendLF = TRUE) 122 | } else { 123 | res <- choices[sort(unique(res))] 124 | break 125 | } 126 | } 127 | res 128 | } 129 | 130 | style_plain$constant <- function(message = "", value) { 131 | value 132 | } 133 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | 2 | check_tty <- function() { 3 | stopifnot(isatty(stdin())) 4 | } 5 | 6 | is_integerish <- function(x) { 7 | all(round(x) == x) 8 | } 9 | 10 | is_index <- function(vector, idx) { 11 | is_integerish(idx) && length(idx) == 1 && 12 | idx >= 1 && idx <= length(vector) 13 | } 14 | 15 | #' @importFrom crayon bold 16 | 17 | msg <- function(..., appendLF = FALSE) { 18 | message(finish(bold), ..., appendLF = appendLF) 19 | } 20 | 21 | #' @importFrom clisymbols symbol 22 | #' @importFrom crayon red 23 | 24 | error_msg <- function(..., appendLF = TRUE, markup = TRUE) { 25 | str <- paste0(...) 26 | if (markup) str <- red(paste0(symbol$cross, " ", str)) 27 | msg(str, appendLF = appendLF) 28 | } 29 | 30 | `%+%` <- function(l, r) { 31 | stopifnot(length(l) == 1, length(r) == 1) 32 | paste0(as.character(l), as.character(r)) 33 | } 34 | 35 | can_move_cursor <- function() { 36 | cmd <- "(tput cuu1 && tput cud1) > /dev/null 2> /dev/null" 37 | suppressWarnings(try(system(cmd), silent = TRUE)) == 0 38 | } 39 | 40 | cursor_up <- function(num) { 41 | cat("\033[", num, "A", sep = "") 42 | } 43 | 44 | strtrim <- function(x) { 45 | gsub("\\s+$", "", gsub("^\\s+", "", x)) 46 | } 47 | 48 | make_spaces <- function(n) { 49 | paste(rep(" ", n), collapse = "") 50 | } 51 | 52 | terminal_width <- function() { 53 | as.numeric(system("tput cols", intern = TRUE)) 54 | } 55 | 56 | wrap_if <- function(x, wrap) { 57 | if (wrap) { 58 | paste( 59 | strwrap(x, indent = 2, exdent = 2, width = terminal_width()), 60 | collapse = "\n" 61 | ) 62 | } else { 63 | x 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | inst/README.md -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # DO NOT CHANGE the "init" and "install" sections below 2 | 3 | # Download script file from GitHub 4 | init: 5 | ps: | 6 | $ErrorActionPreference = "Stop" 7 | Invoke-WebRequest http://raw.github.com/krlmlr/r-appveyor/master/scripts/appveyor-tool.ps1 -OutFile "..\appveyor-tool.ps1" 8 | Import-Module '..\appveyor-tool.ps1' 9 | 10 | install: 11 | ps: Bootstrap 12 | 13 | # Adapt as necessary starting from here 14 | 15 | build_script: 16 | - travis-tool.sh install_github gaborcsardi/readline 17 | - travis-tool.sh install_github gaborcsardi/clisymbols 18 | - travis-tool.sh install_deps 19 | 20 | test_script: 21 | - travis-tool.sh run_tests 22 | 23 | on_failure: 24 | - travis-tool.sh dump_logs 25 | 26 | artifacts: 27 | - path: '*.Rcheck\**\*.log' 28 | name: Logs 29 | 30 | - path: '*.Rcheck\**\*.out' 31 | name: Logs 32 | 33 | - path: '*.Rcheck\**\*.fail' 34 | name: Logs 35 | 36 | - path: '*.Rcheck\**\*.Rout' 37 | name: Logs 38 | 39 | - path: '\*_*.tar.gz' 40 | name: Bits 41 | 42 | - path: '\*_*.zip' 43 | name: Bits 44 | -------------------------------------------------------------------------------- /ask.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | Encoding: UTF-8 9 | 10 | AutoAppendNewline: Yes 11 | StripTrailingWhitespace: Yes 12 | 13 | BuildType: Package 14 | PackageUseDevtools: Yes 15 | PackageInstallArgs: --no-multiarch --with-keep.source 16 | PackageRoxygenize: rd,collate,namespace 17 | -------------------------------------------------------------------------------- /inst/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Friendly R CLI 3 | 4 | > `ask` helps build friendly command line interfaces 5 | 6 | [![Linux Build Status](https://travis-ci.org/gaborcsardi/ask.svg?branch=master)](https://travis-ci.org/gaborcsardi/ask) 7 | [![Windows Build status](https://ci.appveyor.com/api/projects/status/github/gaborcsardi/ask?svg=true)](https://ci.appveyor.com/project/gaborcsardi/ask) 8 | [![](http://www.r-pkg.org/badges/version/ask)](http://www.r-pkg.org/pkg/ask) 9 | [![CRAN RStudio mirror downloads](http://cranlogs.r-pkg.org/badges/ask)](http://www.r-pkg.org/pkg/ask) 10 | 11 | ## Introduction 12 | 13 | Command line interfaces don't have to be grey and boring. 14 | Just because some ancient terminals do not support color, 15 | Unicode glyphs and cursor movement, that does not mean that 16 | we can't use them on newer ones. 17 | 18 | `ask` makes use of terminal colors and other advanced 19 | properties that are actually supported by 99.9% of the 20 | terminals today. 21 | 22 | Sadly, some commonly used R IDEs do not emulate a terminal 23 | at all, so `ask()` will look a lot less nice in these 24 | (but it will still work fine): 25 | * R Studio 26 | * R.app (the default OS X R GUI) 27 | * RWin (the default Windows R GUI) 28 | * Emacs ESS 29 | 30 | `ask` was inspired by the 31 | [Inquirer.js](https://github.com/SBoudrias/Inquirer.js) project. 32 | 33 | ## Installation 34 | 35 | Once on CRAN, you can install the package with: 36 | 37 | ```r 38 | install.packages("ask") 39 | ``` 40 | 41 | ## Usage 42 | 43 | Call the `ask()` function with all your questions to the user, 44 | and the answers are returned in a list. See various question types below. 45 | 46 | Typical usage looks like this. (The example is taken from 47 | [Inquirer.js](https://github.com/SBoudrias/Inquirer.js).) 48 | 49 | ```r 50 | library(ask) 51 | message("Welcome to R Pizza!") 52 | ask( 53 | to_be_delivered = confirm("Is it for a delivery?", default = FALSE), 54 | phone = input("What's your phone number?", 55 | validate = function(v) { 56 | good <- grepl("^[- 0-9\\(\\)]+$", v) && 57 | nchar(gsub("[^0-9]", "", v)) == 10 58 | if (good) TRUE else "Please enter a valid phone number" 59 | }), 60 | size = choose("What size do you need?", c("Large", "Medium", "Small")), 61 | quantity = input("How many do you need?", validate = function(v) { 62 | good <- !is.na(as.integer(v)) 63 | if (good) TRUE else "Please enter a number" 64 | }, filter = as.integer), 65 | toppings = choose("What about the topping?", 66 | c("Peperonni and cheese", "All dressed", "Hawaïan")), 67 | toppings_extra = checkbox("Extra toppings?", 68 | c("Peperonni", "Ham", "Extra Cheese", "Olives", "Mushrooms", "Chilis")), 69 | beverage = choose("You also get a free 2L beverage", 70 | c("Pepsi", "7up", "Coke")), 71 | comments = input("Any comments about your purchase experience?", 72 | default = "Nope, all good!"), 73 | prize = choose("For leaving a comment, you get a freebie", 74 | c("Cake", "Fries"), when = function(a) a$comments != "Nope, all good!") 75 | ) 76 | ``` 77 | 78 | ![](/inst/ask-pizza.png) 79 | 80 | The result is the answer list, a named list: 81 | 82 | ```r 83 | $to_be_delivered 84 | [1] FALSE 85 | 86 | $phone 87 | [1] "555-555-5555" 88 | 89 | $size 90 | [1] "Medium" 91 | 92 | $quantity 93 | [1] 2 94 | 95 | $toppings 96 | [1] "Peperonni and cheese" 97 | 98 | $beverage 99 | [1] "Pepsi" 100 | 101 | $comments 102 | [1] "Nice CLI!" 103 | 104 | $prize 105 | [1] "Cake" 106 | ``` 107 | 108 | The `ask()` function takes named arguments only: 109 | * Each argument corresponds to a question to the user. 110 | * The name of the argument is the identifier of the 111 | question, the answer will have the same name in the result list. 112 | * Each argument is a function call. The name of the function 113 | is the type of the question. See question types below. 114 | * Questions are asked in the order they are given. See 115 | [Conditional execution](#conditional-execution) below for more 116 | flexible workflows. 117 | 118 | ## Question types 119 | 120 | ### `input`: one line of text 121 | 122 | ![](/inst/ask-input.png) 123 | 124 | ### `confirm`: a yes-no question 125 | 126 | ![](/inst/ask-confirm.png) 127 | 128 | ### `choose`: choose one item from a list 129 | 130 | ![](/inst/ask-choose.png) 131 | 132 | ### `checkbox`: select multiple values from a list 133 | 134 | ![](/inst/ask-checkbox.png) 135 | 136 | ### `constant`: not a question, defines a constant 137 | 138 | This is sometimes useful. 139 | 140 | ## Conditional execution 141 | 142 | The `when` argument to a question can be used for conditional 143 | execution of questions. If it is given (and not `NULL`), then 144 | it must be a function. It is called with the answers list up to that 145 | point, and it should return `TRUE` or `FALSE`. For `TRUE`, 146 | the question is shown to the user and the result is inserted into the 147 | answer list. For `FALSE`, the question is not shown, and the 148 | answer list is not chagned. 149 | 150 | ## License 151 | 152 | MIT © [Gábor Csárdi](http://gaborcsardi.org). 153 | -------------------------------------------------------------------------------- /inst/ask-checkbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborcsardi/ask/4de921d5fbcfd921f630c7b30eea35d26099f9e3/inst/ask-checkbox.png -------------------------------------------------------------------------------- /inst/ask-choose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborcsardi/ask/4de921d5fbcfd921f630c7b30eea35d26099f9e3/inst/ask-choose.png -------------------------------------------------------------------------------- /inst/ask-confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborcsardi/ask/4de921d5fbcfd921f630c7b30eea35d26099f9e3/inst/ask-confirm.png -------------------------------------------------------------------------------- /inst/ask-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborcsardi/ask/4de921d5fbcfd921f630c7b30eea35d26099f9e3/inst/ask-input.png -------------------------------------------------------------------------------- /inst/ask-pizza.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborcsardi/ask/4de921d5fbcfd921f630c7b30eea35d26099f9e3/inst/ask-pizza.png -------------------------------------------------------------------------------- /man/ask.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ask.R 3 | \name{ask} 4 | \alias{ask} 5 | \title{Ask questions to the user, at the command line, and get the answers} 6 | \usage{ 7 | ask(..., .prompt = yellow(paste0(symbol$pointer, " "))) 8 | } 9 | \arguments{ 10 | \item{...}{Questions to ask, see details below.} 11 | 12 | \item{.prompt}{Prompt to prepend to all questions.} 13 | } 14 | \value{ 15 | A named list with the answers. 16 | } 17 | \description{ 18 | Ask questions to the user, at the command line, and get the answers 19 | } 20 | \details{ 21 | Ask a series of questions to the user, and return all 22 | results together in a list. 23 | 24 | The \code{ask} function takes named arguments only: 25 | \itemize{ 26 | \item Each argument corresponds to a question to the user. 27 | \item The name of the argument is the identifier of the 28 | question, the answer will have the same name in the result list. 29 | \item Each argument is a function call. The name of the function 30 | is the type of the question. See question types below. 31 | \item Questions are asked in the order they are given. See 32 | \sQuote{Conditional execution} below for more flexible workflows. 33 | } 34 | } 35 | \section{Question types}{ 36 | 37 | \describe{ 38 | \item{input}{One line of text input.} 39 | \item{confirm}{A yes-no question, \sQuote{y} and \sQuote{yes} 40 | are considered as positive, \sQuote{n} and \sQuote{no} as negative 41 | answers (case insensitively).} 42 | \item{choose}{Choose one item form multiple items.} 43 | \item{checkbox}{Select multiple values from a list.} 44 | \item{constant}{Not a question, it defines constants.} 45 | } 46 | } 47 | 48 | \section{\sQuote{input} type}{ 49 | 50 | \preformatted{ 51 | input(message, default = "", filter = NULL, validate = NULL, 52 | when = NULL) 53 | } 54 | \describe{ 55 | \item{\code{message}}{The message to print.} 56 | \item{\code{default}}{The default vaue to return if the user just 57 | presses enter.} 58 | \item{\code{filter}}{If not \code{NULL}, then it must be a function, 59 | that is called to filter the entered result.} 60 | \item{\code{validate}}{If not \code{NULL}, then it must be a function 61 | that is called to validate the input. The function must return 62 | \code{TRUE} for valid inputs and an error message (character scalar) 63 | for invalid ones.} 64 | \item{\code{when}}{See \sQuote{Conditional execution} below.} 65 | } 66 | } 67 | 68 | \section{\sQuote{confirm} type}{ 69 | 70 | \preformatted{ 71 | confirm(message, default = TRUE, when = NULL) 72 | } 73 | \describe{ 74 | \item{\code{message}}{The message to print.} 75 | \item{\code{default}}{The default answer if the user just presses 76 | enter.} 77 | \item{\code{when}}{See \sQuote{Conditional execution} below.} 78 | } 79 | } 80 | 81 | \section{\sQuote{choose} type}{ 82 | 83 | \preformatted{ 84 | choose(message, choices, default = NA, when = NULL) 85 | } 86 | \describe{ 87 | \item{\code{message}}{Message to print.} 88 | \item{\code{choices}}{Possible choices, character vector.} 89 | \item{\code{default}}{Index or value of the default choice (if the user 90 | hits enter, or \code{NA} for no default. Values are matched using 91 | partial matches via \code{pmatch}.} 92 | \item{\code{when}}{See \sQuote{Conditional execution} below.} 93 | } 94 | } 95 | 96 | \section{\sQuote{checkbox} type}{ 97 | 98 | \preformatted{ 99 | checkbox(message, choices, default = numeric(), when = NULL) 100 | } 101 | \describe{ 102 | \item{\code{message}}{Message to print.} 103 | \item{\code{choices}}{Possible choices, character vector.} 104 | \item{\code{default}}{Indices or values of default choices. 105 | values are matches using partial matches via \code{pmatch}.} 106 | \item{\code{when}}{See \sQuote{Conditional execution} below.} 107 | } 108 | } 109 | 110 | \section{\sQuote{constant} type}{ 111 | 112 | \preformatted{ 113 | constant(value = constant_value, when = NULL) 114 | } 115 | \describe{ 116 | \item{\code{constant_value}}{The constant value. Note that the 117 | argument must be named.} 118 | \item{\code{when}}{See \sQuote{Conditional execution} below.} 119 | } 120 | } 121 | 122 | \section{Conditional execution}{ 123 | 124 | The \code{when} argument to a question can be used for conditional 125 | execution of questions. If it is given (and not \code{NULL}), then 126 | it must be a function. It is called with the answers list up to that 127 | point, and it should return \code{TRUE} or \code{FALSE}. For \code{TRUE}, 128 | the question is shown to the user and the result is inserted into the 129 | answer list. For \code{FALSE}, the question is not shown, and the 130 | answer list is not chagned. 131 | } 132 | \examples{ 133 | \dontrun{ 134 | ask( 135 | name = input("What is your name?"), 136 | cool = confirm("Are you cool?"), 137 | drink = choose("Select your poison!", c("Beer", "Wine")), 138 | language = checkbox("Favorite languages?", c("C", "C++", "Python", "R")) 139 | ) 140 | } 141 | } 142 | 143 | -------------------------------------------------------------------------------- /man/ask_.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ask.R 3 | \name{ask_} 4 | \alias{ask_} 5 | \title{Ask a series of questions, stored in an object} 6 | \usage{ 7 | ask_(questions, .prompt = yellow(paste0(symbol$pointer, " "))) 8 | } 9 | \arguments{ 10 | \item{questions}{Questions stored with \code{\link{questions}}.} 11 | 12 | \item{.prompt}{Prompt to prepend to all questions.} 13 | } 14 | \value{ 15 | A named list with the answers, see \code{\link{ask}} 16 | for the format. 17 | } 18 | \description{ 19 | Store questions with \code{\link{questions}}, and then ask them 20 | with \code{ask_}. 21 | } 22 | \examples{ 23 | \dontrun{ 24 | qs <- questions( 25 | name = input("What is your name?"), 26 | color = choose("Red or blue?", c("Red", "Blue")) 27 | ) 28 | ask_(qs) 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /man/questions.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ask.R 3 | \name{questions} 4 | \alias{questions} 5 | \title{Store a series of questions, to ask them later} 6 | \usage{ 7 | questions(...) 8 | } 9 | \arguments{ 10 | \item{...}{Questions to store. See \code{\link{ask}}.} 11 | } 12 | \value{ 13 | An unevaluated series of questions for future use. 14 | } 15 | \description{ 16 | Later you can call \code{\link{ask_}} (note the trailing underscore!) 17 | to ask them. 18 | } 19 | \examples{ 20 | \dontrun{ 21 | qs <- questions( 22 | name = input("What is your name?"), 23 | color = choose("Red or blue?", c("Red", "Blue")) 24 | ) 25 | ask_(qs) 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(ask) 3 | 4 | test_check("ask") 5 | -------------------------------------------------------------------------------- /tests/testthat/test-utils.R: -------------------------------------------------------------------------------- 1 | 2 | context("Utilities") 3 | 4 | test_that("make_spaces is OK", { 5 | 6 | expect_equal(make_spaces(0), "") 7 | expect_equal(make_spaces(1), " ") 8 | expect_equal(make_spaces(2), " ") 9 | 10 | }) 11 | --------------------------------------------------------------------------------