Git Product home page Git Product logo

scribe's Introduction

scribe

R-CMD-check Codecov test coverage

The goal of scribe is to provide a detailed argument parser for Rscript. This package contains no dependencies outside of base and methods. The core functions utilize ReferenceClasses under the hood.

library(scribe)

Install {scribe} from CRAN with:

install.packages("scribe")

Alternatively, you can install the development version of {scribe} GitHub with:

# install.packages("devtools")
devtools::install_github("jmbarbone/scribe")

You can enter command arguments as a vector to test out the behavior. Arguments can be added to the scribeCommandArgs class (here as ca). Default behavior tries to parse objects but additional control can be taken.

ca <- command_args(c("-a", "1", "-b", "2"))
ca$add_argument("-a")
ca$add_argument("-b")
args <- ca$parse()

str(args$a + args$b)
#>  int 3

Control

# don't convert numbers
ca <- command_args(c("-a", "1", "-b", "1.0"))
ca$add_argument("-a", convert = as.character)
ca$add_argument("-b", convert = as.character)
ca$parse()
#> $a
#> [1] "1"
#> 
#> $b
#> [1] "1.0"

# convert numbers to integers
ca <- command_args(c("verbose", "1", "1.5", "1.9"))
ca$add_argument("verbose", action = "flag")
ca$add_argument("...", convert = as.integer)
ca$parse()
#> $verbose
#> [1] TRUE
#> 
#> $...
#> integer(0)

# use functions for more control
ca <- command_args(c("verbose", "12-9-2022", "12-10-2022"))
ca$add_argument("verbose", action = "flag")
ca$add_argument("...", convert = function(i) as.Date(i, "%m-%d-%Y"))
ca$parse()
#> $verbose
#> [1] TRUE
#> 
#> $...
#> Date of length 0

You’ll probably use {scribe} within small scripts that can be called from your favorite terminal. The example below uses a function to call this file, but if it is added to your PATH you’d be able to call it directly:

r-file -a 1 -b 9
lines <- "
#! /usr/bin/env -S Rscript --vanilla 

library(scribe)
ca <- scribe::command_args()
ca$add_argument('-a', default = 1)
ca$add_argument('-b', default = 2)
args <- ca$parse()

foo <- function(a, b, ...) {
  a + b
}

do.call(foo, args)
"

file <- tempfile()
writeLines(lines, file)

rscript <- function(x, args = character()) {
  args <- c("--vanilla", x, args)
  res <- system2("Rscript", args, stdout = TRUE)
  writeLines(res)
}

rscript(file)
#> [1] 3
rscript(file, "-a 0")
#> [1] 2
rscript(file, "-a 0 -b 10")
#> [1] 10

Examples

I’ve been using {scribe} for some personal scripts. Below is a short list of some examples (mostly in my jmb repo):

Other packages

This isn’t the first package. Most contain other dependencies, some even in different languages (e.g., python).

scribe's People

Contributors

jmbarbone avatar

Stargazers

 avatar  avatar

Watchers

 avatar

scribe's Issues

handle dots better

foo <- function(...) Reduce(`+`, list(...))
(params1 <- list(... = 1:5))
#> $...
#> [1] 1 2 3 4 5
(params2 <- as.list(c(... = 1:5)))
#> $...1
#> [1] 1
#> 
#> $...2
#> [1] 2
#> 
#> $...3
#> [1] 3
#> 
#> $...4
#> [1] 4
#> 
#> $...5
#> [1] 5
do.call(foo, params1)
#> [1] 1 2 3 4 5
do.call(foo, params2)
#> [1] 15

ca <- scribe::command_args(c("foo", "bar"))
ca$add_argument("...", default = character())
ca$parse()
#> $...
#> [1] "foo" "bar"

Created on 2022-12-04 with reprex v2.0.2

Release scribe 0.2.0

Prepare for release:

  • git pull
  • Check current CRAN check results
  • Check if any deprecation processes should be advanced, as described in Gradual deprecation
  • Polish NEWS
  • devtools::build_readme()
  • urlchecker::url_check()
  • devtools::check(remote = TRUE, manual = TRUE)
  • devtools::check_win_devel()
  • rhub::check_for_cran()
  • revdepcheck::revdep_check(num_workers = 4)
  • Update cran-comments.md
  • git push
  • Draft blog post

Submit to CRAN:

  • usethis::use_version('minor')
  • devtools::submit_cran()
  • Approve email

