Repository: pre-commit/pre-commit

Issue:
Avoid unquoting weirdness of Windows for  `language: r`
### search you tried in the issue tracker

never, r, found

### describe your issue

Multiple reports in https://github.com/lorenzwalthert/precommit (https://github.com/lorenzwalthert/precommit/issues/441, https://github.com/lorenzwalthert/precommit/issues/473) were raised and describe a problem with (un)quoting the long string that runs when `language: r` is setup in `Rscript -e 'xxx'` where `'xxx'` contains [multiple levels of quotes](https://github.com/pre-commit/pre-commit/blob/6896025288691aafd015a4681c59dc105e61b614/pre_commit/languages/r.py#L101). For the readers convenience, the output looks like:
```
[INFO] Installing environment for https://github.com/lorenzwalthert/precommit.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
[INFO] Restored changes from C:\Users\USER\.cache\pre-commit\patch1678401203-36472.
An unexpected error has occurred: CalledProcessError: command: ('C:/PROGRA~1/R/R-41~1.0\\bin\\Rscript.exe', '--vanilla', '-e', '    options(install.packages.compile.from.source = "never", pkgType = "binary")\n            prefix_dir <- \'C:\\\\Users\\\\USER\\\\.cache\\\\pre-commit\\\\repovawmpj_r\'\n        options(\n            repos = c(CRAN = "https://cran.rstudio.com"),\n            renv.consent = TRUE\n        )\n        source("renv/activate.R")\n        renv::restore()\n        activate_statement <- paste0(\n          \'suppressWarnings({\',\n          \'old <- setwd("\', getwd(), \'"); \',\n          \'source("renv/activate.R"); \',\n          \'setwd(old); \',\n          \'renv::load("\', getwd(), \'");})\'\n        )\n        writeLines(activate_statement, \'activate.R\')\n        is_package <- tryCatch(\n          {\n              path_desc <- file.path(prefix_dir, \'DESCRIPTION\')\n              suppressWarnings(desc <- read.dcf(path_desc))\n              "Package" %in% colnames(desc)\n          },\n          error = function(...) FALSE\n        )\n        if (is_package) {\n            renv::install(prefix_dir)\n        }\n        \n    ')
return code: 1
stdout: (none)
stderr:
    During startup - Warning messages:
    1: Setting LC_COLLATE=en_US.UTF-8 failed 
    2: Setting LC_CTYPE=en_US.UTF-8 failed 
    3: Setting LC_MONETARY=en_US.UTF-8 failed 
    4: Setting LC_TIME=en_US.UTF-8 failed 
    Error in options(install.packages.compile.from.source = never, pkgType = binary) : 
      object 'never' not found
    Execution halted
Check the log at C:\Users\USER\.cache\pre-commit\pre-commit.log
```


The solution described by @asottile in https://github.com/lorenzwalthert/precommit/issues/473#issuecomment-1511498032 is to probably write the contents to a temporary file and avoid unquoting within the expression (i.e. the term after `-e`). This should be quite straight forward.

Question is if we can create a good test first to reproduce the offending behavior and whether or not there are tools already in pre-commit to deal with temp files etc. that we could use.



### pre-commit --version

precommit 3.1.1

### .pre-commit-config.yaml

```yaml
repos:
-   repo: https://github.com/lorenzwalthert/precommit
    rev: v0.3.2.9007
    hooks:
    -   id: style-files
```


### ~/.cache/pre-commit/pre-commit.log (if present)

_No response_


Discussion:
`tempfile.NamedTemporaryFile` from the stdlib should be fine.  arguably this is a bug in R itself since the commandline is correct

Resolving patch:
diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py
--- a/pre_commit/languages/r.py
+++ b/pre_commit/languages/r.py
@@ -4,6 +4,8 @@
 import os
 import shlex
 import shutil
+import tempfile
+import textwrap
 from typing import Generator
 from typing import Sequence
 
@@ -21,6 +23,19 @@
 health_check = lang_base.basic_health_check
 
 
+@contextlib.contextmanager
+def _r_code_in_tempfile(code: str) -> Generator[str, None, None]:
+    """
+    To avoid quoting and escaping issues, avoid `Rscript [options] -e {expr}`
+    but use `Rscript [options] path/to/file_with_expr.R`
+    """
+    with tempfile.TemporaryDirectory() as tmpdir:
+        fname = os.path.join(tmpdir, 'script.R')
+        with open(fname, 'w') as f:
+            f.write(_inline_r_setup(textwrap.dedent(code)))
+        yield fname
+
+
 def get_env_patch(venv: str) -> PatchesT:
     return (
         ('R_PROFILE_USER', os.path.join(venv, 'activate.R')),
@@ -129,20 +144,19 @@ def install_environment(
         }}
         """
 
-    cmd_output_b(
-        _rscript_exec(), '--vanilla', '-e',
-        _inline_r_setup(r_code_inst_environment),
-        cwd=env_dir,
-    )
+    with _r_code_in_tempfile(r_code_inst_environment) as f:
+        cmd_output_b(_rscript_exec(), '--vanilla', f, cwd=env_dir)
+
     if additional_dependencies:
         r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))'
         with in_env(prefix, version):
-            cmd_output_b(
-                _rscript_exec(), *RSCRIPT_OPTS, '-e',
-                _inline_r_setup(r_code_inst_add),
-                *additional_dependencies,
-                cwd=env_dir,
-            )
+            with _r_code_in_tempfile(r_code_inst_add) as f:
+                cmd_output_b(
+                    _rscript_exec(), *RSCRIPT_OPTS,
+                    f,
+                    *additional_dependencies,
+                    cwd=env_dir,
+                )
 
 
 def _inline_r_setup(code: str) -> str:
@@ -150,11 +164,16 @@ def _inline_r_setup(code: str) -> str:
     Some behaviour of R cannot be configured via env variables, but can
     only be configured via R options once R has started. These are set here.
     """
-    with_option = f"""\
-    options(install.packages.compile.from.source = "never", pkgType = "binary")
-    {code}
-    """
-    return with_option
+    with_option = [
+        textwrap.dedent("""\
+        options(
+            install.packages.compile.from.source = "never",
+            pkgType = "binary"
+        )
+        """),
+        code,
+    ]
+    return '\n'.join(with_option)
 
 
 def run_hook(


Test patch: