确定正在执行的脚本的路径

300

我有一个名为foo.R的脚本,它包括另一个脚本other.R,两个脚本位于同一个目录中:

#!/usr/bin/env Rscript
message("Hello")
source("other.R")

但是我希望R能够找到那个other.R,无论当前的工作目录是什么。

换句话说,foo.R需要知道它自己的路径。我该如何做到这一点?


2
没有 :( 我还没有看到任何真正有效的解决方案。除了通过传递目录或使用环境变量的变通方法。 - Frank
4
让脚本完全可移植且甚至 R 初学者也能执行,这将是非常惊人的! - Etienne Low-Décarie
5
似乎所有答案都需要你在某个时刻输入路径(至少要定位文件)!如果你能发送一个压缩文件夹,让任何运行该文件夹内的R脚本文件都从中读取并保存到其中,那将是很好的。 - Etienne Low-Décarie
17
这个单一问题实际上可能成为我完全转向Python的原因。 - Giacomo
9
@giac_man,我觉得R充满了成百上千个这样微小的问题,它们相互累加,使得工作变得非常困难。 - Michael Barton
显示剩余5条评论
31个回答

117

这里有一个解决问题的简单方法。 这个命令:

script.dir <- dirname(sys.frame(1)$ofile)

返回当前脚本文件的路径。在脚本保存后有效。


9
无法运行,我在Windows上运行R。有什么建议吗? - Ehsan88
5
使用保存的脚本并在Windows上安装和运行R 3.2.0时,出现了相同的错误... - RalfB
35
当你尝试直接从Rstudio执行dirname(sys.frame(1)$ofile)时会出现此错误。当使用source("other.R")执行脚本,且dirname(sys.frame(1)$ofile)在"other.R"中时,它可以正常工作。 - Murta
13
当我使用rscript.exe作为脚本调用时,而不是使用source()函数时,我遇到了“堆栈上没有那么多帧”的错误。因此,我不得不使用Suppressingfire提供的解决方案。 - Mark Adamson
8
当我在使用shiny时将其放入server.R中时,出现了NULL的错误。 - Paul
显示剩余10条评论

83
您可以使用commandArgs函数获取由Rscript传递给实际R解释器的所有选项,并搜索其中的--file=。如果您的脚本从路径启动或者是使用完整路径启动的,下面的script.name将以'/'开头。否则,它必须相对于cwd,您可以连接这两个路径以获取完整路径。 编辑:听起来你只需要上面的script.name并剥离路径的最后一部分。我已经删除了不必要的cwd()示例并清理了主要脚本,并发布了我的other.R。只需将此脚本和other.R脚本保存到同一目录中,chmod +x它们,然后运行主脚本。 main.R:
#!/usr/bin/env Rscript
initial.options <- commandArgs(trailingOnly = FALSE)
file.arg.name <- "--file="
script.name <- sub(file.arg.name, "", initial.options[grep(file.arg.name, initial.options)])
script.basename <- dirname(script.name)
other.name <- file.path(script.basename, "other.R")
print(paste("Sourcing",other.name,"from",script.name))
source(other.name)

other.R:

print("hello")

输出:

burner@firefighter:~$ main.R
[1] "Sourcing /home/burner/bin/other.R from /home/burner/bin/main.R"
[1] "hello"
burner@firefighter:~$ bin/main.R
[1] "Sourcing bin/other.R from bin/main.R"
[1] "hello"
burner@firefighter:~$ cd bin
burner@firefighter:~/bin$ main.R
[1] "Sourcing ./other.R from ./main.R"
[1] "hello"

我认为这就是dehmann正在寻找的东西。


3
我下调了评分,因为你的技巧在我看来不能满足使用“source”的需求,但也可能是我误读了提问者的要求。但是,我无法取消下调评分 :( 对不起! - hadley
1
但实际上,它与源代码一起很好地工作!只需使用source(other.name),它就可以正常工作。 - Suppressingfire
我认为我们可能在误解对方的意思。我认为我们对Dehmann感兴趣的事情有不同的理解。 - Suppressingfire
3
对于路径拼接,最好使用 other.name <- file.path(script.basename, "other.R") - Jason
2
当我在shiny应用程序的server.R中尝试运行commandArgs(trailingOnly = FALSE)时,我得到了[1] "RStudio" "--interactive"。没有关于调用它的目录的信息。 - Paul
显示剩余2条评论

61

当我在R控制台中使用“source”时,无法让Suppressingfire的解决方案起作用。
当我使用Rscript时,无法让hadley的解决方案起作用。

两全其美?

thisFile <- function() {
        cmdArgs <- commandArgs(trailingOnly = FALSE)
        needle <- "--file="
        match <- grep(needle, cmdArgs)
        if (length(match) > 0) {
                # Rscript
                return(normalizePath(sub(needle, "", cmdArgs[match])))
        } else {
                # 'source'd via R console
                return(normalizePath(sys.frames()[[1]]$ofile))
        }
}

7
我喜欢这个程序因为它可以在 R 中的 Rscriptsource() 命令中都能使用。我建议对两个版本都使用 normalizePath() 函数,这样可以在两种情况下都给出完整路径。 - wch
1
这是唯一有效的方法。注意,为了使其工作,我花了一些时间才弄清楚需要使用 library(base)。哈哈 - O.rka
2
您先生得到了我的投票,因为这是对我有效的解决方案。 - Vince W.
2
如果有人需要帮助,对于原始帖子来说,在foo.R中意味着source(file.path(dirname(thisFile()), "other.R"))。这对我很有效。 - Kim
一个问题。假设在RStudio中我source main.R,它又source helper.R,然后调用thisFile()。它将获取main.R的路径而不是helper.R。这里有什么提示吗? - Wassadamo
在RStudio/Windows中,这对我不起作用,从sys.frames()[[1]]$ofile中我得到了NULL - understorey

38
frame_files <- lapply(sys.frames(), function(x) x$ofile)
frame_files <- Filter(Negate(is.null), frame_files)
PATH <- dirname(frame_files[[length(frame_files)]])

不过别问我它是如何工作的,因为我已经忘记了 :/


3
在什么情况下可以使用这个?当我运行print(sys.frames())时,结果是NULL。 - Suppressingfire
1
@Suppressingfire:sys.frames 返回调用栈的环境,所以只有从函数中调用时才真正有意义。例如,尝试 foo <- function() {bar <- function() print(sys.frames()); bar()}; foo()。但我无法理解 @hadley 的代码,因为环境没有 ofile 成员。 - Richie Cotton
1
你必须在源文件中引用它 - 例如,如果我保存了那段代码,然后运行 source("~/code/test.r")PATH 将被设置为 ~/desktop。如果你只是在顶层评估它,它将返回 NULL。 - hadley
4
这并没有回答我的问题。我需要自动找到"other.R"文件。由于x$ofile未定义,因此frame_files为空。 - Frank
这个在Windows里面可以用,也在Linux里面可以用,但是当我从crontab调用它的时候却失败了。很伤心! - geneorama
显示剩余3条评论

35

这对我有效

library(rstudioapi)    
rstudioapi::getActiveDocumentContext()$path

7
我猜这只能在RStudio内部运行。如果在终端中尝试,则会出现“错误:RStudio未运行”的提示。 - Ista
1
更具体地说,如果在R Studio中从R脚本运行它,它会起作用。即使在RStudio控制台中,它也不会给出正确的结果,在我的情况下是"" - Kay
只要您不更改焦点文档,这在Rstudio中交互式运行时是有效的。如果您提交要运行的代码行,然后在它们运行时切换到另一个文档,则会返回到其他文档的路径。 - Patrick

29

rakensi 的回答来自 如何获取 R 脚本的路径 ,在我看来是最正确和真正精彩的。然而,它仍然是一种使用虚拟函数的技巧。我在这里引用它,以便其他人可以更轻松地找到。

sourceDir <- getSrcDirectory(function(dummy) {dummy})

这将给出放置此语句的文件的目录(定义虚拟函数的位置)。然后可以将其用于设置工作目录并使用相对路径。

setwd(sourceDir)
source("other.R")

或者创建绝对路径

 source(paste(sourceDir, "/other.R", sep=""))

2
对我来说,你的解决方案是最好的。特别是因为它可以应用于Shiny应用程序,而链接上的那个则不行。 - jcarlos
1
这里的 getSrcDirectory 是 utils::getSrcDirectory - RubenLaguna
10
在Linux/Mac下可能效果很好,但在Windows下的交互式RStudio会话中,对我来说并没有奏效。sourceDir是空白的。 - Contango
2
@Contango 在交互式终端上,没有路径!!!你想要文件的路径。 - pommedeterresautee
3
我得到了character(0)。有什么建议吗? - abalter
显示剩余2条评论

24
我已经为此制作了一个包,可以在CRAN和GitHub上找到,名为this.path。当前版本是2.3.0(2023-12-02),您可以在这里找到它:

https://CRAN.R-project.org/package=this.path

https://github.com/ArcadeAntics/this.path

从CRAN安装它:
utils::install.packages("this.path")

或者从GitHub安装开发版本:
utils::install.packages("this.path",
    repos = "https://raw.githubusercontent.com/ArcadeAntics/PACKAGES")

然后通过以下方式使用它:
this.path::this.path()

或者:

library(this.path)
this.path()

以下答案是我的原始答案,仅供参考,尽管与上面提供的最新版本相比,它的功能要少得多。改进包括:
  • 与更多GUI的兼容性:Windows上的'Rgui','VSCode','Jupyter'和'Emacs' + 'ESS'
  • 与包compilerboxknitrplumbershinytargetstestthat的兼容性,特别是compiler::loadcmp()box::use()knitr::knit()plumber::plumb()shiny::runApp()testthat::source_file()
  • 在Unix类似系统的shell中运行R脚本时处理带有空格的文件名
  • 处理从shell运行R脚本的两种用法(-f FILE--file=FILE
  • 在使用source()时正确规范化路径参数(chdir = TRUE)
  • 处理source()中的文件URI,例如source("file:///path/to/file")source("file:///C:/path/to/file")
  • 改进处理source()中连接而不是字符字符串的方式
  • 处理source()中的URL路径名,即:
source("https://host/path/to/file")

如果在文件中使用了this.path(),它将返回"https://host/path/to/file"。这也适用于以"http://""ftp://""ftps://"开头的URL。例如,尝试一下:
source("https://raw.githubusercontent.com/ArcadeAntics/this.path/main/tests/this.path_w_URLs.R")
  • 介绍了函数here() / / this.proj(),类似于here::here(),用于指定相对于执行脚本目录/ /执行脚本项目根目录的绝对文件路径
  • 在脚本中首次调用this.path()时,将规范化的路径保存在适当的环境中,这样在同一脚本中后续使用时速度更快,并且与工作目录无关。这意味着setwd()将不再破坏this.path()(只要在该脚本中首次调用this.path()之后使用setwd()

原始答案:

我的答案是对Jerry T的答案的改进。我发现的问题是他们在猜测是否通过检查堆栈上的第一个帧中是否找到变量ofile来进行source()调用。这在嵌套的source调用中不起作用,也不适用于从非全局环境进行的source调用。此外,顺序是错误的。我们必须在检查shell参数之前查找source调用。这是我的解决方案:

this.path <- function (verbose = getOption("verbose"))
{
    ## loop through functions that lead here from most recent to
    ## earliest looking for an appropriate source call (a call to
    ## function source / / sys.source / / debugSource in RStudio)
    ##
    ## an appropriate source call is one in which the file argument has
    ## been evaluated (forced)
    ##
    ## for example, `source(this.path())` is an inappropriate source
    ## call. argument 'file' is stored as a promise containing the
    ## expression `this.path()`. when 'file' is requested,
    ## the expression is evaluated at which time there should be two
    ## functions on the calling stack being 'source' and 'this.path'.
    ## clearly, you don't want to request the 'file' argument from that
    ## source call because the value of 'file' is under evaluation
    ## right now! the trick is to ask if 'file' has already been
    ## evaluated, the easiest way of which is to ask if a variable
    ## exists, one which is only created after the expression is
    ## necessarily evaluated.
    ##
    ## if that variable does exist, then argument 'file' has been
    ## forced and the source call is deemed appropriate. otherwise,
    ## the source call is deemed inappropriate and the 'for' loop
    ## moves to the next function up the calling stack
    ##
    ## unfortunately, there is no way to check the argument 'fileName'
    ## has been forced for 'debugSource' since all the work is done
    ## internally in C. Instead, we have to use a 'tryCatch' statement.
    ## When we evaluate a promise, R is capable of realizing if a
    ## variable is asking for its own definition (a recursive promise).
    ## The error is "promise already under evaluation" which indicates
    ## that the promise is requesting its own value. So we use the
    ## 'tryCatch' to get 'fileName' from the evaluation environment of
    ## 'debugSource', and if it does not raise an error, then we are
    ## safe to return that value. If not, the condition returns false
    ## and the 'for' loop moves to the next function up the calling
    ## stack


    debugSource <- if (.Platform$GUI == "RStudio")
        get("debugSource", "tools:rstudio", inherits = FALSE)
    for (n in seq.int(to = 1L, by = -1L, length.out = sys.nframe() - 1L)) {
        if (identical(sys.function(n), source) &&
            exists("ofile", envir = sys.frame(n), inherits = FALSE))
        {
            path <- get("ofile", envir = sys.frame(n), inherits = FALSE)
            if (!is.character(path))
                path <- summary.connection(path)$description
            if (verbose)
                cat("Source: call to function source\n")
            return(normalizePath(path, "/", TRUE))
        }
        else if (identical(sys.function(n), sys.source) &&
                 exists("exprs", envir = sys.frame(n), inherits = FALSE))
        {
            path <- get("file", envir = sys.frame(n), inherits = FALSE)
            if (verbose)
                cat("Source: call to function sys.source\n")
            return(normalizePath(path, "/", TRUE))
        }
        else if (identical(sys.function(n), debugSource) &&
                 tryCatch({
                     path <- get("fileName", envir = sys.frame(n), inherits = FALSE)
                     TRUE
                 }, error = function(c) FALSE))
        {
            if (verbose)
                cat("Source: call to function debugSource in RStudio\n")
            return(normalizePath(path, "/", TRUE))
        }
    }


    ## no appropriate source call was found up the calling stack


    ## running from RStudio
    if (.Platform$GUI == "RStudio") {


        ## ".rs.api.getSourceEditorContext" from "tools:rstudio"
        ## returns a list of information about the document open in the
        ## current tab
        ##
        ## element 'path' is a character string, the document's path


        context <- get(".rs.api.getSourceEditorContext",
            "tools:rstudio", inherits = FALSE)()
        if (is.null(context))
            stop("R is running from RStudio with no documents open\n",
                 " (or document has no path)")


        path <- context[["path"]]
        if (nzchar(path)) {
            Encoding(path) <- "UTF-8"
            if (verbose)
                cat("Source: document in RStudio\n")
            return(normalizePath(path, "/", TRUE))
        }
        else stop("document in RStudio does not exist")
    }


    ## running from a shell
    else if (.Platform$OS.type == "windows" && .Platform$GUI == "RTerm" ||  ## on Windows
             .Platform$OS.type == "unix"    && .Platform$GUI == "X11")      ## under Unix-alikes
    {


        argv <- commandArgs()
        ## remove all trailing arguments
        m <- match("--args", argv, 0L)
        if (m)
            argv <- argv[seq_len(m)]
        argv <- argv[-1L]


        ## get all arguments starting with "--file="
        FILE <- argv[startsWith(argv, "--file=")]
        ## remove "--file=" from the start of each string
        FILE <- substring(FILE, 8L)
        ## remove strings "-"
        FILE <- FILE[FILE != "-"]
        n <- length(FILE)
        if (n) {
            FILE <- FILE[[n]]
            if (verbose)
                cat("Source: shell argument 'FILE'\n")
            return(normalizePath(FILE, "/", TRUE))
        } else {
            stop("R is running from a shell and argument 'FILE' is missing")
        }
    }


    ## running from RGui on Windows
    else if (.Platform$OS.type == "windows" && .Platform$GUI == "Rgui") {
        stop("R is running from Rgui which is currently unimplemented\n",
             " consider using RStudio until such a time when this is implemented")
    }


    ## running from RGui on macOS
    else if (.Platform$OS.type == "unix" && .Platform$GUI == "AQUA") {
        stop("R is running from AQUA which is currently unimplemented\n",
             " consider using RStudio until such a time when this is implemented")
    }


    ## otherwise
    else stop("R is running in an unrecognized manner")
}

1
直到你评论时,我才意识到可以从'RGui'中的脚本运行代码,之前我认为仅能使用'source'从'RGui'中的脚本运行代码。我正在寻找解决此问题的方法,希望能尽快找到。目前,您可以使用'RStudio'编辑和运行脚本,因为我知道它可以正常工作。非常抱歉无法提供答案,但感谢您指出这个错误! - Iris
2
刚在 RGui 会话中测试了版本为 0.2.0 的脚本文件,对我而言可以正常工作。谢谢! - johnny
1
当我尝试在VSCode中运行它时,我会收到以下错误:Error in this.path() : 'this.path' used in an inappropriate fashion. * no appropriate source call was found up the calling stack. * R is being run from a shell where argument 'FILE' is missing. - coip
1
@Andrew 感谢您的回复并尝试相应地更新您的软件包。正如您所提到的,我安装了GitHub版本并在VSCode中再次尝试。不幸的是,它仍然会出现错误消息。错误消息与我上面写的完全相同,只是Error in this.path():现在变成了Error in this.path(verbose): - coip
1
这应该是正确的答案,至少对我来说是可行的,并且是唯一一个可行的。 - GitHunter0
显示剩余6条评论

19

我的全能工具!(--01/09/2019更新以处理RStudio控制台)

#' current script file (in full path)
#' @description current script file (in full path)
#' @examples
#' works with Rscript, source() or in RStudio Run selection, RStudio Console
#' @export
ez.csf <- function() {
    # https://dev59.com/k3I-5IYBdhLWcg3whIqk#32016824
    cmdArgs = commandArgs(trailingOnly = FALSE)
    needle = "--file="
    match = grep(needle, cmdArgs)
    if (length(match) > 0) {
        # Rscript via command line
        return(normalizePath(sub(needle, "", cmdArgs[match])))
    } else {
        ls_vars = ls(sys.frames()[[1]])
        if ("fileName" %in% ls_vars) {
            # Source'd via RStudio
            return(normalizePath(sys.frames()[[1]]$fileName))
        } else {
            if (!is.null(sys.frames()[[1]]$ofile)) {
            # Source'd via R console
            return(normalizePath(sys.frames()[[1]]$ofile))
            } else {
                # RStudio Run Selection
                # https://dev59.com/fXA75IYBdhLWcg3wJFcL#35842176
                pth = rstudioapi::getActiveDocumentContext()$path
                if (pth!='') {
                    return(normalizePath(pth))
                } else {
                    # RStudio Console
                    tryCatch({
                            pth = rstudioapi::getSourceEditorContext()$path
                            pth = normalizePath(pth)
                        }, error = function(e) {
                            # normalizePath('') issues warning/error
                            pth = ''
                        }
                    )
                    return(pth)
                }
            }
        }
    }
}

无法在交互式 R 会话中使用;我得到了以下错误信息:> source("csf.R") > csf() Error: RStudio 未运行 - ManicMailman
这太棒了。有人能制作一个包吗? - Joe Flack
只要您不更改焦点文档,这在Rstudio中交互式运行时是有效的。如果您提交要运行的代码行,然后在它们运行时切换到另一个文档,则会返回到其他文档的路径。 - Patrick
3
我已经为此制作了一个名为“this.path”的软件包,您可以在CRAN网站上找到它,网址是https://CRAN.R-project.org/package=this.path。 - Iris

13

Supressingfire的答案进行了精简:

source_local <- function(fname){
    argv <- commandArgs(trailingOnly = FALSE)
    base_dir <- dirname(substring(argv[grep("--file=", argv)], 8))
    source(paste(base_dir, fname, sep="/"))
}

1
这不能递归地工作; 我所引用的文件正在寻找一个数据文件(但在错误的目录中)。 - The Unfun Cat

11
这对我有用。只需从命令行参数中将其筛选出来,剥离不需要的文本,执行 dirname ,最后从那里获取完整路径。
args <- commandArgs(trailingOnly = F)  
scriptPath <- normalizePath(dirname(sub("^--file=", "", args[grep("^--file=", args)])))

1
这是正确的答案。真让人费解为什么有那么多人在浪费时间看其他提出的答案。 - user5359531
在交互式的 R 会话中,这只是返回了空值。 - Ryan Howard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接