1  整个流程

剧透警告!

本章介绍了一个小型示例软件包的开发过程。 其目的是勾勒出整体的蓝图,为我们深入讨论 R 包的关键组件之前提供一个工作流程的概览。

为了保持快节奏,我们充分利用 devtools 包和 RStudio 集成开发环境(IDE)中提供的现代便利工具。在后续的章节中,我们会更详细地说明这些辅助工具是如何帮助我们的。

这一章的内容是独立的,因为完成练习并不是继续阅读本书其余部分的严格要求,但是我们强烈建议你跟着我们一起创建这个示例软件包。

1.1 加载 devtools 和 相关程序包

你可以从任何活动的 R 会话中初始化新的软件包。 无需担心当前是在一个已存在的项目中还是新项目中, 我们使用的函数会确保为这个软件包创建一个新的干净的项目。

首先需要加载 devtools 包, 它是一组支持软件包开发各个方面的包的公开接口。 其中最明显的是 usethis 包,你可以看到它也会被加载。

library(devtools)
#> Warning: package 'devtools' was built under R version 4.2.2
#> Loading required package: usethis
#> Warning: package 'usethis' was built under R version 4.2.3

你现在安装的是旧版本的 devtools 包吗? 比较一下你的版本和我们的版本,必要时可以进行升级。

packageVersion("devtools")
#> [1] '2.4.5'

1.2 示例软件包:regexcite

为了帮助你完成这个过程,我们使用 devtools 包中的各种函数从头开始构建一个小型示例软件包,其中包含了已发布软件包中常见的功能:

  • 用于满足特定需求的函数,在本例中是用于处理正则表达式的辅助函数。
  • 版本控制和开放的开发过程。
    • 在你的工作中这一配置是完全可选的,但我们强烈建议这样做。你将看到 Git 和 GitHub 如何帮助我们展示示例软件包开发的所有中间阶段。
  • 能够访问已建立的工作流程1,进行软件包安装、获取帮助和检查质量。
    • 使用 roxygen2 为每个函数建立文档。
    • 使用 testthat 进行单元测试。
    • 通过一个可执行的 README 文件 README.Rmd 来整体展示软件包。

我们把这个软件包命名为 regexcite,它包含了几个函数,能够让使用正则表达式的常见任务变得更加容易。 请注意,这些函数非常简单,我们在这里使用它们只是为了引导你完成软件包开发过程。 如果你正在寻找使用正则表达式的辅助函数,这里有几个合适的 R 包可以解决这个问题:

同样,regexcite 包本身只是一个设备,用于演示使用 devtools 进行软件包开发的典型工作流。

1.3 看看成品

我们使用了 Git 版本控制系统来跟踪 regexcite 包开发的整个过程。 这一配置完全是可选的,你完全可以在不实现这一配置的情况下继续跟着进行其他步骤。 但是它有一个附加的好处,我们最终会将它连接到 GitHuub 上的远程储存库,这意味着你能够通过访问 GitHub 上的 regexcite 库来浏览我们努力取得的光荣成果: https://github.com/jennybc/regexcite。 通过检查 commit history,特别是版本差异,你可以准确地看到在下面列出的流程中,每个步骤发生了哪些更改。

1.4 create_package()

调用 create_package() 来初始化计算机上指定目录中的新软件包。 如果该目录不存在,create_package() 将会自动创建该目录(通常都是这种情况)。 有关创建软件包的更多信息,请参阅 Section 4.1

慎重选择创建软件包的目录。 它应该在你的主目录 (home) 中,与其他 R 项目放在一起。 另外它不应该嵌套在其他 RStudio 项目、R 包或 Git 储存库中。 它也不应该在 R 软件包库 (R package library) 中,里面包含了已经构建和安装的包。 将我们在这里创建的源码包转换为已安装的包是 devtools 功能的一部分。 不要自己完成 devtools 能够完成的事情!

一旦你选择了创建这个软件包的位置,将下面 create_package() 中的路径替换为你选择的路径并调用:

create_package("~/path/to/regexcite")

为了创建这本书,我们必须在一个临时目录中工作,因为这本书是在云中以非交互方式构建的。 在幕后,我们正在执行我们自己的 create_package() 命令,如果我们的输出与你的略有不同,请不要感到惊讶。

#> ✔ Creating 'C:/Users/xiaob/AppData/Local/Temp/Rtmpk30OW9/regexcite/'
#> ✔ Setting active project to 'C:/Users/xiaob/AppData/Local/Temp/Rtmpk30OW9/regexcite'
#> ✔ Creating 'R/'
#> ✔ Writing 'DESCRIPTION'
#> Package: regexcite
#> Title: What the Package Does (One Line, Title Case)
#> Version: 0.0.0.9000
#> Authors@R (parsed):
#>     * First Last <first.last@example.com> [aut, cre] (YOUR-ORCID-ID)
#> Description: What the package does (one paragraph).
#> License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a
#>     license
#> Encoding: UTF-8
#> Roxygen: list(markdown = TRUE)
#> RoxygenNote: 7.3.0
#> ✔ Writing 'NAMESPACE'
#> ✔ Writing 'regexcite.Rproj'
#> ✔ Adding '^regexcite\\.Rproj$' to '.Rbuildignore'
#> ✔ Adding '.Rproj.user' to '.gitignore'
#> ✔ Adding '^\\.Rproj\\.user$' to '.Rbuildignore'
#> ✔ Setting active project to '<no active project>'

如果你正在 RStudio 中工作,你应该会发现自己进入了一个新的 RStudio 程序界面,它在新的 regexcite 软件包项目目录中打开。 如果你出于某种原因需要手动执行这个操作,请进入该目录并双击 regexcite.Rproj。 RStudio 对于软件包项目做了特殊处理,你应该可以在 EnvironmentHistory 所在的窗格中看到一个 Build 选项卡。

