Здесь() проблема в сценариях R

Здесь проблема в сценариях R

Я пытаюсь понять, как здесь() будет работать переносимым способом. Найдено: посмотрите, что работает позже, в разделе «Окончательный ответ» — TL; DR — в нижней строке, here() на самом деле не так уж полезно запускать script.R из командной строки.

Как я это понимаю с помощью JBGruber: here() ищет корневой каталог проекта (например, проект RStudio, проект Git или другой проект, определенный с помощью файла .here), начиная с текущего рабочего каталога и продвигаясь вверх, пока не найдет любой проект. Если он ничего не находит, он возвращается к использованию полного рабочего каталога. Что в случае запуска скрипта cron по умолчанию будет моим домашним каталогом. Можно, конечно, передать каталог в качестве параметра через команду cron, но это довольно громоздко. Ниже ответы дают хорошие объяснения, и я резюмировал то, что я нашел наиболее полезным в разделе «Окончательный ответ». Но не заблуждайтесь, ответ Николы тоже очень хорош и полезен.

Первоначальная цель — написать набор R-скриптов, включая R-markdown .Rmd, чтобы я мог заархивировать каталог, отправить кому-то другому, и он запустился на их компьютере. Потенциально на очень слабом компьютере, таком как RaspberryPi или на старом оборудовании под управлением Linux.

Условия:

  • можно запустить из командной строки через Rscript
  • как указано выше, но запланировано через cron
  • Основным методом настройки рабочего каталога является set_here() - выполняется один раз из консоли, а затем папка становится переносимой, поскольку файл .here включен в заархивированный каталог.
  • не нуждается Rstudio - следовательно не хочет делать R-проекты
  • также можно запустить в интерактивном режиме из Rstudio (разработка)
  • может быть выполнено из shiny (я предполагаю, что все будет в порядке, если будут выполнены вышеуказанные условия)

Я специально не хочу создавать проекты 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() обеспечит абсолютный базовый путь.

Ошибки

Конечно - вышеописанное не работает. Это работает, только если я запускаю Rscript ./dataFetcherMailer.R из того же myGoodScripts/ каталога.

Моя цель - понять, как выполнять сценарии, чтобы относительные пути разрешались относительно местоположения сценария, и сценарий можно было запускать из командной строки независимо от текущего рабочего каталога. Теперь я могу запустить это из bash, только если я сделал cd в каталоге, содержащем скрипт. Если я назначу cron выполнение сценария, рабочим каталогом по умолчанию будет /home/user, и сценарий завершится ошибкой. Мой наивный подход, который независимо от текущего рабочего каталога оболочки basedir <- here() должен указывать точку файловой системы, из которой могут быть разрешены относительные пути, не работает.

От Rstudio без предварительного setwd()

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

Из bash с помощью Rscript, если cwd не установлен в каталог сценария.

$ 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. Поэтому универсально можно использовать только местоположение basepath from script.R, а не имя файла. Другими словами, эта функция при размещении в .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.

Мне непонятно, в чем проблема. Вы можете отправить папку, и тот, кто ее получит, сможет запустить ее так же, как вы запускаете ее на своем ПК. Может быть, вы хотите запустить его откуда-то еще? Пожалуйста, покажите, что не работает.

nicola 26.12.2020 08:44

Спасибо, я добавил объяснение и сообщения об ошибках.

r0berts 26.12.2020 10:23

Это получилось весьма красиво! Я не проверял все различные случаи, которые вы освещаете, но я рад, что вы закончили то, что я начал :) Я скопирую это в свой ответ, чтобы показать, что это окончательный ответ.

JBGruber 02.01.2021 20:02
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
10
3
1 986
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

то, что вы просили

Думаю, поведение 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, исходит из этого ответа. Я попробовал это, вставив определение функции в начало вашего файла 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 находится в папке с именем «скрипты» в вашей корневой папке, и вы хотите сохранить свои данные в папке с именем «данные» в вашей корневой папке, вы пишете saveRDS(data, "../data/dat.RDS").

Вы можете запустить файл Rmd из командной строки/cron с помощью Rscript -e 'rmarkdown::render("/home/johannes/Desktop/myGoodScripts/dataFetcher.Rmd")'.

Это выглядит великолепно и может просто решить эту проблему. Я проверю это и вернусь через час (когда доберусь до своего ноутбука)

r0berts 26.12.2020 13:22

Я проверил, и это работает очень хорошо. Благодаря вашей функции и объяснению я даже понимаю, как это работает. Будучи непрограммистом, я только задаюсь вопросом, какая польза от аргумента/параметра variables в объявлении function(). Но я должен быть в состоянии воспроизвести это в своих более сложных сценариях.

r0berts 26.12.2020 15:55

Извините, variables был просто остатком от шаблона функции, который я использовал. Я исправил это!

JBGruber 26.12.2020 16:13

Я еще немного подумал об этом и добавил альтернативный ответ, который, вероятно, лучше соответствует вашим потребностям.

JBGruber 28.12.2020 12:15

Спасибо, Йоханнес, это очень полезно. Я знал об относительных путях при вязании .Rmd, но элегантность вашей функции в том, что ее можно просто добавить в файл и она работает. Мне нужно будет передать эти сценарии коллегам, которые часто не знают, что такое путь. В данный момент пытаюсь модифицировать вашу функцию, чтобы она работала и при вязании Rmd из Rstudio. Я вставил измененную функцию в свой исходный вопрос, основываясь на том факте, что второй аргумент равен --slave при вязании из Rstudio.

r0berts 28.12.2020 13:39

Ради интереса, @JBGruber, как бы вы кратко определили поведение here()?

