【问题标题】:here() issue in R scriptsR脚本中的here()问题
【发布时间】:2021-04-02 03:22:54
【问题描述】:

R 脚本中的此处问题

我试图了解 here() 如何以可移植的方式工作。找到它:在 Final answer - TL;DR 下查看稍后的工作原理 - 最重要的是,here() 从命令行运行 script.R 并没有那么有用。

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

最初的目标 - 编写一组 R 脚本,包括 R-markdown .Rmd,这样我就可以压缩目录,发送给其他人,它可以在他们的计算机上运行。可能在非常低端的计算机上 - 例如 RaspberryPi 或运行 linux 的旧硬件。

条件:

  • 可以通过Rscript从命令行运行
  • 同上,但通过cron安排
  • 设置工作目录的主要方法是set_here() - 从控制台执行一次,然后文件夹是可移植的,因为.here 文件包含在压缩目录中。
  • 不需要Rstudio - 因此不想做 R 项目
  • 可以也可以从Rstudio(开发)交互式运行
  • 可以从shiny执行(我假设满足以上条件就OK了)

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

示例代码:

假设工作目录为myGoodScripts,如下所示:

/Users/john/src/myGoodScripts/

开始开发时,我会使用setwd() 进入上述目录并执行set_here() 以创建.here 文件。然后有2个脚本dataFetcherMailer.RdataFetcher.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 时它才有效。

我的目标是了解如何执行脚本,以便相对于脚本的位置解析相对路径,并且可以从命令行独立于当前工作目录运行脚本。现在,只有在将 cd 完成到包含脚本的目录后,我才能从 bash 运行它。如果我安排 cron 执行脚本,默认工作目录将是 /home/user 并且脚本失败。我的天真方法是,无论 shell 的当前工作目录 basedir &lt;- here() 是什么,都应该给出一个文件系统点,从该点可以解析相对路径是行不通的。

来自 Rstudio,没有事先 setwd()

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

如果 cwd 未设置到脚本目录,则从带有 Rscript 的 bash。

$ 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 回答以来的更新:

我稍微修改了函数,以便它可以返回文件的文件名或目录。我目前正在尝试对其进行修改,以便当.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(".") - 无论您从脚本调用 .Rmd 文件,它都有效,命令行或来自 Rstudio。

【问题讨论】:

  • 我不清楚是什么问题。您可以发送该文件夹,任何收到它的人都可以像在 PC 上运行它一样运行它。也许您想从其他地方启动它?请说明什么不起作用。
  • 谢谢,我添加了解释和错误信息。
  • 结果非常好!我没有检查您涵盖的所有不同案例,但我很高兴您完成了我开始的内容:) 我会将其复制到我的答案中,以表明这是最终答案。

标签: r path cron knitr


【解决方案1】:

你要求什么

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")

您可以使用Rscript -e 'rmarkdown::render("/home/johannes/Desktop/myGoodScripts/dataFetcher.Rmd")' 从命令行/cron 运行 Rmd 文件。

【讨论】:

  • 我已经测试过了,效果很好。感谢您的功能和解释,我什至了解它是如何工作的。作为一名非程序员,我只想知道variables 参数/参数对function() 声明的用途。但我应该能够在我更复杂的脚本中复制它。
  • 抱歉variables 只是我使用的函数模板的剩余部分。我更正了!
  • 我想了很多,并添加了一个可能更好地满足您需求的替代答案。
  • 我可能错了,但是:here() 开始查找项目的根目录(例如,RStudio 项目、Git 项目或使用 .here 文件定义的其他项目)当前工作目录并向上移动直到找到任何项目。如果它没有找到任何东西,它会回退到使用完整的工作目录。我一直对这种行为感到困惑,因为我希望搜索从文件位置而不是工作目录开始。我很高兴.Rmd 文件没有这个问题,这让它们对我更有用。
  • 谢谢,这正是我所怀疑的,但不知何故我无法从用户文档中轻松获取。以简单的方式了解事物是非常令人放心的。造成混淆的另一个原因是这通常是不一致的。例如。加载here,在控制台中发出setwd()getwd() 发出set_here() 确认该特定目录,但是here()dr_here() 仍然指向初始工作目录(默认为主目录)。我必须为here() 重新启动 R 会话才能注意到。我怀疑这与之前的 setwd() 之前的 R 会话重启有关。令人困惑。
【解决方案2】:

虽然您的问题需要使用 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。

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

【讨论】:

  • 谢谢,这很有用。也许我会在未来实现这一点。但是现在我需要一些足够简单的东西,以便其他用户在 Rstudio 中打开并运行 - 并且在通过 cron 调用时无需更改即可运行。使用安装程序时,我会遇到大多数同事使用 Windows 10 的障碍,不知道 path 是什么,也没有他们计算机的管理员权限。因此,主要目的是让最终用户在他们的机器上运行它时不会感到慌张;如果要将脚本放在 linux 无头服务器上,我将不得不这样做。
  • 我看不出上述解决方案在任何方面都更复杂。如果您必须设置一个 cron 作业,安装程序对您自己和想要使用它的每个人来说都更容易。如果用户只想使用 Rstudio 运行脚本,在“运行脚本”之前唯一要做的就是从菜单中“设置工作目录”,这几乎不是一项复杂的任务。关键是运行和/或设置工作目录的用户必须知道你的文件在哪里。
  • 谢谢 Nicola,我认为您总体上是完全正确的,非常感谢您的建议。我一定会在以后尝试实现这个过程。但是,我提出问题的原因是我需要了解从命令行或 Rstudio 执行的.R/.Rmd 文件的“位置”有何不同。我现在了解已经完成了答案并更新了我的问题。安装程序的问题是 - 可能需要使用该脚本的其他人将使用 Windows,因此 cronbash 会使他们感到困惑并且看起来几乎不可能。
  • 好的,但我假设 Windows 用户不应该对cron 感兴趣,因此他们可以安全地跳过“安装程序”部分。他们只需要打开 Rstudio 并设置工作目录。您与您的要求的不同之处在于他们应该直接进入“运行脚本”,但我没有看到增加的复杂性。考虑到拥有一个“主”目录是基本上每个 应用程序都有并且在安装过程中设置的东西。在运行时依赖于它们所在路径的程序示例并不多。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多