R 脚本中的 here() 问题

here() issue in R scripts

这里是 R 脚本中的问题

我想了解 here() 如何以可移植的方式工作。找到它:查看稍后在 最终答案下的工作原理 - TL;DR - 最重要的是,here() 并不是那么有用 运行宁一个 script.R 来自命令行。

我在 JBGruber 的帮助下理解它的方式:here() 查找项目的根目录(例如,RStudio 项目、Git 项目或使用 .here 文件定义的其他项目) 从当前工作目录开始 并向上移动直到找到任何项目。如果它没有找到任何东西,它会回退到使用完整的工作目录。如果 cron 的脚本 运行 将默认为我的主目录。当然,可以通过 cron 命令将目录作为参数传递,但这相当麻烦。下面的答案提供了很好的解释,我在“最终答案”部分总结了我发现最直接有用的内容。但是请不要误会,Nicola 的回答非常好,也很有帮助。

原创 Objective - 写了一套R脚本,包括R-markdown .Rmd 这样我就可以压缩目录,发给别人它会在他们的计算机上 运行。可能在非常低端的计算机上 - 例如 RaspberryPi 或旧硬件 运行ning linux.

条件:

我特别不想创建 Rstudio 项目,因为在我看来它需要安装和使用 Rstudio,但我希望我的脚本尽可能可移植并且 运行 在低资源、无头平台上。

示例代码:

让我们假设工作目录 myGoodScripts 如下:

/Users/john/src/myGoodScripts/

开始开发时,我会使用 setwd() 进入上述目录并执行 set_here() 以创建 .here 文件。然后有2个脚本dataFetcherMailer.R,dataFetcher.Rmd和一个子目录bkp:

dataFetcherMailer.R

library(here)
library(knitr)

basedir <- here()
# this is where here should give path to .here file

rmarkdown::render(paste0(basedir,"/dataFetcher.Rmd"))

# email the created report
# email_routine_with_gmailr(paste0(basedir,"dataFetcher.pdf"))
# now substituted with verification that a pdf report was created
file.exists(paste0(basedir,"/dataFetcher.pdf"))

dataFetcher.Rmd

---
title: "Data collection control report"
author: "HAL"
date: "`r Sys.Date()`"
output: pdf_document
---

```{r setup, include=FALSE}
library(knitr)
library(here)

basedir <- here()

# in actual program this reads data from a changing online data source
df.main <- mtcars

# data backup
datestamp <- format(Sys.time(),format="%Y-%m-%d_%H-%M")
backupName <- paste0(basedir,"/bkp/dataBackup_",datestamp,"csv.gz")
write.csv(df.main, gzfile(backupName))
```

# This is data collection report

Yesterday's data total records: `r nrow(df.main)`. 

The basedir was `r basedir`

The current directory is `r getwd()`

The here path is `r here()`

我猜报告中的最后 3 行是匹配的。即使 getwd() 与其他两个不匹配,也没关系,因为 here() 将确保绝对基本路径。

错误

当然 - 以上方法不起作用。它只有在我从同一个 myGoodScripts/ 目录执行 Rscript ./dataFetcherMailer.R 时才有效。

我的目标 是了解如何执行脚本,以便相对于脚本位置解析相对路径,并且脚本可以 运行 来自独立于命令行的当前工作目录。我现在可以从 bash 运行 执行此操作,前提是我已对包含脚本的目录执行 cd 操作。如果我安排 cron 执行脚本,则默认工作目录将为 /home/user 并且脚本失败。我天真的方法是不管 shell 的当前工作目录 basedir <- here() 应该给出一个可以解析相对路径的文件系统点是行不通的。

来自 Rstudio,无需事先 setwd()

here() starts at /home/user
Error in abs_path(input) : 
The file '/home/user/dataFetcher.Rmd' does not exist.

如果 cwd 未设置为脚本目录,则来自 bash 和 Rscript

$ cd /home/user/scrc
$ Rscript ./myGoodScripts/dataFetcherMailer.R 
here() starts at /home/user/src
Error in abs_path(input) : 
The file '/home/user/src/dataFetcher.Rmd' does not exist.
Calls: <Anonymous> -> setwd -> dirname -> abs_path

如果有人能帮助我理解并解决这个问题,那就太好了。如果存在另一种设置没有 here() 的基本路径的可靠方法,我很想知道。最终从 Rstudio 执行脚本比了解如何从 commandline/cron.

执行此类脚本重要得多

自 JBGruber 回答以来的更新:

我稍微修改了函数,以便它可以 return 文件的文件名或目录。我目前正在尝试修改它,以便当 .Rmd 文件从 Rstudio 编织时它可以工作,同样地 运行 通过 R 文件编织。

