Title: | Framework for Building Interfaces to Shell Commands |
---|---|
Description: | Writing interfaces to command line software is cumbersome. 'cmdfun' provides a framework for building function calls to seamlessly interface with shell commands by allowing lazy evaluation of command line arguments. 'cmdfun' also provides methods for handling user-specific paths to tool installs or secrets like API keys. Its focus is to equally serve package builders who wish to wrap command line software, and to help analysts stay inside R when they might usually leave to execute non-R software. |
Authors: | Spencer Nystrom [aut, cre, cph] |
Maintainer: | Spencer Nystrom <[email protected]> |
License: | MIT + file LICENSE |
Version: | 1.0.2 |
Built: | 2024-11-22 04:22:38 UTC |
Source: | https://github.com/snystrom/cmdfun |
Not meant to be called directly
.check_valid_command_path(path)
.check_valid_command_path(path)
path |
path to file or directory |
expanded system path
if (.Platform$OS.type == "unix" & file.exists("~/bin")) { # will return /full/path/to/home/bin, or error if path doesn't exist .check_valid_command_path("~/bin") }
if (.Platform$OS.type == "unix" & file.exists("~/bin")) { # will return /full/path/to/home/bin, or error if path doesn't exist .check_valid_command_path("~/bin") }
Not meant to be called directly
.check_valid_util(util, utils = NULL, path = NULL)
.check_valid_util(util, utils = NULL, path = NULL)
util |
name of target located in path |
utils |
name of supported targets in path |
path |
path to directory |
safe path to util, or error if util does not exist
if (.Platform$OS.type == "unix") { # this will return /full/path/to/bin # or return an error for all values of util that are not "ls" and "pwd" # or error if "ls" does not exist in "/bin" .check_valid_util("ls", utils = c("ls", "pwd"), "/bin") ## Not run: # This will throw error .check_valid_util("badUtil", utils = c("ls", "pwd"), "/bin") ## End(Not run) }
if (.Platform$OS.type == "unix") { # this will return /full/path/to/bin # or return an error for all values of util that are not "ls" and "pwd" # or error if "ls" does not exist in "/bin" .check_valid_util("ls", utils = c("ls", "pwd"), "/bin") ## Not run: # This will throw error .check_valid_util("badUtil", utils = c("ls", "pwd"), "/bin") ## End(Not run) }
Return all named arguments and arguments passed as dots from parent function call
cmd_args_all(keep = NULL, drop = NULL)
cmd_args_all(keep = NULL, drop = NULL)
keep |
name of arguments to keep |
drop |
name of arguments to drop (NOTE: keep or drop are mutually exclusive settings) |
named list of all arguments passed to parent
theFunction <- function(arg1, ...) { cmd_args_all() } theArgs <- theFunction(arg1 = "test", example = "hello")
theFunction <- function(arg1, ...) { cmd_args_all() } theArgs <- theFunction(arg1 = "test", example = "hello")
return function dots from parent function as named list
cmd_args_dots(keep = NULL, drop = NULL)
cmd_args_dots(keep = NULL, drop = NULL)
keep |
name of arguments to keep |
drop |
name of arguments to drop (NOTE: keep or drop are mutually exclusive settings) |
named list of kwargs from ...
theFunction <- function(...) { cmd_args_dots() } theDots <- theFunction(example = "hello", boolFlag = TRUE, vectorFlag = c(1,2,3))
theFunction <- function(...) { cmd_args_dots() } theDots <- theFunction(example = "hello", boolFlag = TRUE, vectorFlag = c(1,2,3))
Return all named arguments from parent function call
cmd_args_named(keep = NULL, drop = NULL)
cmd_args_named(keep = NULL, drop = NULL)
keep |
name of arguments to keep |
drop |
name of arguments to drop (NOTE: keep or drop are mutually exclusive settings) |
named list of all defined function arguments from parent
theFunction <- function(arg1, ...) { cmd_args_named() } theNamedArgs <- theFunction(arg1 = "test", example = "hello")
theFunction <- function(arg1, ...) { cmd_args_named() } theNamedArgs <- theFunction(arg1 = "test", example = "hello")
Check that file(s) exist, error if not
cmd_error_if_missing(files)
cmd_error_if_missing(files)
files |
list or vector of paths to check |
nothing or error message for each missing file
cmd_error_if_missing(tempdir()) ## Not run: # Throws error if file doesn't exist cmd_error_if_missing(file.path(tempdir(), "notreal")) ## End(Not run)
cmd_error_if_missing(tempdir()) ## Not run: # Throws error if file doesn't exist cmd_error_if_missing(file.path(tempdir(), "notreal")) ## End(Not run)
See documentation of cmd_file_expect() for more details about how this works
cmd_file_combn(prefix, ext, outdir = ".")
cmd_file_combn(prefix, ext, outdir = ".")
prefix |
file name to be given each ext. If a character vector, must be equal length of ext or shorter |
ext |
file extension (no ".", ie "txt", "html") |
outdir |
optional directory where files should exist |
list of file paths by each ext or prefix (whichever is longer)
# Makes list for many file types of same prefix # ie myFile.txt, myFile.html, myFile.xml cmd_file_combn("myFile", c("txt", "html", "xml")) # Makes list for many files of same type # ie myFile1.txt, myFile2.txt, myFile3.txt cmd_file_combn(c("myFile1", "myFile2", "myFile3"), "txt")
# Makes list for many file types of same prefix # ie myFile.txt, myFile.html, myFile.xml cmd_file_combn("myFile", c("txt", "html", "xml")) # Makes list for many files of same type # ie myFile1.txt, myFile2.txt, myFile3.txt cmd_file_combn(c("myFile1", "myFile2", "myFile3"), "txt")
Ext or prefix can be a vector or single character. The shorter value will be propagated across all values of the other. See Examples for details.
cmd_file_expect(prefix, ext, outdir = ".")
cmd_file_expect(prefix, ext, outdir = ".")
prefix |
name of file prefix for each extension. |
ext |
vector of file extensions |
outdir |
directory the files will be inside |
If files are not found, throws an error
vector of valid file paths
## Not run: # Expects many file types of same prefix # ie myFile.txt, myFile.html, myFile.xml cmd_file_expect("myFile", c("txt", "html", "xml")) # Expects many files of same type # ie myFile1.txt, myFile2.txt, myFile3.txt cmd_file_expect(c("myFile1", "myFile2", "myFile3"), "txt") # Expects many files with each prefix and each extension # ie myFile1.txt, myFile1.html, myFile2.txt, myFile2.html cmd_file_expect(c("myFile1", "myFile2"), c("txt", "html")) ## End(Not run)
## Not run: # Expects many file types of same prefix # ie myFile.txt, myFile.html, myFile.xml cmd_file_expect("myFile", c("txt", "html", "xml")) # Expects many files of same type # ie myFile1.txt, myFile2.txt, myFile3.txt cmd_file_expect(c("myFile1", "myFile2", "myFile3"), "txt") # Expects many files with each prefix and each extension # ie myFile1.txt, myFile1.html, myFile2.txt, myFile2.html cmd_file_expect(c("myFile1", "myFile2"), c("txt", "html")) ## End(Not run)
Suggest alternative name by minimizing Levenshtein edit distance between valid and invalid arguments
cmd_help_flags_similar( command_flag_names, flags, .fun = NULL, distance_cutoff = 3L )
cmd_help_flags_similar( command_flag_names, flags, .fun = NULL, distance_cutoff = 3L )
command_flag_names |
character vector of valid names (can be output of |
flags |
a vector names correspond to values to be checked against |
.fun |
optional function to apply to |
distance_cutoff |
Levenshtein edit distance beyond which to suggest ??? instead of most similar argument (default = 3). Setting this too liberally will result in nonsensical suggestions. |
named vector where names are names from flags
and their values are the suggested best match from command_flag_names
# with a flagsList, need to pass names() flagsList <- list("output" = "somevalue", "missplld" = "anotherValue") cmd_help_flags_similar(c("output", "misspelled"), names(flagsList)) command_flags <- c("long-flag-name") flags <- c("long_flag_naee") cmd_help_flags_similar(command_flags, flags, .fun = ~{gsub("-", "_", .x)}) # returns NULL if no errors cmd_help_flags_similar(c("test"), "test")
# with a flagsList, need to pass names() flagsList <- list("output" = "somevalue", "missplld" = "anotherValue") cmd_help_flags_similar(c("output", "misspelled"), names(flagsList)) command_flags <- c("long-flag-name") flags <- c("long_flag_naee") cmd_help_flags_similar(command_flags, flags, .fun = ~{gsub("-", "_", .x)}) # returns NULL if no errors cmd_help_flags_similar(c("test"), "test")
Error & Suggest different flag name to user
cmd_help_flags_suggest(suggest_names)
cmd_help_flags_suggest(suggest_names)
suggest_names |
named character vector, names correspond to original value, values correspond to suggested replacement. |
error message suggesting alternatives to user
user_flags <- list("output", "inpt") valid_flags <- c("output", "input") suggestions <- cmd_help_flags_similar(valid_flags, user_flags) ## Not run: # Throws error cmd_help_flags_suggest(suggestions) ## End(Not run)
user_flags <- list("output", "inpt") valid_flags <- c("output", "input") suggestions <- cmd_help_flags_similar(valid_flags, user_flags) ## Not run: # Throws error cmd_help_flags_suggest(suggestions) ## End(Not run)
When using cmdfun to write lazy shell wrappers, the user can easily mistype a commandline flag since there is not text completion. Some programs behave unexpectedly when flags are typed incorrectly, and for this reason return uninformative error messages.
cmd_help_parse_flags(help_lines, split_newline = FALSE)
cmd_help_parse_flags(help_lines, split_newline = FALSE)
help_lines |
|
split_newline |
|
cmd_help_parse_flags
tries to grab flags from –help documentation which
can be used for error checking. It will try to parse flags following "-" or
"–" while ignoring hyphenated words in help text. Although this should cover
most use-cases, it may be necessary to write a custom help-text parser for
nonstandard tools. Inspect this output carefully before proceeding. Most
often, characters are leftover at the end of parsed names, which will
require additional parsing.
character vector of flag names parsed from help text
cmd_help_flags_similar
cmd_help_flags_suggest
if (.Platform$OS.type == "unix" & file.exists("/bin/tar")) { # below are two examples parsing the --help method of GNU tar # with processx if (require(processx)) { out <- processx::run("tar", "--help", error_on_status = FALSE) fn_flags <- cmd_help_parse_flags(out$stdout, split_newline = TRUE) } # with system2 lines <- system2("tar", "--help", stderr = TRUE) fn_flags <- cmd_help_parse_flags(lines) # NOTE: some of the "tar" flags contain the extra characters: "\[", "\)", and ";" # ie "one-top-level\[" which should be "one-top-level" # These can be additionally parsed using gsub("[\\[;\\)]", "", fn_flags) }
if (.Platform$OS.type == "unix" & file.exists("/bin/tar")) { # below are two examples parsing the --help method of GNU tar # with processx if (require(processx)) { out <- processx::run("tar", "--help", error_on_status = FALSE) fn_flags <- cmd_help_parse_flags(out$stdout, split_newline = TRUE) } # with system2 lines <- system2("tar", "--help", stderr = TRUE) fn_flags <- cmd_help_parse_flags(lines) # NOTE: some of the "tar" flags contain the extra characters: "\[", "\)", and ";" # ie "one-top-level\[" which should be "one-top-level" # These can be additionally parsed using gsub("[\\[;\\)]", "", fn_flags) }
This function can be lightly wrapped by package builders to build a user-friendly install checking function.
cmd_install_check(path_search, path = NULL)
cmd_install_check(path_search, path = NULL)
path_search |
|
path |
user-override path to check (identical to |
pretty printed message indicating whether files exits or not. Green check = Yes, red X = No.
## Not run: path_search <- cmd_path_search(default = "/bin", utils = "ls") cmd_install_check(path_search) ## End(Not run)
## Not run: path_search <- cmd_path_search(default = "/bin", utils = "ls") cmd_install_check(path_search) ## End(Not run)
Macro for constructing boolean check for valid path
cmd_install_is_valid(path_search, util = NULL)
cmd_install_is_valid(path_search, util = NULL)
path_search |
function output of |
util |
value to pass to |
a function returning TRUE or FALSE if a valid install is detected.
With arguments: path
(a path to install location), util
an optional character(1)
to
if (.Platform$OS.type == "unix") { search <- cmd_path_search(option_name = "bin_path", default_path = "/bin/") valid_install <- cmd_install_is_valid(search) # Returns TRUE if "/bin/" exists valid_install() # Returns FALSE if "bad/path/" doesn't exist valid_install("bad/path/") # Also works with options search_option_only <- cmd_path_search(option_name = "bin_path") valid_install2 <- cmd_install_is_valid(search_option_only) options(bin_path = "/bin/") valid_install2() # Setting util = TRUE will check that all utils are also installed search_with_utils <- cmd_path_search(default_path = "/bin", utils = c("ls", "pwd")) valid_install_all <- cmd_install_is_valid(search_with_utils, util = TRUE) valid_install_all() }
if (.Platform$OS.type == "unix") { search <- cmd_path_search(option_name = "bin_path", default_path = "/bin/") valid_install <- cmd_install_is_valid(search) # Returns TRUE if "/bin/" exists valid_install() # Returns FALSE if "bad/path/" doesn't exist valid_install("bad/path/") # Also works with options search_option_only <- cmd_path_search(option_name = "bin_path") valid_install2 <- cmd_install_is_valid(search_option_only) options(bin_path = "/bin/") valid_install2() # Setting util = TRUE will check that all utils are also installed search_with_utils <- cmd_path_search(default_path = "/bin", utils = c("ls", "pwd")) valid_install_all <- cmd_install_is_valid(search_with_utils, util = TRUE) valid_install_all() }
Drop entries from list of flags by name, name/value pair, or index
cmd_list_drop(flags, drop)
cmd_list_drop(flags, drop)
flags |
named list output of cmd_list_interp |
drop |
vector of flag entries to drop. Pass a character vector to drop flags by name. Pass a named vector to drop flags by name/value pairs. Pass a numeric vector to drop by position. |
flags list with values in drop removed
exFlags <- list("flag1" = 2, "flag2" = "someText") cmd_list_drop(exFlags, "flag1") # will drop flag2 because its name and value match 'drop' vector cmd_list_drop(exFlags, c("flag2" = "someText")) # Will drop "flag1" by position index cmd_list_drop(exFlags, 1) # won't drop flag2 because its value isn't 'someText' exFlags2 <- list("flag1" = 2, "flag2" = "otherText") cmd_list_drop(exFlags, c("flag2" = "someText"))
exFlags <- list("flag1" = 2, "flag2" = "someText") cmd_list_drop(exFlags, "flag1") # will drop flag2 because its name and value match 'drop' vector cmd_list_drop(exFlags, c("flag2" = "someText")) # Will drop "flag1" by position index cmd_list_drop(exFlags, 1) # won't drop flag2 because its value isn't 'someText' exFlags2 <- list("flag1" = 2, "flag2" = "otherText") cmd_list_drop(exFlags, c("flag2" = "someText"))
A pipe-friendly wrapper around list[!(names(list) %in% names)]
This function is slightly faster than using cmd_list_drop()
to drop items
by name.
cmd_list_drop_named(list, names)
cmd_list_drop_named(list, names)
list |
an R list |
names |
vector of names to drop |
list removing items defined by names
cmd_list_drop_named(list("a" = 1, "b" = 2), "a")
cmd_list_drop_named(list("a" = 1, "b" = 2), "a")
Function also handles error checking to ensure args contain valid data types, and looks for common usage mistakes.
cmd_list_interp(args, flag_lookup = NULL)
cmd_list_interp(args, flag_lookup = NULL)
args |
named list output from get*Args family of functions. |
flag_lookup |
optional named vector used to convert args to command flags |
The list structure is more amenable to manipulation by package developers for advanced use before evaluating them to the command flags vector with cmd_list_to_flags().
named list
theFunction <- function(...){cmd_args_all()} theArgs <- theFunction(arg1 = "value", arg2 = TRUE) flagList <- cmd_list_interp(theArgs) flags <- cmd_list_to_flags(flagList)
theFunction <- function(...){cmd_args_all()} theArgs <- theFunction(arg1 = "value", arg2 = TRUE) flagList <- cmd_list_interp(theArgs) flags <- cmd_list_to_flags(flagList)
keep entries from list of flags by name, name/value pair, or index
cmd_list_keep(flags, keep)
cmd_list_keep(flags, keep)
flags |
named list output of cmd_list_interp |
keep |
vector of flag entries to keep. Pass a character vector to keep flags by name. Pass a named vector to keep flags by name/value pairs. Pass a numeric vector to keep by position. |
flags list with values not in keep removed
exFlags <- list("flag1" = 2, "flag2" = "someText") cmd_list_keep(exFlags, "flag1") # will keep flag2 because its name and value match 'keep' vector cmd_list_keep(exFlags, c("flag2" = "someText")) # Will keep "flag1" by position index cmd_list_keep(exFlags, 1) # won't keep flag2 because its value isn't 'someText' exFlags2 <- list("flag1" = 2, "flag2" = "otherText") cmd_list_keep(exFlags, c("flag2" = "someText"))
exFlags <- list("flag1" = 2, "flag2" = "someText") cmd_list_keep(exFlags, "flag1") # will keep flag2 because its name and value match 'keep' vector cmd_list_keep(exFlags, c("flag2" = "someText")) # Will keep "flag1" by position index cmd_list_keep(exFlags, 1) # won't keep flag2 because its value isn't 'someText' exFlags2 <- list("flag1" = 2, "flag2" = "otherText") cmd_list_keep(exFlags, c("flag2" = "someText"))
A pipe-friendly wrapper around list[(names(list) %in% names]
.
cmd_list_keep_named(list, names)
cmd_list_keep_named(list, names)
list |
an R list |
names |
vector of names to keep |
This function is slightly faster than using cmd_list_keep()
to keep items
by name.
list keeping only items defined by names
cmd_list_keep_named(list("a" = 1, "b" = 2), "a")
cmd_list_keep_named(list("a" = 1, "b" = 2), "a")
Convert flag list to vector of command flags
cmd_list_to_flags(flagList, prefix = "-", sep = ",")
cmd_list_to_flags(flagList, prefix = "-", sep = ",")
flagList |
output from cmd_list_interp(). A named list where names correspond to flags and members correspond to the value for the flag. |
prefix |
flag prefix, usually "-" or "–". |
sep |
separator to use if flag has a vector of values (default: NULL). |
character vector of parsed commandline flags followed by their values
theFunction <- function(...){cmd_args_all()} theArgs <- theFunction(arg1 = "value", arg2 = TRUE) flagList <- cmd_list_interp(theArgs) flags <- cmd_list_to_flags(flagList)
theFunction <- function(...){cmd_args_all()} theArgs <- theFunction(arg1 = "value", arg2 = TRUE) flagList <- cmd_list_interp(theArgs) flags <- cmd_list_to_flags(flagList)
A common pattern in designing shell interfaces is to ask the user to give an absolute path to the target shell utility. It is common to pass this information from the user to R by using either R environment variables defined in .Renviron, using options (set with option(), and got with getOption()), having the user explicitly pass the path in the function call, or failing this, using a default install path.
cmd_path_search( environment_var = NULL, option_name = NULL, default_path = NULL, utils = NULL )
cmd_path_search( environment_var = NULL, option_name = NULL, default_path = NULL, utils = NULL )
environment_var |
name of R environment variable defining target path. Can be set in .Renviron. |
option_name |
name of user-configurable option (called by getOption) which will hold path to target |
default_path |
default install path of target. Can contain shell specials like "~" which will be expanded at runtime (as opposed to build time of the search function). |
utils |
optional character vector containing names of valid utils inside target path, used to populate error checking for valid install. |
Another common use-case involves software packages with many tools packaged in a single directory, and the user may want to call one or many utilities within this common structure.
For example, the software "coolpackage" is installed in "~/coolpackage", and has two programs: "tool1", and "tool2" found in "~/coolpackage/tool1" and ~/coolpackage/tool2", respectively.
To design an interface to coolpackage, this function can automate checking and validation for not only the package, but for each desired utility in the package.
The hierarchy of path usage is: user-defined > option_name > environment_var > default_path
function that returns a valid path to tool or optional utility.
The returned path_search function takes as input a path or util. where path is a user override path for the supported tool. If the user-defined path is invalid, this will always throw an error and not search the defined defaults.
util must be found within the target path, but does not have to be present in
the original "utils" call. The user will be warned if this is the case. If
util
is set to TRUE
will return all paths to utilities without checking
the install. This can be used for writing user-facing install checkers.
if (.Platform$OS.type == "unix") { bin_checker <- cmd_path_search(default_path = "/bin", utils = c("ls", "pwd")) # returns path to bin bin_checker() # returns path to bin/ls bin_checker(util = "ls") }
if (.Platform$OS.type == "unix") { bin_checker <- cmd_path_search(default_path = "/bin", utils = c("ls", "pwd")) # returns path to bin bin_checker() # returns path to bin/ls bin_checker(util = "ls") }
Checks if file exists, returns pretty status message
cmd_ui_file_exists(file)
cmd_ui_file_exists(file)
file |
path to file |
ui_done or ui_oops printed to terminal.
cmd_ui_file_exists("/path/to/file.txt")
cmd_ui_file_exists("/path/to/file.txt")