Wait for CRAN...

  • Accepted 🎉
  • git push
  • usethis::use_github_release()
  • usethis::use_dev_version()
  • git push
  • Finish blog post
  • Tweet
  • Add link to blog post in pkgdown news menu

consider `execute` field

Function that is to be run with the value of the parameter

scribe_help_arg <- function() {
  new_arg(
    aliases = "--help",
    action = "flag",
    default = FALSE,
    n = 0,
    info = "prints this and quietly exits",
    options = list(no = FALSE),
    stop = "hard",
    execute = function() {
      if (isTRUE(self$values$help)) {
        self$help()
        exit()
        return(invisible(self))
      }
    }
  )
}

May need a warning "For expert usage only". May be able to redefine an execution field after looping through each each/parsed value.

flags don't work correctly

Should return list(f = TRUE) when "-f" is present; list(f = FALSE) when not.

library(scribe)
ca <- command_args()
ca$add_argument("-f", action = "flag")
ca$parse()
#> $f
#> logical(0)

ca <- command_args("-f")
ca$add_argument("-f", action = "flag")
ca$parse()
#> $f
#> character(0)

Created on 2022-12-09 with reprex v2.0.2

default for dots isn't correct

maybe because commandArgs(TRUE) returns character() when nothing is included

library(scribe)
ca <- command_args("value")
ca$add_argument("...", "--foo", default = character())
ca$parse()
#> $foo
#> [1] "value"

ca <- command_args()
ca$add_argument("...", "--foo", default = "bar")
ca$parse()
#> $foo
#> character(0)

Created on 2022-12-05 with reprex v2.0.2

`convert` ignored when `default` is set

library(scribe)
ca <- command_args(string = "1")
ca$add_argument("foo", convert = stop, default = 1)
ca$parse()
#> $foo
#> [1] 1

ca <- command_args(string = "1")
ca$add_argument("foo", convert = stop)
try(ca$parse())
#> Error in value_convert(value, to = default %||% self$convert) : 1

Created on 2023-09-27 with reprex v2.0.2.9000

pass `scribeArg` to `ca$add_argument()`

this should work:

ca <- command_args()
arg <- new_arg("--alias")
ca$add_argument(arg)

Can save default help and version arguments and easily append. May need to think about (or remove) the id filed in scribeArg.

allow `TRUE` for as `default` for `action = "flag"`

library(scribe)
ca <- command_args()
ca$add_argument("--foo", action = "flag", default = TRUE)
#> Warning: flag must be NULL or TRUE when action="flag"
ca$parse()
#> $foo
#> [1] FALSE
ca$set_input("--foo")
ca$parse()
#> $foo
#> [1] TRUE

Created on 2023-04-25 with reprex v2.0.2

consider `---` args?

Triples could be used to interact with scribe based things. Currently program --version prints out the {scribe} version, which doesn't seem very good.