你可能需要再次调用 library(devtools),因为 create_package() 可能已经在新的软件包目录中启动了一个新的 R 会话。

在这个新目录里的内容是一个 R 包,也许还是个 RStudio 项目? 这里是一个文件清单(在本地,您可以查看 Files 窗格2):

path type
.gitignore file
.Rbuildignore file
DESCRIPTION file
NAMESPACE file
R directory
regexcite.Rproj file
RStudio

Files 窗格3中,依次点击 More(齿轮图标) > Show Hidden Files 来切换隐藏文件(也称为 “dotfiles”)的可见性。 一些文件是始终可见的,但有时你可能会希望看到全部的文件。

  • .Rbuildignore 列出了我们开发 R 包时需要的,但是从源代码构建 R 包时并不应该包含进来的文件。如果你不使用 RStudio,create_package() 一开始可能并不会创建这个文件(同样也不会创建 .gitignore),因为没有 RStudio 相关的机制需要被忽略。然而,无论你使用的是什么编辑器,都可能在某些时候产生对 .Rbuildignore 的需求。Section 3.3.1 将对此进行更加详细的讨论。
  • .Rproj.user,如果有的话,它会是 RStudio 内部使用的目录。
  • .gitignore 为 Git 的使用做好准备。它将忽略一些由 R 和 RStudio 创建的标准的幕后文件。即使你不打算使用 Git,它也并不会产生妨害。
  • DESCRIPTION 提供了有关软件包的元数据。我们很快将开始编写这个文件,同时 ?sec-description 涵盖了 DESCRIPTION 文件的常用内容。
  • NAMESPACE 声明了软件包导出以供外部使用的函数以及软件包从其他包导入的外部函数。现在,除了一个注释声明这是一个我们不应该手动编辑的文件外,它是空的。
  • R/ 目录是你的软件包的“业务端”。它很快将包含带有函数声明的 .R 文件。
  • regexcite.Rproj 是使得该目录成为 RStudio 项目的文件。即使你不使用 RStudio,这个文件也没有妨害。或者你可以使用 create_package(..., rstudio = FALSE) 来避免创建该文件,详见 Section 4.2

1.5 use_git()

regexcite 目录是一个 R 源码包,同时也是一个 RStudio 项目。 现在,我们使用 use_git() 让它变成一个 Git 储存库。 (顺便一提,use_git() 可以在任何项目中工作,不论它是否是一个 R 包。)

use_git()
#> ✔ Initialising Git repo
#> ✔ Adding '.Rhistory', '.Rdata', '.httr-oauth', '.DS_Store', '.quarto' to '.gitignore'

在交互式会话中,系统将询问你是否要在此处提交 (commit) 这些文件,一般来说会选择提交。 在幕后,我们也将提交这些相同的文件。

那么在软件包中发生了什么变化呢? 可以发现只创建了 .git 目录,这个目录在大多数环境中都是隐藏的,包括 RStudio 文件浏览器 它的存在证明我们确实在这个目录下初始化了 Git 存储库。

path type
.git directory

如果你使用的是 RStudio,它可能会请求在此项目中重新启动,遵从它的请求即可。 你也可以通过关闭 RStudio 然后双击 regexcite.Rproj 来手动重新启动RStudio。 现在,除了软件包的开发支持外,你也可以在 Environment/History/Build 窗格中的 Git 选项卡访问一个基础的 Git 客户端。

点击 History(Git 窗格中的时钟图标),如果你之前同意了提交,你将会看到一个通过 use_git() 完成的初始提交。

commit author message
df926df8cd… YuanchenZhu2020 Initial commit
RStudio

只要你配置了 RStudio + Git 集成环境,RStudio 可以在任何项目中初始化一个 Git 储存库,即使该项目不是一个 R 软件包。 依次点击 Tools > Version Control > Project Setup。 然后选择 Version control system: Git为这个项目初始化一个新的 git 储存库

1.6 编写第一个函数

在处理字符串时,一个相当常见的任务是需要将单个字符串拆分为许多部分。 base R中的 strsplit() 函数就是完成这个任务的。

(x <- "alfa,bravo,charlie,delta")
#> [1] "alfa,bravo,charlie,delta"
strsplit(x, split = ",")
#> [[1]]
#> [1] "alfa"    "bravo"   "charlie" "delta"

仔细看看返回值。

str(strsplit(x, split = ","))
#> List of 1
#>  $ : chr [1:4] "alfa" "bravo" "charlie" "delta"

这个返回值的形状常常使人们感到惊讶,或者至少使他们感到不方便。 函数输入是长度为 1 的字符向量,而函数输出则是长度为 1 的列表。 考虑到 R 向矢量化演进的基本趋势,这是完全有意义的。 但有时它仍然有点讨人厌。 通常情况下,你知道你的输入在逻辑上是一个标量,即它只是一个单一的字符串,并且希望输出包含它各部分的字符向量。

这导致 R 用户采用各种方法对输出结果进行列表展开 (unlist):

unlist(strsplit(x, split = ","))
#> [1] "alfa"    "bravo"   "charlie" "delta"

strsplit(x, split = ",")[[1]]
#> [1] "alfa"    "bravo"   "charlie" "delta"

上面的第二种更安全的解决方案是 regexcite 的首个函数 —— strsplit1() 的基础:

strsplit1 <- function(x, split) {
  strsplit(x, split = split)[[1]]
}

这本书不会教你如何用 R 写函数。 要了解更多信息,请查看 R for Data Scienc 的 Functions chapter 以及 Advanced R 的 Functions chapter

Tip