here2 <- function(type = 'dir') {
  args <- commandArgs(trailingOnly = FALSE)
  if ("RStudio" %in% args) {
    filepath <- rstudioapi::getActiveDocumentContext()$path
  } else if ("interactive" %in% args) {
    file_arg <- "--file="
    filepath <- sub(file_arg, "", grep(file_arg, args, value = TRUE))
  } else if ("--slave" %in% args) {
    string <- args[6]
    mBtwSquotes <- "(?<=')[^']*[^']*(?=')"
    filepath <- regmatches(string,regexpr(mBtwSquotes,string,perl = T))
  } else if (pmatch("--file=" ,args)) {
    file_arg <- "--file="
    filepath <- sub(file_arg, "", grep(file_arg, args, value = TRUE))
  } else {
    if (type == 'dir') {
      filepath <- '.'
      return(filepath)
    } else {
      filepath <- "error"
      return(filepath)
    }
  }
  if (type == 'dir') {
    filepath <- dirname(filepath)
  }  
  return(filepath)
}

然而我发现 commandArgs() 是从 R 脚本继承的,即当 .Rmd 文档从 script.R 编织时它们保持不变。因此只有 script.R 位置的 basepath 可以通用,而不是文件名。换句话说,当放置在 .Rmd 文件中时,此函数将指向调用 script.R 路径而不是 .Rmd 文件路径。

最终答案 (TL;DR)

因此,此函数的较短版本会更有用:

here2 <- function() {
  args <- commandArgs(trailingOnly = FALSE)
  if ("RStudio" %in% args) {
    # R script called from Rstudio with "source file button"
    filepath <- rstudioapi::getActiveDocumentContext()$path
  } else if ("--slave" %in% args) {
    # Rmd file called from Rstudio with "knit button"  
    # (if we placed this function in a .Rmd file)
    file_arg <- "rmarkdown::render"
    string <- grep(file_arg, args, value = TRUE)
    mBtwQuotes <- "(?<=')[^']*[^']*(?=')"
    filepath <- regmatches(string,regexpr(mBtwQuotes,string,perl = T))
  } else if ((sum(grepl("--file=" ,args))) >0) {
    # called in some other way that passes --file= argument
    # R script called via cron or commandline using Rscript
    file_arg <- "--file="
    filepath <- sub(file_arg, "", grep(file_arg, args, value = TRUE))
  } else if (sum(grepl("rmarkdown::render" ,args)) >0 ) {
    # Rmd file called to render from commandline with 
    # Rscript -e 'rmarkdown::render("RmdFileName")'
    file_arg <- "rmarkdown::render"
    string <- grep(file_arg, args, value = TRUE)
    mBtwQuotes <- "(?<=\")[^\"]*[^\"]*(?=\")"
    filepath <- regmatches(string,regexpr(mBtwQuotes,string,perl = T))
  } else {
    # we do not know what is happening; taking a chance; could have  error later
    filepath <- normalizePath(".")
    return(filepath)
  }
  filepath <- dirname(filepath)
  return(filepath)
}

NB:.Rmd 文件中到达文件的包含目录调用 normalizePath(".") 就足够了 - 无论你调用来自脚本、命令行或 Rstudio 的 .Rmd 文件。

你问的是什么

here() 的行为并不是您真正想要的,我认为。相反,您正在寻找的是确定源文件的路径,也就是 .R 文件。我对 here() 命令进行了一些扩展,以按照您期望的方式运行:

here2 <- function() {
  args <- commandArgs(trailingOnly = FALSE)
  if ("RStudio" %in% args) {
    dirname(rstudioapi::getActiveDocumentContext()$path)
  } else {
    file_arg <- "--file="
    filepath <- sub(file_arg, "", grep(file_arg, args, value = TRUE))
    dirname(filepath)
  }
}

在 RStudio 中脚本不是 运行 的情况的想法来自 this answer。我通过将函数定义粘贴到 dataFetcherMailer.R 文件的开头来尝试这样做。您也可以考虑将它放在您的主目录中的另一个文件中,并使用例如 source("here2.R") 而不是 library(here) 来调用它,或者您可以为此目的编写一个小的 R 包。

r0berts 的最终版本(op)

