Evaluate expression given as a string
Asked Answered
P

8

366

I'm curious to know if R can use its eval() function to perform calculations provided by e.g. a string.

This is a common case:

eval("5+5")

However, instead of 10 I get:

[1] "5+5"

Any solution?

Personally answered 16/11, 2009 at 17:39 Comment(5)
Despite all the answers showing how to solve that with parse ... Why do you need to store language types in a character string ? Martin Mächler's answer should deserve much more upvotes.Accountancy
Thank you @PetrMatousu. Yes, I'm shocked to see how mis-information is spread on SO now.. by people upvoting eval(parse(text = *)) fake solutions.Mizzen
I want to run scrips of the form: QQ = c('11','12','13','21','22','23'), i.e.: QQ =c(...,'ij',..) with i,j varying on a range that is may vary from run to run. For this and similar examples, I can write the script as paste( "QQ = c('", paste(rep(1:2,each=3),1:3, sep="", collapse="','"), "')",sep=""), and the option eval(parse(text=...)) creates the vector QQ in the working environment as per the script. What would be the proper R coder way to do this, if not with "text=..."?Budworth
@MartinMächler how is eval(parse(text = "5+5")) a "fake solution"? It seems to work fine for all the cases I have tried. There are reasons that one might need to evaluate something read-in as a string. I am finding your answer more confusing and less useful to evaluating a string (OP) than the others, which might be why the other answers have more upvotes?Architecture
@VictorZurkowski: With your example, written in a "generalizable way", i <- rep(1:2, each=3) ; j <- 1:3 the proper solution is (many times faster and more readable) QQ <- paste0(i, j) -- voilà, that's all.Mizzen
G
521

The eval() function evaluates an expression, but "5+5" is a string, not an expression. Use parse() with text=<string> to change the string into an expression:

> eval(parse(text="5+5"))
[1] 10
> class("5+5")
[1] "character"
> class(parse(text="5+5"))
[1] "expression"

Calling eval() invokes many behaviours, some are not immediately obvious:

> class(eval(parse(text="5+5")))
[1] "numeric"
> class(eval(parse(text="gray")))
[1] "function"
> class(eval(parse(text="blue")))
Error in eval(expr, envir, enclos) : object 'blue' not found

See also tryCatch.