strsplit1() 的名字是对非常方便的 paste0() 的致敬,它首次出现在 2012 年的 R 2.15.0 中。 创建 paste0() 是为了解决 paste() 不使用分隔符将字符串连接在一起的极其常见的用例。 paste0() 被亲切地描述为 “statistical computing’s most influential contribution of the 21st century”.

strsplit1() 函数非常鼓舞人心,现在它是 stringr 包中的一个真正的函数: stringr::str_split_1()!

1.7 use_r()

strsplit1() 的函数定义应该放在哪里呢? 它应该被保存在软件包的 R/ 子目录下的 .R 文件中。 一个合理的处理方法是为包中每个面向用户的函数创建一个新的 .R 文件,并用函数名命名对应文件。 当你添加更多函数时,你可能会希望放宽一点这个要求,并将相关函数分组组织在一起。 我们将会把 strsplit1() 的函数定义保存在文件 R/strsplit1.R 中。

辅助函数 use_r() 会在 R/ 目录下创建和(或)打开对应脚本文件。 在一个开发逐渐成熟的软件包中,当你需要在 .R 文件以及关联的测试文件之间切换时,它真的很好用。 但是,即使在目前刚开始开发的阶段,它在防止你自己沉迷于在 Untitled4 中工作也是很有帮助的。

use_r("strsplit1")
#> • Edit 'R/strsplit1.R'

strsplit1() 的函数定义,并且只有 strsplit1() 的函数定义放在 R/strsplit1.R 文件中并保存。 文件 R/strsplit1.R 不应该包含其他任何我们最近执行的顶层代码,例如我们用于实践的输入 xlibrary(devtools)use_git()。 这预示着从编写 R 脚本过渡到编写 R 包时需要进行的调整。 软件包和脚本使用不同的机制来声明它们对其他包的依赖性,并存储示例或测试代码。 我们将在 ?sec-r 中进一步讨论这一点。

1.8 load_all()

我们应该如何测试 strsplit1()? 如果这是一个普通的 R 脚本,我们可以使用 RStudio 将函数定义发送到 R 控制台,并在全局环境中定义 strsplit1()。 或者我们可以调用 source("R/strsplit1.R")。 然而,对于软件包开发来说,devtools 提供了一种更健壮的方法。

调用 load_all() 来使 strsplit1() 可以用于测试运行。

load_all()
#> ℹ Loading regexcite

现在可以调用 strsplit1(x) 来看看它是如何工作的。

(x <- "alfa,bravo,charlie,delta")
#> [1] "alfa,bravo,charlie,delta"
strsplit1(x, split = ",")
#> [1] "alfa"    "bravo"   "charlie" "delta"

请注意 load_all() 会使得 strsplit1() 函数可以使用,尽管它在全局环境中并不存在。

exists("strsplit1", where = globalenv(), inherits = FALSE)
#> [1] FALSE

如果你的运行结果是 TRUE 而不是 FALSE,这意味着你仍然在使用面向脚本的工作流,并导入了你脚本的源代码。 下面是回到正轨的方法:

  • 清理你的全局环境并重启 R。
  • 使用 library(devtools) 重新载入 devtools 并调用 load_all() 来重新加载 regexcite 包。
  • 重新定义测试输入 x,然后再次调用 strsplit1(x, split = ",")。这应该可以正常执行!
  • 再次运行 exists("strsplit1", where = globalenv(), inherits = FALSE),此时你应该可以看到输出了 FALSE

load_all() 模拟了构建、安装和载入 regexcite 软件包的过程。 当你的软件包积累了更多的函数时,有的函数导出供用户使用,而有的没有,有的函数会互相调用而有的从依赖的其他包中调用,使用 load_all() 相比于在全局工作空间中测试函数,能够使你对于软件包的开发过程有更为准确的了解。 同样的,load_all() 对于构建、安装和载入软件包的过程,能够允许更加快速的迭代。 有关 load_all() 的更多信息,请参阅 Section 4.4

到目前为止的内容:

  • 我们以及编写了第一个函数 strsplit1(),它用于将一个字符串拆分为一个字符向量(而不是包含字符向量的列表)。
  • 我们使用 load_all() 来快速地让这个函数可以用于交互式使用,就好像我们已经构建安装了 regexcite 并通过 library(regexcite) 载入了这个软件包一样。
RStudio

RStudio 在 Build 菜单中,或者在 Build 窗格中通过依次点击 More > Load All 提供了 load_all() 的快捷调用,另外使用快捷键 Ctrl + Shift + L (Windows & Linux) 或 Cmd + Shift + L (macOS) 也可以快速调用该函数。

1.8.1 提交对 strsplit1() 的更改

如果你正在使用 Git,你可以使用你喜欢的方法来提交新的 R/strsplit1.R 文件。 我们在幕后也进行了这一操作,这是提交前后的差异。

diff --git a/R/strsplit1.R b/R/strsplit1.R
new file mode 100644
index 0000000..29efb88
--- /dev/null
+++ b/R/strsplit1.R
@@ -0,0 +1,3 @@
+strsplit1 <- function(x, split) {
+  strsplit(x, split = split)[[1]]
+}

从这一节后,我们会在每一步执行完后进行提交。 请记住这些提交在公开储存库中都是可见的。

1.9 check()

我们现在有非正式的经验证据表明 strsplit1() 工作正常。 但是,我们如何确保 regexcite 包的所有可变部分仍然工作呢? 在这么小的一个增加之后,检查其他部分似乎很愚蠢,但养成经常检查的习惯是很有益处的。

在 shell 中执行的 R CMD check 是检查 R 包是否处于完全工作状态的黄金标准。 check() 是在不离开 R 会话的情况下运行这一操作的方便方法。

