【发布时间】:2023-02-08 00:53:06
【问题描述】:
对于我的包,我正在寻找一种方法来识别用户提供的R脚本中的所有函数分配,没有执行它。
因此,让我们假设以下示例脚本 (ex.R):
ex.R
## user supplied script
a <- function(x) 1
b1 = b2 <- function() {
y <- 1
2 -> j
j
}
d <<- function(x) {
k <- function(l) 1
k(x)
}
(function(x) 2) -> e
(function() {
y <- 1
2 -> j
j
}) -> f1 -> f2
(function() 1)()
g <- 4
5 -> h
i <- lapply(1:3, FUN = function(x) x + 1)
assign('j', function() 1)
k1 <- (function() {1}) -> k2
该函数应返回c("a", "b1", "b2", "d", "e", "f1", "f2", "j", "k1", "k2")
我想出了以下解决方案:
library(dplyr)
code <- parse("ex.R")
get_identifier <- function(nm) {
if (is.symbol(nm)) {
deparse(nm)
} else if (is.character(nm)) {
nm
} else {
stop(paste0("unknown type", typeof(nm), "for `nm`"))
}
}
get_fns <- function(expr) {
assignment_ops <- c("<-", "=", "assign", "<<-")
is_assign <- deparse(expr[[1L]]) %in% assignment_ops
is_call <- is_assign && is.call(expr[[3L]])
if (is_call) {
next_call <- deparse(expr[[3L]][[1L]])
if (next_call == "function") {
get_identifier(expr[[2L]])
} else if (next_call %in% c(assignment_ops, "(")) {
c(get_identifier(expr[[2L]]), Recall(expr[[3L]]))
} else {
NULL
}
} else {
NULL
}
}
unlist(lapply(code, get_fns))
# [1] "a" "b1" "b2" "d" "e" "f2" "f1" "j" "k1" "k2"
这至少对于这个用例是正确的。
但是只添加另外两个讨厌的边缘情况会破坏代码:
l1 <- (1 + (l2 <- function(x) 2 * x)(3))
(m <- function(x) x)
应该返回c("l2", "m"),但它没有。我的递归有问题,但我无法发现问题所在。我将如何修复代码?
更新
评论表明我应该解释一下我最终想要实现的目标:
- 我想开发一个包,它采用“任意”R 脚本(
script.Rsay)并将此脚本转换为具有命令行界面的脚本(script_ammended.Rsay),最终可以调用通过Rscript ammended_script.R [ARGS]。 - 想法是用户脚本包含一些函数和一些特殊的 cmets,并通过这些函数自动生成 CLI。
- 我知道有几个库已经可以进行不错的命令行解析,但是所有这些库当然都需要用户花一些时间进行 CLI 编程。
- 我的用例有些不同。我想要一个独立的脚本,它只使用一些函数来完成它想要做的事情。如果用户以后想用它创建一个 CL 工具,应该就像按一个按钮一样简单(假设用户向原始功能添加了一些最小的 cmets)。
- 自动生成的脚本将始终添加额外的代码、记录、确保安装所需的库等。
一个人为的例子可能是这样的:
script.Rgreet <- function(msg, from = "me") { #! short: -g #! params: [., -f] #! description: greeting <msg> from user <me> is shown print(paste0("Message from <", from, ">: ", msg)) } bye <- function() { #! short: -b greet("Good Bye", "system") } greet("Test")这将是一个典型的用户脚本,可以非常方便地以交互方式使用。现在,我的包应该采用此脚本并将其转换为以下脚本:
script_amended.Rlibrary(optigrab) greet <- function(msg, from = "me") { print(paste0("Message from <", from, ">: ", msg)) } bye <- function() { greet("Good Bye", "system") } msg <- opt_get("g", default = NA_character_, description = "greeting <msg> from user <me> is shown") from <- opt_get("f", default = "me") bye_flag <- opt_get("b", default = FALSE) if (!is.na(msg)) { greet(msg, from) quit(status = 0) } else if (bye_flag) { bye() quit(status = 0) }
【问题讨论】:
-
不能简单的在封闭环境下执行脚本,返回
function模式的对象名吗?您的代码会遗漏许多其他“令人讨厌的”边缘情况。 (在我脑海中:list2env、eval、source、load、getfromNamespace、Rcpp::cppFunction)。要成为一个完整的解决方案,您需要编写一个 R 解析器。幸运的是,您已经有了一个——R 本身。如果你想覆盖,你应该使用它全部基地。 -
很确定有一个包可以提取所有变量并制作网络图,这可能很有用,但不记得名字了。
-
如果你的代码能够涵盖所有情况可能会更好,但我从未遇到过至少 50% 的这些语法,而且
l1的定义方式似乎非常令人费解。出于好奇,你见过这样的代码吗? -
也许添加正则表达式标签?
-
感谢 cmets,我完全意识到如何定义函数有无数种可能性,我不想涵盖所有这些(因为我不想编写新的 R 解析器)。我将在帖子中添加一些背景信息,以解释我最终的目的。
标签: r parsing metaprogramming rlang