r0berts 06.01.2021 11:58

Я могу ошибаться, но: here() ищет корневой каталог проекта (например, проект RStudio, проект Git или другой проект, определенный с помощью .here файла), начиная с текущего рабочего каталога и продвигаясь вверх, пока не найдет какой-либо проект. Если он ничего не находит, он возвращается к использованию полного рабочего каталога. Меня постоянно смущало такое поведение, так как я ожидал, что поиск начнется с расположения файла, а не с рабочего каталога. Я рад, что у файлов .Rmd нет этой проблемы, что делает их более полезными для меня.

JBGruber 06.01.2021 12:34

Спасибо, именно это я и подозревал, но как-то не смог легко трут из пользовательской документации. Очень приятно знать вещи простым способом. Другая причина путаницы заключается в том, что часто это непоследовательно. Например. загрузите here, введите setwd() в консоли, подтвердите это с помощью getwd() issue set_here() для этого конкретного каталога, однако here() и dr_here() по-прежнему указывают на исходный рабочий каталог (по умолчанию домашний каталог). Мне нужно перезапустить сеанс R, чтобы here() заметил. Я подозреваю, что это связано с предыдущим setwd() предыдущим перезапуском сеанса R. Сбивает с толку.

r0berts 06.01.2021 12:44

На самом деле это сказано здесь: github.com/jennybc/here_here#the-fine-print, но я прочитал это первое предложение около 20 раз, прежде чем понял, что там говорится о рабочем каталоге.

JBGruber 06.01.2021 16:12

Да, так оно и есть, но мне это тоже было не очевидно, когда я лихорадочно искал надежное решение. Введение в gitHub звучит примерно так, как here() — это решение всех задач, связанных с путями, но, как мне кажется, оно действительно больше относится к проектам, запускаемым из Rstudio или другого интерфейса, а не к простой командной строке.

r0berts 06.01.2021 17:18

Хотя ваш вопрос требует использования пакета here, я предлагаю решение без необходимости. Я думаю, что это намного чище и одинаково портативно.

Если я правильно понимаю, вы хотите, чтобы ваш сценарий знал об их местонахождении. Это хорошо, но в большинстве случаев не нужно, потому что вызывающая сторона вашего скрипта должна знать, где находится скрипт, чтобы вызвать его, и вы можете использовать это знание. Так:

  • избавиться от всех звонков here;
  • не пытайтесь определить в своих сценариях расположение файла, а просто пишите каждый путь относительно корня вашей папки (так же, как вы это делаете в разработке).

Далее пару вариантов.

Первый, минимальный, — просто не регистрироваться на cron голом Rstudio /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. Создает папку в домашнем каталоге, что-то вроде .robertsProject (обратите внимание, что точка более вероятна, что каталог не существует).
  2. Копирует все файлы и каталоги из вашей папки в эту вновь созданную папку.
  3. Создает файл .sh точно так же, как и выше (обратите внимание, что вы знаете, куда вы перемещаете файлы и их местоположение, поэтому вы можете указать правильный путь в сценарии).
  4. Регистрирует файл .sh в crontab.

Сделанный! Тот, кто получит файл, должен будет просто запустить этот установщик один раз (вы задокументируете, как это сделать в README), и они смогут использовать ваш инструмент.

Спасибо, это очень полезно. Возможно, я реализую это в будущем. Однако на данный момент мне нужно что-то достаточно простое, чтобы другие пользователи могли открыть его в Rstudio и запустить, а также работать без изменений при вызове через cron. С установщиком я столкнулся бы с препятствием, что большинство коллег будут использовать Windows 10, не будут знать, какой путь и не будут иметь прав администратора на своих компьютерах. Таким образом, основная цель состоит в том, чтобы конечные пользователи не беспокоились, если они запустят его на своих машинах; если дело доходит до размещения скрипта на безголовом сервере Linux, мне придется это сделать.

r0berts 28.12.2020 17:48

Я не вижу, как вышеприведенное решение является более сложным. Если вам нужно настроить задание cron, установщик намного проще для вас и для всех, кто хочет его использовать. Если пользователь хочет запустить сценарий только с помощью Rstudio, единственное, что нужно сделать перед «запуском сценария», — это «установить рабочий каталог» из меню, что вряд ли является сложной задачей. Дело в том, что пользователь как для запуска, так и для установки рабочего каталога должен знать, где находятся ваши файлы.

nicola 31.12.2020 15:07

Спасибо, Никола, я думаю, что вы совершенно правы в целом, и я очень ценю ваш совет. Я обязательно попробую реализовать этот процесс позже. Однако причина моего вопроса заключалась в том, что мне нужно было понять, в чем разница между «местоположением» файла .R/.Rmd при выполнении из командной строки или Rstudio. Что я теперь понимаю, пройдя оба ответа и обновив свой вопрос. Проблема с установщиком заключается в том, что другие люди, которым может понадобиться использовать скрипт, будут использовать Windows, поэтому cron и bash запутают их и кажутся почти невозможными.

r0berts 01.01.2021 19:12

Хорошо, но я исходил из того, что пользователю Windows не должно быть интересно cron, и поэтому он может спокойно пропустить часть «установщика». Им просто нужно открыть Rstudio и установить рабочий каталог. Ваша разница с вашим запросом заключается в том, что они должны перейти непосредственно к «запуску сценария», но я не вижу дополнительных осложнений. Учтите, что наличие «домашнего» каталога — это то, что есть практически у каждого приложения и устанавливается во время установки. Существует не так много примеров программ, которые полагаются на путь, по которому они расположены во время выполнения.

nicola 01.01.2021 22:47

Другие вопросы по теме