请注意 check() 生成的输出相当庞大,针对交互式使用进行了优化。 我们在这里截取了一部分,并展示一个摘要。 你本地运行 check() 的输出会有所不同。

── R CMD check results ─────────────────── regexcite 0.0.0.9000 ────
Duration: 26.9s

❯ checking DESCRIPTION meta-information ... WARNING
  Non-standard license specification:
    `use_mit_license()`, `use_gpl3_license()` or friends to pick a
    license
  Standardizable: FALSE

0 errors ✔ | 1 warning ✖ | 0 notes ✔

阅读检查的输出是十分必要的!请尽可能早并经常性地解决出现的问题。 这就像在 .R.Rmd 文件上进行增量开发。 你检查每件事是否正常的时间间隔越长,就越难找到问题所在并解决问题。

在这一步中,我们收到了 1 个警告 (warnings)(0 个错误 (errors),0 个提示信息 (notes)):

Non-standard license specification:
  `use_mit_license()`, `use_gpl3_license()` or friends to pick a
  license

我们将会完全按照它所说的去做,完全解决这一问题。 你可以在 Section 4.5 中了解更多有关 check() 的信息。

RStudio

RStudio 在 Build 菜单中,或者在 Build 窗格中通过点击 Check 提供了 check() 的快捷调用,另外使用快捷键 Ctrl + Shift + E (Windows & Linux) 或 Cmd + Shift + E (macOS) 也可以快速调用该函数。

1.10 编辑 DESCRIPTION

DESCRIPTION 文件提供了关于你的软件包的元数据,这在 ?sec-description 中有完整的介绍。 现在是查看 regexcite 当前 DESCRIPTION 的好时机。 你将看到它被填充了样板内容,这些内容需要替换

要添加你自己的元数据,请进行以下编辑操作:

  • 让自己成为作者。如果你没有 ORCID,你可以忽略 comment = ... 部分。
  • TitleDescription 字段中写一些描述性文字。
RStudio

在 RStudio 中使用 Ctrl + . 然后输入 “DESCRIPTION” 来激活辅助功能,这样可以轻松打开指定文件进行编辑。 除了可以输入文件名外,还可以输入函数名。 当软件包具有大量文件时,这一功能十分便利。

当你完成上面的操作后,DESCRIPTION 的文件内容应该和下面类似:

Package: regexcite
Title: Make Regular Expressions More Exciting
Version: 0.0.0.9000
Authors@R: 
    person("Jane", "Doe", , "jane@example.com", role = c("aut", "cre"))
Description: Convenience functions to make some common tasks with string
    manipulation and regular expressions a bit easier.
License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a
    license
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.1.2

1.11 use_mit_license()

Pick a License, Any License. – Jeff Atwood

我们目前在 DESCRIPTIONLicense 字段中有一个占位符,该占位符故意设置为无效的,并提供了一种解决方案。

License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a
    license

要为软件包配置有效的许可证,请调用 use_mit_license()

use_mit_license()
#> ✔ Adding 'MIT + file LICENSE' to License
#> ✔ Writing 'LICENSE'
#> ✔ Writing 'LICENSE.md'
#> ✔ Adding '^LICENSE\\.md$' to '.Rbuildignore'

这将会把 License 字段正确地设置为 MIT 许可证,该许可证要求在 LICENSE 文件中写入版权持有人和年份。 打开新创建的 LICENSE 文件然后确保它看起来和下面的类似:

YEAR: 2024
COPYRIGHT HOLDER: regexcite authors

就像其他创建许可证的辅助函数一样,use_mit_license() 还会将完整的许可证副本放入 LICENSE.md 文件中,并将这个文件添加到 .Rbuildignore。 最好的做法是在软件包的源代码中包含完整的许可证文本,就像在 GitHub 中一样,但是 CRAN 禁止在软件包源代码中包含这一文件。 你可以在 ?sec-license 中找到更多有关添加许可证的内容。

1.12 document()

就像其他 R 函数那样,在使用 strsplit1() 时能够获得帮助文档不是很好吗? 这要求软件包具有特殊的 R 文档文件 man/strsplit1.Rd,这是一个以类似于 LaTeX 的 R 的特殊标记语言编写的文档。 幸运的是我们不需要直接编辑这类文档。

我们在源代码文件中的 strsplit1() 函数上方直接编写一个特别格式的注释,然后让一个叫做 roxygen2 的软件包来完成 man/strsplit1.Rd 的创建。 roxygen2 设计的动机和机制将在 ?sec-man 中进行介绍。

如果你使用 RStudio,则在源代码编辑器中打开 R/strsplit1.R,将光标放在 strsplit1() 函数定义中的某处。 然后依次点击 Code > Insert roxygen skeleton。 函数上方应该会出现一个非常特殊的注释模板,每行以 # 开头。 RStudio 只插入模板框架,因此你需要对其进行编辑,如下所示。

如果你不使用 RStudio,请自己创建注释。 无论使用哪种方式,你都应该修改注释,让它看起来像下面那样:

#' Split a string
#'
#' @param x A character vector with one element.
#' @param split What to split on.
#'
#' @return A character vector.
#' @export
#'
#' @examples
#' x <- "alfa,bravo,charlie,delta"
#' strsplit1(x, split = ",")
strsplit1 <- function(x, split) {
  strsplit(x, split = split)[[1]]
}

但是我们还没有完成! 我们还需要使用 document() 开始执行将这个新的 roxygen 注释转换为 man/strsplit1.Rd 的过程:

document()
#> ℹ Updating regexcite documentation
#> Setting `RoxygenNote` to "7.3.0"
#> ℹ Loading regexcite
#> Writing 'NAMESPACE'
#> Writing 'strsplit1.Rd'
RStudio