here2 <- function() {
  args <- commandArgs(trailingOnly = FALSE)
  if ("RStudio" %in% args) {
    # R script called from Rstudio with "source file button"
    filepath <- rstudioapi::getActiveDocumentContext()$path
  } else if ("--slave" %in% args) {
    # Rmd file called from Rstudio with "knit button"  
    # (if we placed this function in a .Rmd file)
    file_arg <- "rmarkdown::render"
    string <- grep(file_arg, args, value = TRUE)
    mBtwQuotes <- "(?<=')[^']*[^']*(?=')"
    filepath <- regmatches(string,regexpr(mBtwQuotes,string,perl = T))
  } else if ((sum(grepl("--file=" ,args))) >0) {
    # called in some other way that passes --file= argument
    # R script called via cron or commandline using Rscript
    file_arg <- "--file="
    filepath <- sub(file_arg, "", grep(file_arg, args, value = TRUE))
  } else if (sum(grepl("rmarkdown::render" ,args)) >0 ) {
    # Rmd file called to render from commandline with 
    # Rscript -e 'rmarkdown::render("RmdFileName")'
    file_arg <- "rmarkdown::render"
    string <- grep(file_arg, args, value = TRUE)
    mBtwQuotes <- "(?<=\")[^\"]*[^\"]*(?=\")"
    filepath <- regmatches(string,regexpr(mBtwQuotes,string,perl = T))
  } else {
    # we do not know what is happening; taking a chance; could have  error later
    filepath <- normalizePath(".")
    return(filepath)
  }
  filepath <- dirname(filepath)
  return(filepath)
}

我认为大多数人实际需要的东西

我不久前发现了这种方法,但后来实际上完全改变了我的工作流程,只使用 R Markdown 文件(和 RStudio 项目)。这样做的优点之一是 Rmd 文件的工作目录始终是该文件的位置。因此,您不必为设置工作目录而烦恼,只需在脚本中编写相对于 Rmd 文件位置的所有路径即可。

---
title: "Data collection control report"
author: "HAL"
date: "`r Sys.Date()`"
output: pdf_document
---

```{r setup, include=FALSE}
library(knitr)

# in actual program this reads data from a changing online data source
df.main <- mtcars

# data backup
datestamp <- format(Sys.time(),format="%Y-%m-%d_%H-%M")

# create bkp folder if it doesn't exist
if (!dir.exists(paste0("./bkp/"))) dir.create("./bkp/")

backupName <- paste0("./bkp/dataBackup_", datestamp, "csv.gz")
write.csv(df.main, gzfile(backupName))
```

# This is data collection report

Yesterday's data total records: `r nrow(df.main)`. 

The current directory is `r getwd()`

注意以./开头的路径表示从Rmd文件的文件夹开始。 ../ 表示您升级了一个级别。 ../../ 你往上两层等等。因此,如果您的 Rmd 文件位于根文件夹中名为“scripts”的文件夹中,并且您想将数据保存在根文件夹中名为“data”的文件夹中,则可以编写 saveRDS(data, "../data/dat.RDS").

您可以 运行 来自命令 line/cron 和 Rscript -e 'rmarkdown::render("/home/johannes/Desktop/myGoodScripts/dataFetcher.Rmd")' 的 Rmd 文件。

虽然您的问题需要使用 here 包,但我提出了一个不需要它的解决方案。我认为它更干净,同样便携。

如果我的理解是正确的,您希望您的脚本知道它们的位置。这很好,但在大多数情况下是不必要的,因为脚本的调用者 必须 知道脚本所在的位置才能实际调用它,并且您可以利用这些知识。所以:

  • 摆脱所有 here 电话;
  • 不要试图在您的脚本中确定文件位置,而只需将每个路径写为相对于您的文件夹的根目录(就像您在开发中所做的那样)。

接下来,有几个选项。

第一个,最小的,就是不要注册到 cronRstudio /path/to/yourfolder/yourscript.R,而是创建一个 bash 脚本如下(我们称之为 script.sh ):

#!/bin/sh
cd /path/to/yourfolder
Rscript yourscript.R

并将此脚本注册到 crontab。当您指示执行上述操作时,您可以将 README 文件添加到您的文件夹中(类似于:“将文件夹解压缩到您想要的任何位置,记下路径,构建一个 script.sh 文件并对其进行 crotab” ).当然,使用 Rstudio,您可以以通常的方式打开并 运行 文件(setwd 然后 运行 它;您将其记录在 README 中)。

第二种是编写“安装程序”(您可以选择 makefile、简单的 R 脚本、bash 文件或其他),它会自动执行上述操作。它只是执行这些步骤。

  1. 在 homedir 下创建一个文件夹,类似于 .robertsProject(注意圆点更可能是该目录不存在)。
  2. 将文件夹中的所有文件和目录复制到这个新创建的文件夹中。
  3. 创建一个 .sh 文件,就像上面的文件一样(请注意,您知道要将文件移动到哪里以及它们的位置,因此您可以在脚本中写入正确的路径)。
  4. .sh 文件注册到 crontab。

完成!收到该文件的人只需 运行 安装一次此安装程序(您将在 README 中记录如何安装),他们就可以使用您的工具。