Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

error-safe templating with brew / whisker

Tags:

r

templates

An external program needs an input file with some control parameters, and I wish to generate those automatically using R. Usually, I simply use paste("parameter1: ", param1, ...) to create the long string of text, and output to a file, but the script rapidly becomes unreadable. This problem is probably well suited to whisker,

library(whisker)

template= 'Hello {{name}}
You have just won ${{value}}!
'

data <- list( name = "Chris", value= 124)

whisker.render(template, data)

My issue here, is that there's no safe-checking that data contains all the required variables, e.g.

whisker.render(template, data[-1])

will silently ignore the fact that I forgot to specify a name. My end-program will crash, however, if I fail to produce a complete config file.

Another templating system is provided by brew; it has the advantage of actually evaluating things, and potentially this can also help detect missing variables,

library(brew)

template2 = 'Hello <%= name %>
You have just won $<%= value %>!
'

data <- list( name = "Chris", value= 124)

own_brew <- function(template, values){
  attach(values, pos=2)
  out = capture.output(brew(text = template)) 
  detach(values, pos=2)
  cat(out, sep='\n')
  invisible(out)
}

own_brew(template2, data)
own_brew(template2, data[-1]) # error

However, I am stuck with two issues:

  • attach() ... detach() is not ideal, (gives warnings every now and then), or at least I don't know how to use it properly. I tried to define an environment for brew(), but it was too restrictive and didn't know about base functions anymore...

  • even though an error occurs, a string is still returned by the function. I tried to wrap the call in try() but I have no experience in error handling. How do I tell it to quit the function producing no output?

Edit: I have updated the brew solution to use a new environment instead of attach(), and stop execution in case of an error. (?capture.output suggests that it was not the right function to use here, since "An attempt is made to write output as far as possible to file if there is an error in evaluating the expressions"...)

own_brew <- function(template, values, file=""){
  env <- as.environment(values)
  parent.env(env) <- .GlobalEnv 
  a  <-  textConnection("cout", "w")
  out <- try(brew(text = template, envir=env, output=a))

  if(inherits(out, "try-error")){
    close(a)
    stop()
  }
  cat(cout, file=file, sep="\n")
  close(a) 
  invisible(cout)
}

There must be an easier way with tryCatch, but I can't understand a single thing in its help page.

I welcome other suggestions on the more general problem.

like image 884
baptiste Avatar asked May 16 '13 19:05

baptiste


3 Answers

Using regular expressions to retrieve the variable names from the template, you could validate before rendering, e.g.,

render <- function(template, data) {
    vars <- unlist(regmatches(template, gregexpr('(?<=\\{\\{)[[:alnum:]_.]+(?=\\}\\})', template, perl=TRUE)))
    stopifnot(all(vars %in% names(data)))
    whisker.render(template, data) 
}

render(template, data)
like image 186
Matthew Plourde Avatar answered Oct 22 '22 07:10

Matthew Plourde


The new glue package provides another alternative,

library(glue)
template <- 'Hello {name} You have just won ${value}!'
data <- list( name = "Chris", value= 124)

glue_data(template, .x=data)
# Hello Chris You have just won $124!

glue_data(template, .x=data[-1])
# Error in eval(expr, envir, enclos) : object 'name' not found
like image 26
baptiste Avatar answered Oct 22 '22 06:10

baptiste


While preparing the stringr answer I noticed that OP's questions concerning the usage of brew() hasn't been addressed so far. In particular, the OP was asking how to provide his data to the environment and how to prevent a character string being returned in case of an error.

The OP has created a function own_brew() which wraps the call to brew(). Although, there are alternative package available now, I feel the original question deserves an answer.

This is my attempt to improve baptiste's version:

own_brew <- function(template, values, file=""){
  a  <-  textConnection("cout", "w")
  out <- brew::brew(text = template, envir=list2env(values), output=a)
  close(a)
  if (inherits(out, "try-error")) stop()
  cat(cout, file=file, sep="\n")
  invisible(cout)
}

The main differences are that list2env() is used to pass the list of values to brew() and that the call to try() is avoided by testing the return value out for an error.

template <- "Hello <%= name %> You have just won $<%= value %>!"
data <- list( name = "Chris", value= 124)

own_brew(template, data)
Hello Chris You have just won $124!
own_brew(template, data[-1L])
Error in cat(name) : object 'name' not found
Error in own_brew(template, data[-1L]) :
like image 36
Uwe Avatar answered Oct 22 '22 07:10

Uwe