RStudio 在 Build 菜单中,或在 Build 窗格中通过 More > Document 提供了 document() 的快捷调用,另外使用快捷键 Ctrl + Shift + D (Windows & Linux) 或 Cmd + Shift + D (macOS) 也可以快速调用该函数。

你现在应该能够通过如下方式预览你的函数帮助文档:

?strsplit1

你将看到类似 “Rendering development documentation for ‘strsplit1’” 的提示信息,它告诉你,你基本上正在预览草稿文档。 也就是说,该文档存在于你的包的源代码中,但是尚未存在于已安装的包中。 事实上,我们还没有安装 regexcite,但很快就要安装了。 如果你发现 ?strsplit1 并不管用,你可能需要先调用 load_all(),然后再试一次。

注意,在正式构建和安装之前,你的软件包的文档不会正确地关联起来。 这样就省去了一些细节,比如帮助文件之间的链接和软件包索引的创建。

1.12.1 NAMESPACE 的更改

除了将 strsplit1() 的特殊注释转化为 man/strsplit1.Rd,对 document() 的调用会基于在 roxygen 注释中找到的 @export 标签来更新 NAMESPACE 文件。 打开 NAMESPACE 进行检查。 其中的内容应该如下所示:

# Generated by roxygen2: do not edit by hand

export(strsplit1)

在通过 library(regexcite) 载入 regexcite 后,NAMESPACE 中的 export 指令使得 strsplit1() 对于用户来说可用。 就像完全有可能“手工”编写 .Rd 文件一样,你可以自己显式地管理 NAMESPACE。 但我们选择将这个任务委托给 devtools(以及 roxygen2)来完成。

1.13 再次 check()

regexcite 应该可以在现在并且永远干净地通过 R CMD check,并且 0 错误 (errors),0 警告 (warnings),0 提示信息 (notes)。

── R CMD check results ─────────────────── regexcite 0.0.0.9000 ────
Duration: 24.3s

0 errors ✔ | 0 warnings ✔ | 0 notes ✔

1.14 install()

由于现在我们已经有了一个最小的可行软件包,让我们通过 install() 将 regexcite 包安装到你的库中:

── R CMD build ─────────────────────────────────────────────────────
* checking for file 'C:\Users\xiaob\AppData\Local\Temp\Rtmpk30OW9\regexcite/DESCRIPTION' ... OK
* preparing 'regexcite':
* checking DESCRIPTION meta-information ... OK
* checking for LF line-endings in source and make files and shell scripts
* checking for empty or unneeded directories
* building 'regexcite_0.0.0.9000.tar.gz'
Running "D:/R/R-4.2.1/bin/x64/Rcmd.exe" INSTALL \
  "C:\Users\xiaob\AppData\Local\Temp\Rtmpk30OW9/regexcite_0.0.0.9000.tar.gz" \
  --install-tests 
* installing to library 'D:/R/R-4.2.1/library'
* installing *source* package 'regexcite' ...
** using staged installation
** R
** byte-compile and prepare package for lazy loading
** help
*** installing help indices
** building package indices
** testing if installed package can be loaded from temporary location
** testing if installed package can be loaded from final location
** testing if installed package keeps a record of temporary installation path
* DONE (regexcite)
RStudio

RStudio 在 Build 菜单中,或在 Build 窗格中通过 Install and Restart 提供了类似功能的快捷调用,另外使用快捷键 Ctrl + Shift + B (Windows & Linux) 或 Cmd + Shift + B (macOS) 也可以快速调用这一功能。

安装完成后,我们可以像其他包一样载入和使用 regexcite。 让我们从头开始回顾我们的小型示例。 这也是重新启动 R 会话并确保你有一个干净的工作空间的好时机。

library(regexcite)

x <- "alfa,bravo,charlie,delta"
strsplit1(x, split = ",")
#> [1] "alfa"    "bravo"   "charlie" "delta"

成功!

1.15 use_testthat()

我们已经在一个示例中非正式地测试了 strsplit1()。 我们还可以将其形式化为单元测试。 这意味着我们对于特定输入的 strsplit1() 的正确结果表达了明确的期望。

首先,我们声明我们将使用 testthat 包中的 use_testthat() 来编写单元测试:

use_testthat()
#> ✔ Adding 'testthat' to Suggests field in DESCRIPTION
#> ✔ Adding '3' to Config/testthat/edition
#> ✔ Creating 'tests/testthat/'
#> ✔ Writing 'tests/testthat.R'
#> • Call `use_test()` to initialize a basic test file and open it for editing.

这将初始化软件包的单元测试机制。 它会将 Suggests: testthat 添加到 DESCRIPTION,创建目录 tests/testthat/,并添加脚本文件 tests/testthat.R。 你会注意到 testthat 可能添加了 3.0.0 的最小版本依赖要求,以及第二个 DESCRIPTION 字段 Config/testthat/edition: 3。 我们将会在 ?sec-testing-basics 中详细讨论这些细节。

然而,是否编写实际的测试仍然取决于你!

辅助函数 use_test() 用于打开并(或)创建测试文件。 你可以提供文件名,或者,如果你在 RStudio 中编辑相关的源文件,文件名将自动生成。 对于大部分人来说,如果 R/strsplit1.R 是 RStudio 中打开的文件,你只需要调用 use_test() 就好。 然而,由于本书是非交互构建的,我们必须显式地提供文件名:

use_test("strsplit1")
#> ✔ Writing 'tests/testthat/test-strsplit1.R'
#> • Edit 'tests/testthat/test-strsplit1.R'

它将会生成文件 tests/testthat/test-strsplit1.R。 如果该文件已经存在,use_test() 将只会打开它。 你会注意到在新创建的文件中有一个测试样例——你需要删除这些代码,并将以下内容添加到文件中:

test_that("strsplit1() splits a string", {
  expect_equal(strsplit1("a,b,c", split = ","), c("a", "b", "c"))
})

这将测试 strsplit1() 在分割字符串时是否给出预期的结果。

交互式地运行这个测试,就像你编写自己的测试时会做的那样。 如果无法找到 test_that()strsplit1(),那么这表示你可能需要调用 load_all()

在以后的过程中,你的测试大部分将主要通过 test()批量方式运行:

test()
#> ℹ Testing regexcite
#> ✔ | F W  S  OK | Context
#> 
#> ⠏ |          0 | strsplit1                                          
#> ⠋ |          1 | strsplit1                                          
#> ✔ |          1 | strsplit1
#> 
#> ══ Results ═════════════════════════════════════════════════════════
#> [ FAIL 0 | WARN 0 | SKIP 0 | PASS 1 ]
RStudio

RStudio 在 Build 菜单中,或者在 Build 窗格中通过 More > Test package 提供 test() 的快捷调用,另外使用快捷键 Ctrl + Shift + T (Windows & Linux) 或 Cmd + Shift + T (macOS) 也可以快速调用这一函数。

每当你使用 check() 检查软件包时,你的测试也会运行。 这样,你基本上就可以使用一些特定于自己的包的检查来扩展标准检查。 使用 covr package 跟踪该测试所执行的源代码的比例也是一个好主意。 更多细节见 ?sec-testing-design-coverage

1.16 use_package()

在开发自己的软件包时,你不可避免地会想要在自己的包中使用另一个包中的函数。 要在我们的包中声明我们需要的其他包(即我们的依赖项),以及在我们的包中使用这些包,需要使用专用于软件包的方法来完成。 请注意,如果你计划将一个包提交到 CRAN,这种方法甚至适用于一些你认为是“始终可用”的包,例如 stats::median()utils::head()

在使用 R 的正则表达式函数时,一个常见的困境是对于是否要求 perl = TRUEperl = FALSE 存在不确定性。 此外,通常(但并非总是)还有其他参数会改变模式的匹配方式,例如 fixedignore.caseinvert。 跟踪哪个函数使用了哪个参数以及参数之间如何交互是一件很困难的事,因此许多用户在不重复读文档的情况下永远不会记住这些细节。

stringr 包“提供了一组协调一致的函数,旨在使处理字符串变得尽可能简单”。 具体而言,stringr 在所有地方都使用一个正则表达式系统(ICU 正则表达式),并在每个函数中使用相同的接口来控制匹配行为,比如大小写敏感性。 一些人发现这样更容易内化知识和编程。 让我们假设你决定基于 stringr(和 stringi)构建 regexcite,而不是基于 R 的基础正则表达式函数。

首先,通过使用 use_package() 来声明你的通用意图,即使用 stringr 命名空间中的一些函数:

use_package("stringr")
#> ✔ Adding 'stringr' to Imports field in DESCRIPTION
#> • Refer to functions with `stringr::fun()`

这一命令会把 stringr 包加入到 DESCRIPTIONImports 字段。 这就是它的全部功能。

让我们重新回到 strsplit1(),使它更像 stringr 的风格。 这里有一个新的实现方案4:

str_split_one <- function(string, pattern, n = Inf) {
  stopifnot(is.character(string), length(string) <= 1)
  if (length(string) == 1) {
    stringr::str_split(string = string, pattern = pattern, n = n)[[1]]
  } else {
    character()
  }
}

请注意,我们:

  • 将函数重命名为 str_split_one(),以表示它是 stringr::str_split() 的一个封装。
  • 采用了 stringr::str_split() 的参数名称。现在我们有了 stringpattern(以及 n),而不是 xsplit
  • 引入了一些参数检查和边界情况处理。这与切换到 stringr 无关,并且在基于 strsplit() 构建的版本中同样有益。
  • 在调用 stringr::str_split() 时使用了 package::function() 形式。这指定我们要从 stringr 命名空间中调用 str_split() 函数。从另一个包中调用函数的方法不止一种,而我们在这里建议的方法在 ?sec-dependencies-in-practice 中有完整的解释。

我们应该在哪里写这个新的函数定义? 如果我们想继续遵循我们将 .R 文件命名为其中定义的函数的约定,那么我们现在需要进行一些繁琐的文件移动和重新组织的操作。 因为这在现实生活中经常出现,所以我们使用了 rename_files() 函数, 它会协调在 R/ 目录下的文件重命名以及与之相关的 test/ 目录下伴随文件的重命名。

rename_files("strsplit1", "str_split_one")
#> ✔ Moving 'R/strsplit1.R' to 'R/str_split_one.R'
#> ✔ Moving 'tests/testthat/test-strsplit1.R' to 'tests/testthat/test-str_split_one.R'

请记住:对文件名进行操作远远不够。 我们仍然需要更新这些文件的内容!

以下是 R/str_split_one.R 的更新内容。 除了更改函数定义之外,我们还更新了 roxygen 注释以反映参数的更新,并包含展示 stringr 特性的示例。

#' Split a string
#'
#' @param string A character vector with, at most, one element.
#' @inheritParams stringr::str_split
#'
#' @return A character vector.
#' @export
#'
#' @examples
#' x <- "alfa,bravo,charlie,delta"
#' str_split_one(x, pattern = ",")
#' str_split_one(x, pattern = ",", n = 2)
#'
#' y <- "192.168.0.1"
#' str_split_one(y, pattern = stringr::fixed("."))
str_split_one <- function(string, pattern, n = Inf) {
  stopifnot(is.character(string), length(string) <= 1)
  if (length(string) == 1) {
    stringr::str_split(string = string, pattern = pattern, n = n)[[1]]
  } else {
    character()
  }
}

别忘了也要更新测试文件!