Could start with ---help, ---version, ---verbose(to set verbosity within{scribe}` backend)

> program ---help
# {scribe} Command Argument Parsing for R
# 
# https://jmbarbone.github.io/scribe/
#
# ...
> program ---version
# {scribe} package version: 0.3.0
> program ---version-r
# [email protected]
> program ---version-n
# 0.3.0

100% coverage

will probably involve removing some functions/params

Release scribe 0.3.0

Prepare for release:

  • git pull
  • Check current CRAN check results
  • Polish NEWS
  • usethis::use_github_links()
  • urlchecker::url_check()
  • devtools::build_readme()
  • devtools::check(remote = TRUE, manual = TRUE)
  • devtools::check_win_devel()
  • revdepcheck::revdep_check(num_workers = 4)
  • Update cran-comments.md
  • git push
  • Draft blog post

Submit to CRAN:

  • usethis::use_version('minor')
  • devtools::submit_cran()
  • Approve email

Wait for CRAN...

  • Accepted 🎉
  • usethis::use_github_release()

allow `NA` for `action = "flag"`

NA should be accepted as a flag value -- which should represent a flag that is not explicitly stated

ca <- command_args()
ca$add_argument("--foo", action = "flag")
ca$parse()
#> $foo
#> [1] FALSE

ca <- command_args()
ca$add_argument("--foo", action = "flag", default = NA)
ca$parse()
#> $foo
#> [1] NA

clean up defaults in scribeArg

clean up some smarter default values

library(scribe)
# this should be fine
try(new_arg())
#> Error in new_arg() : could not find function "new_arg"

# n = NA fails, but 0 is okay?
try(new_arg("", action = "dots", n = NA_integer_))
#> Error in new_arg("", action = "dots", n = NA_integer_) : 
#>   could not find function "new_arg"
new_arg("", action = "dots", n = 0L)$n
#> Error in new_arg("", action = "dots", n = 0L): could not find function "new_arg"

# alias = "", n = NA_integer_ (default to 1)
str(new_arg("", n = NA_integer_))
#> Error in new_arg("", n = NA_integer_): could not find function "new_arg"
str(new_arg("", action = "flag", n = NA_integer_))
#> Error in new_arg("", action = "flag", n = NA_integer_): could not find function "new_arg"

Created on 2022-12-04 with reprex v2.0.2

organize code

  • define classes in one statement (don't use separate $methods()) ugh, RStudio has some weird indentation with functions -- it's fine as is
  • move wrappers to separate file
  • consider $field(name, value) for assignment
  • consider $initFields(...) for default field values
  • be consistent with order of methods/functions in code
  • consider some use of private/internal environment seem too frustrating to implement right now; instead can just remove the explicit methods and reduce fields
  • create default args for help and version
  • richer descriptions
  • more examples
  • review vignette

Release scribe 0.1.0

First release:

Prepare for release:

  • git pull
  • Check if any deprecation processes should be advanced, as described in Gradual deprecation
  • devtools::build_readme()
  • urlchecker::url_check()
  • devtools::check(remote = TRUE, manual = TRUE)
  • devtools::check_win_devel()
  • rhub::check_for_cran()
  • git push
  • Draft blog post

Submit to CRAN:

  • usethis::use_version('minor')
  • devtools::submit_cran()
  • Approve email

Wait for CRAN...

  • Accepted 🎉
  • git push
  • usethis::use_github_release()
  • usethis::use_dev_version()
  • usethis::use_news_md()
  • git push
  • Finish blog post
  • Tweet
  • Add link to blog post in pkgdown news menu

parse `--no-foo` flags

this should work by include "--no" at the beginning of each Arg that uses the "flag" method.

ca <- scribe::command_args(string = "--no-foo")
ca$add_argument("-f", "--foo")
ca$parse()
#> $foo
#> [1] FALSE

Probably "--foo --no-foo" should throw an error.

parse `-` to `_` in name

this should render to list(foo_bar = NULL) instead

library(scribe)
ca <- command_args()
ca$add_argument("--foo-bar")
ca$parse()
#> $`foo-bar`
#> NULL

Created on 2023-02-08 with reprex v2.0.2

allow string for ca initialization

command_arg(string = "-a one -b two")

should be equivalent to

command_arg(c("-a", "one", "-b", "two"))

just for testing purposes; commandArgs() handles splitting

fix snapshots

testthat complains about curly braces:

library(scribe)
ca <- command_args()
ca$help()
#> {scribe} command_args
#> 
#> file : {path}
#> 
#> USAGE
#>   {command} [--help | --version]
#>   {command} [--help] [--version] 
#> 
#> ARGUMENTS
#>   --help    : prints this and quietly exits                   
#>   --version : prints the version of {scribe} and quietly exits
try(testthat::expect_snapshot(ca$help()))
Error in `lapply(text, glue_cmd, .envir = .envir)`:
! Could not evaluate cli `{}` expression: `scribe`.
Caused by error in `eval(expr, envir = envir)`:
! object 'scribe' not found
Type .Last.error to see the more details.

can just remove the { and }.

correct input changing

adding inputs after ca is created fails

library(scribe)

# works fine
ca <- command_args(c("foo", "bar"))
ca$add_argument("foo", action = "flag")
ca$add_argument("bar", action = "flag")
ca$get_input()
#> [1] "foo" "bar"
ca$parse()
#> $foo
#> [1] TRUE
#> 
#> $bar
#> [1] TRUE

# fails after ca is created
ca <- command_args()
ca$set_input(1:2, c("foo", "bar"))
#> Initial call:  foo bar
#> w Call $resolve() or $parse() to resolve arguments
ca$add_argument("foo", action = "flag")
ca$add_argument("bar", action = "flag")
ca$get_input()
#> [1] "foo" "bar"
ca$parse()
#> $foo
#> [1] FALSE
#> 
#> $bar
#> [1] FALSE

Created on 2022-12-09 with reprex v2.0.2

improve `arg` pass through

Should be able to let one arg be dependent on another

library(scribe)
ca <- command_args("-a")
arg <- new_arg("-a", action = "flag")
ca$add_argument(arg)
ca$add_argument("-b", default = arg)
args <- ca$parse()
print(args)
#> $a
#> [1] TRUE
#> 
#> $b
#> [1] TRUE

early stop args

Include parameters that stop the parsing of additional further arguments.

# use default values
ca <- command_args(c("--bar", 2))
ca$add_argument("--bar", default = 1)
ca$add_argument("--foo", action = "flag", stop = "soft") # or TRUE
ca$parse()
#> $bar
#> [1] 1
#> $foo
#> [1] TRUE

# strip out other values
ca <- command_args(c("--bar", 2))
ca$add_argument("--bar", default = 1)
ca$add_argument("--foo", action = "flag", stop = "hard")
ca$parse()
#> $foo
#> [1] TRUE

improve $get_names()

shouldn't have to use -- for names

library(scribe)
ca <- command_args("one")
ca$add_argument("--foo")
ca$add_argument("--bar")
ca$add_argument("...", "--one", default = character())
ca$parse()
#> $foo
#> NULL
#> 
#> $bar
#> NULL
#> 
#> $one
#> [1] "one"

ca <- command_args("one")
ca$add_argument("foo")
ca$add_argument("bar")
ca$add_argument("...", "--one", default = character())
try(ca$parse())
#> Error in vapply(args, function(arg) arg$get_name(), NA_character_) : 
#>   values must be length 1,
#>  but FUN(X[[1]]) result is length 0

Created on 2022-12-05 with reprex v2.0.2

clean up todos

mark::todos()
#> Found 7 TODO:
#>       ./R/class-args.R
#> [ 73] include validation
#> [150] need to determine when actions can be anything other than list
#> [249] add print_scribe.arg
#>       ./R/class-command-args.R
#> [  2] use `args`, `argsList`, `params` consistently as names
#> [202] just set `include` to character()?
#> [289] pad examples based on comments
#>       ./tests/testthat/test-class-command-args.R
#> [290] potentially some issue with the "{scribe}" in the output confusing

Created on 2023-03-04 with reprex v2.0.2

positional args

Include position args. I think as long as they are adjacent, it should be fine:

ca <- command_args(c(1, 2, 3))
ca$add_argument("foo", position = TRUE)
ca$add_argument("bar", position = TRUE)
ca$add_argument("fun", position = TRUE)
ca$parse()
#> $foo
#> [1] 1
#> 
#> $bar
#> [1] 2
#>
#> $fun
#> [1] 3

would be the same as

ca <- command_args()
ca$add_argument("foo", position = 1L)
ca$add_argument("bar", position = 2L)
ca$add_argument("fun", position = 3L)

then we could do

my-file character 3

on

#? /usr/bin/env -S Rscript --vanilla
library(script)
ca <- command_args()
ca$add_argument("mode", position = TRUE, type = character())
ca$add_argument("length", position = TRUE, type = integer())
args <- ca$parse()
do.call(vector, args)

and get

[1] "" "" ""

improve help

improve help documentation

should automatically go for ca_help() when "--help" is listed in commandArgs()

ca <- command_args("--help", description = "a neat little thing I'm doing")
ca$add_argument("foo", help = "foobar")
ca$add_argument("-v", "--verbose", action = "flag", help = "triggers verbose messages")
ca$add_argument("...", help = "list of cool things")
ca$add_argument("var", n = 3, help = "your top 3 things")
ca$add_argument("-a", "arg", help = "a single argument to use")
ca$parse()

#> {scribe} command_args
#> 
#> file : ...
#> 
#> DESCRIPTION
#>  a neat little thing I'm doing
#> 
#> USAGE
#>   file [--help] [--version]
#>   file [--verbose] [var [..3]] [--arg [ARG]] [...]
#> 
#> FLAGS
#>   --help           show this message then, quietly stop
#>   --version        show the {scribe} package version, then quietly stop
#>   -v, --verbose    triggers verbse messages
#> 
#> OPTIONS
#>   foo              foobar
#>   var [..3]        your top 3 things
#>   -a, --arg [ARG]  a single argument to use
#> 

maybe play with commandArgs()[1] to print out a file location?

handle n better

this should return list(values = c(1L, 2L))

library(scribe)
ca <- command_args(c("--values", "1", "2"))
ca$add_argument("--values", n = 2)
ca$parse()
#> Warning: Not all values parsed:
#> 2
#> $values
#> [1] 1

Created on 2022-12-09 with reprex v2.0.2

`--help` fails when `length(info) > 1`

probably just needs a paste(collapse)

library(scribe)

ca <- command_args("--help")
ca$add_argument("bad", info = c("one", "two"))
ca$parse()
#> Error in apply(lines, 1L, format): dim(X) must have a positive length

Created on 2023-05-01 with reprex v2.0.2

return parsed values in order they are assigned

and also fix the dots here

library(scribe)
ca <- command_args(c("-b", "one", "-c", "two", "-a", "three", "foo", "bar"))
ca$add_argument("...")
ca$add_argument("-a", default = "zero")
ca$add_argument("-b", default = "zero")
ca$add_argument("-c", default = "zero")
ca$parse()
#> $a
#> [1] "zero"
#> 
#> $b
#> [1] "zero"
#> 
#> $c
#> [1] "two"
#> 
#> $...
#> [1] "-b"    "one"   "-a"    "three" "foo"   "bar"

Created on 2022-12-18 with reprex v2.0.2

update conversions

Some simple conversions -- though recommended to use simple types.

convert <- function(x, to = default) {
  if (is.null(to)) {
    return(x)
  }

  if (is.factor(to)) {
    # special case for factors because they are annoying
    to <- function(x) {
      factor(x, levels = levels(to), ordered = is.ordered(to))
    }
  }

  if (is.function(to)) {
    to <- match.fun(to)
    return(to(x))
  }

  typeof(x) <- typeof(to)
  attribute(x) <- attributes(to)
  class(x) <- class(to)
  x
}

bool <- function(x) {
  if (is.logical(x)) {
    return(x)
  }

  out <- rep_len(NA, length(x))
  out[tolower(x) %in% c("y", "yes", "t", "true", "1")] <- TRUE
  out[tolower(x) %in% c("n", "no", "f", "false", "0")] <- FALSE
  out
}

default <- function(x) {
  out <- utils::type.convert(x, as.is = TRUE)

  # only handles defaults
  if (is.character(out)) {
    if (all(tolower(out) %in% c("true", "false", "na", "t", "f"))) {
      out <- as.logical(out)
    } else {
      ok <- !is.na(out)
      dates <- suppressWarnings(as.POSIXct(x[ok], optional = TRUE))
      if (!anyNA(dates)) {
        out <- rep(as.POSIXct(NA), length(ok))
        out[ok] <- dates
      }
    }
  }

  out
}

x <- c("yes", "no", "TRUE", "1", "what", "2")
data.frame(x = x, bool = bool(x))
#>      x  bool
#> 1  yes  TRUE
#> 2   no FALSE
#> 3 TRUE  TRUE
#> 4    1  TRUE
#> 5 what    NA
#> 6    2    NA

str(convert(c("true", "false")))
#>  logi [1:2] TRUE FALSE
str(convert(c("1", "0", "-2")))
#>  int [1:3] 1 0 -2
str(convert(c("2022-01-01", "NA")))
#>  POSIXct[1:2], format: "2022-01-01" NA
str(convert(c("2022-01-01", "NA", "a")))
#>  chr [1:3] "2022-01-01" NA "a"

Created on 2022-12-02 with reprex v2.0.2

add example field to `scribeCommandArgs`

Should be able ot include examples and append to description

library(scribe)
ca <- command_args()
ca$add_argument("foo")
ca$add_description("stuff here")
# add examples one-by-one; parameters for appending `"$ "` or `"> "` or `""`
# multiple examples should have an option for aligning?
ca$add_example("commamd value", "single value")
ca$add_example("command value value", "two values")
ca$help()
#> {scribe} command_args
#> 
#> file : {path}
#> 
#> DESCRIPTION
#>   stuff here
#> 
#> USAGE
#>   {command} [--help | --version]
#>   {command} [--help] [--version] [foo [ARG]] 
#> 
#> ARGUMENTS
#>   --help    : prints this and quietly exits                   
#>   --version : prints the version of {scribe} and quietly exits
#>   foo [ARG] : help information for foo  
#> 
#> EXAMPLES
#>   $ command value       # single value
#>   $ command value value # two values

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.