knitr 的 tangle(代码提取)功能变得更智能

注

这是关于近期一次为期四天的 knitr 问题排查冲刺中新功能的几篇博文之一;有关此次冲刺的完整详情,请参阅那篇文章。

Tangle 是旧式文学编程(例如 WEB/Sweave)中用于从文档中提取代码并丢弃说明性文字的术语。在 knitr 中,您可以使用 purl() 来实现这一操作:传入一个 .Rmd,即可得到一个仅包含代码的 .R 脚本。

这是一项小功能,但实际使用频率比我预想的要高。在本轮开发周期中,该功能得到了多项改进:

处理非 R 语言的文档

purl() 一直假定代码为 R 语言。然而,knitr 可以顺畅地运行 Python、Julia、SQL 以及其他十几种引擎,那么为什么 tangling 功能只能局限于 R 语言呢?

现在,如果文档中的第一个代码块使用了其他语言,则 purl() 会将整个文档导出为该语言的脚本。对于以 Python 为主的文档,您将得到一个 .py 文件:

```{python setup}
import pandas as pd
```

```{python}
df = pd.read_csv("data.csv")
df.describe()
```

在那上面运行 knitr::purl("report.Rmd"),就会得到一个 report.py。代码块的标题会变成 #%% 单元(这是 VS Code 和 Jupyter 所识别的代码单元格式),而其他语言的代码块则会被丢弃(你仍然无法在一个混杂的输出文件中混合使用多种语言)。

如果你想使用不同的文件扩展名,请使用 output 参数(#1928)。

从另一份文档中读取代码块

read_chunk() 允许一份文档从外部文件中借用已标记的代码块。在此之前,该文件必须是一个带有 ## ---- label 标记的纯 R 脚本。

现在,它也可以是另一份knitr 源文档——一个 .Rmd、一个 .Rnw

```{r, cache=FALSE}
knitr::read_chunk("analysis.Rmd")
```

Here is the model-fitting step from the full analysis:

```{r fit-model}
```

标签可以来自代码块的头部({r fit-model}),也可以来自一个#| label:选项。这对于教学材料和论文非常有用,因为您希望展示更大分析中的一个精选子集,而无需复制粘贴那些会逐渐不同步的代码(#2041)。

通过选项钩子决定哪些代码块参与混编

选项钩子(通过opts_hooks$set()注册)在编织时运行,现在在混编时也会运行。这意味着钩子可以根据任意代码块选项来决定哪些代码块会被纳入最终的脚本中。

一个常见的需求是:从文档中仅提取“解决方案”代码块。借助对label选项的钩子,您完全无需修改源文件:

knitr::opts_hooks$set(label = function(options) {
  options$purl <- grepl("-solution$", options$label)
  options
})
knitr::purl("exercises.Rmd")  # only chunks labelled *-solution end up in the script

以前,purl 是一个必须写入每个代码块头部的静态选项;现在你可以对其进行计算了(#1903)。

按你的方式添加注释

当 purl() 将某个代码块与 eval = FALSE 混合时,它会将该代码注释掉,理由是用户可能并不希望执行这段代码。但有时你确实希望这段代码能够在脚本中直接运行。

现在可以通过 comment 选项来控制这一行为:将 comment = ''(或 NA)设置为保持代码不被注释,即使它处于 eval = FALSE 状态:

```{r, eval=FALSE, comment=""}
# this stays runnable in the tangled script
dangerous_but_reproducible()
```

反过来也一样。在已求值的代码块上设置一个显式的 comment 前缀,其代码就会在脚本中被注释掉——这有助于记录那些只需执行一次、不应重复运行的初始化步骤(#2425,#1352)。

其他小改进

在同一领域还有两项较小的修复:

  • hook_purl() 是一个在编织过程中同时写入混乱脚本的钩子,现在会在输出中保留 #| 单元格选项,与 purl() 保持一致(#2414)。
  • 对 YAML 中定义了 params 的文档进行混乱处理时,不再会静默地丢弃那些在其选项中引用这些参数的代码块(例如 eval = params$run)。这一点我在待办事项总结中提到过(#1938)。
参与讨论