以下是 tests/testthat/test-str_split_one.R 的更新内容。 除了更改函数的名称和参数之外,我们还添加了几个测试。

test_that("str_split_one() splits a string", {
  expect_equal(str_split_one("a,b,c", ","), c("a", "b", "c"))
})

test_that("str_split_one() errors if input length > 1", {
  expect_error(str_split_one(c("a,b","c,d"), ","))
})

test_that("str_split_one() exposes features of stringr::str_split()", {
  expect_equal(str_split_one("a,b,c", ",", n = 2), c("a", "b,c"))
  expect_equal(str_split_one("a.b", stringr::fixed(".")), c("a", "b"))
})

在我们导出新的 str_split_one() 进行测试之前,我们需要调用 document()。 为什么呢? 请记住 document() 做了两件主要的工作:

  1. 将我们的 roxygen 注释转换为适当的 R 文档。
  2. (重新)生成 NAMESPACE

第二个工作在这里特别重要,因为我们将不再导出 strsplit1(),而是导出新的 str_split_one()。 不要对 "Objects listed as exports, but not present in namespace: strsplit1" 的警告感到沮丧。 当你从命名空间中删除某些内容时,这种情况总是会发生。

document()
#> ℹ Updating regexcite documentation
#> ℹ Loading regexcite
#> Warning: Objects listed as exports, but not present in namespace:
#> • strsplit1
#> Writing 'NAMESPACE'
#> Writing 'str_split_one.Rd'
#> Deleting 'strsplit1.Rd'

通过 load_all() 模拟软件包安装,试试新的 str_split_one() 函数:

load_all()
#> ℹ Loading regexcite
str_split_one("a, b, c", pattern = ", ")
#> [1] "a" "b" "c"

1.17 use_github()

你已经看到我们在 regexcite 的开发过程中进行了许多提交。 你可以在 https://github.com/jennybc/regexcite 中看到指示性的提交历史记录 我们使用版本控制系统并公开开发过程的决定意味着你可以在每个开发阶段检查 regexcite 源代码的状态。 通过查看所谓的文件差异 (diff),你可以确切地看到每个 devtools 辅助函数是如何修改构成 regexcite 软件包的源文件的。

如何将你的本地 regexcite 软件包和 Git 存储库连接到 GitHub 上的配套存储库呢? 这里有三种方法:

  1. use_github() 是我们一直以来推荐使用的辅助函数。我们不会在这里演示,因为它需要在你的主机端进行一些登录凭证的设置。我们也不想在每次建立这本书的时候都删除和重建公共 regexcite 软件包储存库。
  2. 先设置 GitHub 储存库!这听起来有悖常理,但把你的工作放到 GitHub 托管的最简单方法是在那里初始化,然后使用 RStudio 在同步的本地副本中开始工作。这种方法在 Happy Git 的工作流 New project, GitHub firstExisting project, GitHub first 中进行了描述。
  3. 命令行 Git (Command line Git) 始终可以用于在事后添加远程存储库。这在 Happy Git 工作流 Existing project, GitHub last 中进行了描述。

这些方法都会将你的本地 regexcite 项目连接到公共或私有的 GitHub 储存库,你可以使用 RStudio 中内置的 Git 客户端来推送 (push) 或拉取 (pull) 它。 在 ?sec-sw-dev-practices 中,我们详细说明了为什么版本控制(例如 Git),特别是托管版本控制(例如 GitHub)值得合并到软件包开发过程中。

1.18 use_readme_rmd()

现在你的软件包已经公开到 GitHub 上了,那么 README.md 文件就很重要。 它是软件包的主页和欢迎界面,至少在你决定为它建立一个网站(见 ?sec-website),添加一份主题文档 (vignette)(见 ?sec-vignettes),或者提交到 CRAN(见 ?sec-release)之前是这样。

use_readme_rmd() 函数的作用是初始化一个基础的,可执行的 README.Rmd 以供你编辑:

use_readme_rmd()
#> ✔ Writing 'README.Rmd'
#> ✔ Adding '^README\\.Rmd$' to '.Rbuildignore'
#> • Update 'README.Rmd' to include installation instructions.
#> ✔ Writing '.git/hooks/pre-commit'

除了创建 README.Rmd 外,它还会在 .Rbuildignore 添加几行内容并创建一个 Git 预提交钩子 (pre-commit hook) 来帮助你保持 README.RmdREADME.md 的同步。

README.Rmd 中已经包含了一些部分,提示你:

  • 描述开发这个软件包的目的。
  • 提供安装说明。如果在调用 use_readme_rmd() 时检测到已配置 GitHub 远程仓库,这一节将预先填充如何从 GitHub 进行安装的说明。
  • 展示一些用法

如何填充这个内容框架? 可以从 DESCRIPTION 和任何正式或非正式的测试和示例中大量复制内容。 有内容总比没有好。 这很有帮助,因为人们可能不会安装你的软件包并仔细检查各个帮助文件来弄清楚如何使用它。

我们喜欢使用 R Markdown 编写 README,这样它就可以展示实际用法。 包含可实时运行的代码能够减少你的 README 变得过时,并且与实际的软件包不同步的可能性。

如果 RStudio 还没有像上面描述的那样做,请打开 README.Rmd 自己进行编辑。 确保它显示了 str_split_one() 的一些用法。

我们使用的 README.Rmd 可以在这里找到:README.Rmd,以下是该文件的内容:

---
output: github_document
---