Gerdes answered 16/11, 2009 at 17:55 Comment(5)
As Shane notes below, "You need to specify that the input is text, because parse expects a file by default"Honor
the side-effects of using eval(parse) should be specified. For example, if you have a pre-defined variable name equal to "David" and you reassign using eval(parse(text = "name") == "Alexander", you will get an error because eval & parse do not return an R expression that can be evaluated.Territory
@NelsonGon: Unevaluated expressions constructed using quote(), bquote(), or the more sophisticated tools provided by the rlang package.Mansour
@ArtemSokolov Thanks, I somehow keep coming back to this question looking for an alternative. I've looked at rlang but the closest I found was parse_expr which calls parse_exprs which in turn is the same as using parse and wrapping it in eval which seems to be the same thing as done here. I am unsure what the advantage would be of using rlang.Stringfellow
@NelsonGon: with rlang, you would work directly with expressions, not strings. No parse step necessary. It has two advantages. 1. Expression manipulations will always produce valid expressions. String manipulations will only produce valid strings. You won't know if they are valid expressions until you parse them. 2. There's no equivalent to the substitute() class of functions in the string world, which severely limits your ability to manipulate function calls. Consider this glm wrapper. What would a string equivalent look like?Mansour
P
120

You can use the parse() function to convert the characters into an expression. You need to specify that the input is text, because parse expects a file by default:

eval(parse(text="5+5"))
Pectoral answered 16/11, 2009 at 17:47 Comment(2)
> fortunes::fortune("answer is parse") If the answer is parse() you should usually rethink the question. -- Thomas Lumley R-help (February 2005) >Mizzen
@MartinMächler That's ironic, because the core R packages use parse all the time! github.com/wch/r-source/…Leatherneck
B
67

Sorry but I don't understand why too many people even think a string was something that could be evaluated. You must change your mindset, really. Forget all connections between strings on one side and expressions, calls, evaluation on the other side.

The (possibly) only connection is via parse(text = ....) and all good R programmers should know that this is rarely an efficient or safe means to construct expressions (or calls). Rather learn more about substitute(), quote(), and possibly the power of using do.call(substitute, ......).

fortunes::fortune("answer is parse")
# If the answer is parse() you should usually rethink the question.
#    -- Thomas Lumley
#       R-help (February 2005)

Dec.2017: Ok, here is an example (in comments, there's no nice formatting):

q5 <- quote(5+5)
str(q5)
# language 5 + 5

e5 <- expression(5+5)
str(e5)
# expression(5 + 5)

and if you get more experienced you'll learn that q5 is a "call" whereas e5 is an "expression", and even that e5[[1]] is identical to q5:

identical(q5, e5[[1]])
# [1] TRUE
Beghtol answered 20/10, 2016 at 20:38 Comment(11)
could you give an example? maybe you could show us how to "hold on" to 5+5 in an r object, then evaluate it later, using quote and substitute rather than a character and eval(parse(text=)?Continuance
I may be a little lost. At what point do you get 10? Or is that not the point?Jehias
@RichardDiSalvo: yes, q5 <- quote(5+5) above is the expression (actually the "call") 5+5 and it is an R object, but not a string. You can evaluate it any time. Again: using, quote(), substitute(), ... instead parse creates calls or expressions directly and more efficiently than via parse(text= . ). Using eval() is fine, using parse(text=*) is error prone and sometimes quite inefficient in comparison to construction calls and manipulating them.. @Nick S: It's eval(q5) or eval(e5) in our running exampleMizzen
@NickS : To get 10, you evaluate the call/expression, i.e., call eval(.) on it. My point was that people should not use parse(text=.) but rather quote(.) etc, to construct the call which later will be eval()ed.Mizzen
eval(quote()) does work in a few cases but will fail for some cases where eval(parse()) would work well.Stringfellow
I don't understand this solution still. I have a dataframe where one of the columns is a fraction (ie, "73/873") that's been stored as a string. I need to turn this into a decimal so I can sort/graph the data. When I try to use the included solutions, I can't get reference the variables correctly. So df[[1,1]] becomes "73/873", but quote(df[[1,1]]) simply produces "df[1,1]"Sonja
Let me give you a use case. If I want to join 80 data frames, whose names I get from vcdExtra::datasets as strings, then what is a better way than this: ``` data <- vcdExtra::datasets("dbdataset") data$Item %>% lapply( function(i) i %>% parse(text = .) %>% eval()) %>% Reduce(function(x, y) merge(x, y), .)```Laszlo
eval(quote("5+10*2")) returns "5+10*2" whereas eval(parse(text="5+10*2")) returns 25. In this case, quote doesn't seem to be the better function to use here...Architecture
@Dylan_Gomes, @NelsonGon: I did not say, that quote() works when you give it a string. You should not have strings at all, when you need expressions or calls. Yes, sometimes your input does consist of strings, and you must parse these one way or the other, to turn them into expressions or calls which then you can evaluate. Using packages such as rlang or lazy_eval do the same thing: Always two steps: 1. parse (which does "string" -> expression) 2. eval (expression -> "value").Mizzen
@MartinMächler thanks for the clarification. It seems that this doesn't exactly answer the OP then. I understand that you are saying not to use strings at all, but one doesn't always have a choice. If you are handed a dataset that has character strings of equations, how does one evaluate them? There must be an acceptable way to do this, no?Architecture
@Dylan_Gomes: Yes, as I say above, if you really get string data, you are "out of luck" (because parsing is necessarily somewhat error prone), and you need 1. parse, 2. eval, which indeed you can combine in one eval(parse(text= * )) (or any other variants of that).Mizzen
T
37

Not sure why no one has mentioned two Base R functions specifically to do this: str2lang() and str2expression(). These are variants of parse(), but seem to return the expression more cleanly:

eval(str2lang("5+5"))

# > 10
  
eval(str2expression("5+5"))

# > 10

Also want to push back against the posters saying that anyone trying to do this is wrong. I'm reading in R expressions stored as text in a file and trying to evaluate them. These functions are perfect for this use case.

Trehala answered 20/9, 2020 at 0:47 Comment(1)
It's not that it's always wrong, it's just that there are many, many cases where it's safer and better to do things in a different way.Collage
E
21

Nowadays you can also use lazy_eval function from lazyeval package.

> lazyeval::lazy_eval("5+5")
[1] 10
Etruscan answered 4/2, 2018 at 0:40 Comment(0)
C
20

Alternatively, you can use evals from my pander package to capture output and all warnings, errors and other messages along with the raw results:

> pander::evals("5+5")
[[1]]
$src
[1] "5 + 5"

$result
[1] 10

$output
[1] "[1] 10"

$type
[1] "numeric"

$msg
$msg$messages
NULL

$msg$warnings
NULL

$msg$errors
NULL


$stdout
NULL

attr(,"class")
[1] "evals"
Cacuminal answered 12/10, 2015 at 3:46 Comment(2)
Nice function; fills a hole left by evaluate::evaluate by actually returning the result object; that leaves your function suitable for use for calling via mclapply. I hope that feature remains!Advowson
Thank you, @rpierce. This function was originally written in 2011 as part of our rapport package, and have been actively maintained since then as being heavily used in our rapporter.net service besides a few other projects as well -- so I'm sure it will remain maintained for a while :) I'm glad you find it useful, thanks for your kind feedback.Cacuminal
J
11

Similarly using rlang:

eval(parse_expr("5+5"))
Jilt answered 13/4, 2019 at 0:42 Comment(3)
Came here looking for an rlang answer but what if any is the advantage of this over base alternatives? Actually, close examination of the code used shows that it is in fact using eval(parse(....)) which I wanted to avoid.Stringfellow
Not only those negatives, but its name is also misleading. It is NOT evaluating an expression. Should be called parse_to_expr ot something else to indicate that the user will know that it intended for character arguments.Gravettian
It's just a wrapper that handles seamlessly any type of input (character, list, connection)Moment
S
2

I agree there are concerns around eval and parse, but if the expression is in a string, there appears nothing much that can be done. This eval parse is also used in the glue package by the tidyverse experts, see https://github.com/tidyverse/glue/blob/d47d6c7701f738f8647b951df418cfd5e6a7bf76/R/transformer.R#L1-L12

Perhaps the accepted answer of eval(parse(text="5+5")) has security concerns if the text string is from an untrusted source, eg imagine user_input = "list.files()" or worse file.remove...

One potential work around is below.

The idea is to set the R environment in which the expression is to be evaluated. In R, most functions that 'comes with R' are actually in packages that gets autoloaded at R start up, eg 'list.files', 'library' and 'attach' functions come from the 'base' package. By setting the evaluation environment to empty environment, these functions are no longer available to the expression to be evaluated, preventing malicious code from executing. In the code below, by default I include only arithmetic related functions, otherwise user can provide the evaluation environment with explicitly allowed functions.

eval_text_expression <- function(text_expression, data_list, eval_envir = NULL) {
  # argument checks
  stopifnot(is.character(text_expression) && length(text_expression) == 1)
  stopifnot(is.list(data_list))
  stopifnot(length(data_list) == 0 || (!is.null(names(data_list)) && all(names(data_list) != "")))
  stopifnot(all(!(lapply(data_list, typeof) %in% c('closure', 'builtin'))))
  stopifnot(is.null(eval_envir) || is.environment(eval_envir))
  # default environment for convenience 
  if (is.null(eval_envir)) {
    arithmetic_funcs <- list("+" = `+`, "-" = `-`, "*" = `*`, "/" = `/`, "^" = `^`, "(" = `(`)
    eval_envir = rlang::new_environment(data = arithmetic_funcs, parent = rlang::empty_env())
  }
  # load data objects into evaluation environment, then evaluate expression
  eval_envir <- list2env(data_list, envir = eval_envir)
  eval(parse(text = text_expression, keep.source = FALSE), eval_envir)
}

eval_text_expression("(a+b)^c - d", list(a = 1, b = 2, c = 3, d = 4))
# [1] 23
eval_text_expression("list.files()", list())
# Error in list.files() : could not find function "list.files"
eval_text_expression("list.files()", list(), eval_envir = rlang::new_environment(list("list.files" = list.files)))
# succeeds in listing my files if i explicitly allow it

Sofia answered 6/7, 2022 at 1:6 Comment(2)
That looks great, but I have a little trouble understanding how it works. Could you please explain it a bit how the solution addresses the security concerns? That would make the answer more meaningful. Thanks.Nonresident
edited with explanationsSofia

© 2022 - 2024 — McMap. All rights reserved.