<!-- README.md is generated from README.Rmd. Please edit that file -->

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.path = "man/figures/README-",
  out.width = "100%"
)
```

**NOTE: This is a toy package created for expository purposes, for the second edition of [R Packages](https://r-pkgs.org). It is not meant to actually be useful. If you want a package for factor handling, please see [stringr](https://stringr.tidyverse.org), [stringi](https://stringi.gagolewski.com/),
[rex](https://cran.r-project.org/package=rex), and
[rematch2](https://cran.r-project.org/package=rematch2).**

# regexcite

<!-- badges: start -->
<!-- badges: end -->

The goal of regexcite is to make regular expressions more exciting!
It provides convenience functions to make some common tasks with string manipulation and regular expressions a bit easier.

## Installation

You can install the development version of regexcite from [GitHub](https://github.com/) with:
      
``` r
# install.packages("devtools")
devtools::install_github("jennybc/regexcite")
```

## Usage

A fairly common task when dealing with strings is the need to split a single string into many parts.
This is what `base::strplit()` and `stringr::str_split()` do.

```{r}
(x <- "alfa,bravo,charlie,delta")
strsplit(x, split = ",")
stringr::str_split(x, pattern = ",")
```

Notice how the return value is a **list** of length one, where the first element holds the character vector of parts.
Often the shape of this output is inconvenient, i.e. we want the un-listed version.

That's exactly what `regexcite::str_split_one()` does.

```{r}
library(regexcite)

str_split_one(x, pattern = ",")
```

Use `str_split_one()` when the input is known to be a single string.
For safety, it will error if its input has length greater than one.

`str_split_one()` is built on `stringr::str_split()`, so you can use its `n` argument and stringr's general interface for describing the `pattern` to be matched.

```{r}
str_split_one(x, pattern = ",", n = 2)

y <- "192.168.0.1"
str_split_one(y, pattern = stringr::fixed("."))
```

别忘了渲染该文件以生成 README.md! 如果你尝试提交 README.Rmd 而不是 README.md,或者 README.md 已经过时了,预提交钩子 (pre-commit hook) 应该会提示你。

渲染 README.Rmd 的最好方法是使用 build_readme(),因为它会注意使用软件包的最新版本来进行渲染, 即它会从当前包的源代码中安装一个临时副本进行渲染。

build_readme()
#> ℹ Installing regexcite in temporary library
#> ℹ Building
#>   'C:/Users/xiaob/AppData/Local/Temp/Rtmpk30OW9/regexcite/README.Rmd'

你只需要简单地访问 GitHub 上的 regexcite就可以看到已经渲染好的 README.md

最后,别忘了做最后一次提交。 如果你使用了 GitHub,还需要推送至远程仓库。

1.19 最后一步:check(),然后 install()

让我们再次运行 check(),确保软件包仍然一切正常。

── R CMD check results ─────────────────── regexcite 0.0.0.9000 ────
Duration: 25.7s

0 errors ✔ | 0 warnings ✔ | 0 notes ✔

regexcite 应该没有错误 (errors)、警告 (warnings) 或提示信息 (notes)。 现在是重新构建和安装软件包的最好时机。 庆祝一下!

── R CMD build ─────────────────────────────────────────────────────
* checking for file 'C:\Users\xiaob\AppData\Local\Temp\Rtmpk30OW9\regexcite/DESCRIPTION' ... OK
* preparing 'regexcite':
* checking DESCRIPTION meta-information ... OK
* checking for LF line-endings in source and make files and shell scripts
* checking for empty or unneeded directories
* building 'regexcite_0.0.0.9000.tar.gz'
Running "D:/R/R-4.2.1/bin/x64/Rcmd.exe" INSTALL \
  "C:\Users\xiaob\AppData\Local\Temp\Rtmpk30OW9/regexcite_0.0.0.9000.tar.gz" \
  --install-tests 
* installing to library 'D:/R/R-4.2.1/library'
* installing *source* package 'regexcite' ...
** using staged installation
** R
** tests
** byte-compile and prepare package for lazy loading
** help
*** installing help indices
** building package indices
** testing if installed package can be loaded from temporary location
** testing if installed package can be loaded from final location
** testing if installed package keeps a record of temporary installation path
* DONE (regexcite)

请随意访问 GitHub 上的 regexcite 软件包,它看起来和这里开发的完全一样。 提交历史记录反映了每一个单独的步骤,因此你可以使用 diffs 来查看在软件包开发过程中哪些文件被添加和修改。 本书的其余部分将更详细地介绍你在这里看到的每一个步骤以及其它更多的内容。

1.20 回顾

这一章的目的是给你一个典型的软件包开发流程的印象,可以总结为 Figure 1.1 所示的流程图。 除了 GitHub Actions 外,你在这里看到的所有内容都已经在这一章中提到过了,你将在 ?sec-sw-dev-practices-gha 中学到更多。

图中展示了在 devtools 工作流中的 4 个关键函数:load_all(), test(), document() 和 check()。 每个函数都属于一个或多个由箭头指示的循环,这些循环描述了 编辑代码、编写测试或撰写文档的典型过程,然后 尝试运行代码、运行测试或预览文档。 check() 函数则连接到外部的 `git commit`, `git push` 和 GitHub Actions 过程。
Figure 1.1: devtools 软件包开发工作流程。

下面是对本章中提到的关键函数的回顾,根据它们在开发流程中的角色进行了粗略的组织。

这些函数用于配置软件包的各个部分,通常对于每个软件包只需要调用一次:

当你添加函数、测试代码或依赖项时,将会定期调用这些函数:

在开发过程中,你将每天或每小时频繁调用这些函数:


  1. 译者注:可能指能够执行例如安装、构建文档等标准工作流程。↩︎

  2. 译者注:原文为 pane,准确来说是 tab。↩︎

  3. 译者注:原文为 pane,准确来说是 tab。↩︎

  4. 回想一下,这个例子是如此鼓舞人心,以至于它现在是 stringr 包中的一个真正的函数: `stringr::str_split_1()`!↩︎