hell脚本是一个命令语言,面向的是操作系统执行。如果写过shell脚本的话,应该体会过编写过程的痛苦。因为shell并不是一个编程语言,并不支持常见的数组,JSON等数据结构,也不支持面向对象编程的开发方法,因此对开发人员很不友好。
目前针对这种情况,大家一般会用shell调用node执行JS脚本,真正的处理逻辑放在JS脚本中处理。现在谷歌推出了 ZX NPM包,它能够用JS编写shell脚本。
那如何使用呢?
npm install -g zx
安装完后,在终端中输入 zx 命令检查安装是否成功。
新建zx脚本文件:test.mjs
#!/usr/bin/env zx
const branch=await $`git branch --show-current`
console.log(`Current branch: ${branch}`)
第一行是指定脚本的执行器。
$ 是内置的函数,能够执行命令并配合 await 返回执行结果。其他的写法都和JS毫无差别。
zx ./test.mjs
或者:
chmod +x ./test.mjs
./test.mjs
控制台就会输出当前的分支。
上面只是小试牛刀,zx 的强大远不止如此。由于 zx 在内部实现了 Bash 的解释器,所以可以执行全部的shell命令。另外 zx 还内置很多nodejs模块,比如 fs, os,fetch等。所以可以直接在脚本中使用这些模块。
另外作为TS编写的库,全部的JS语法都能够支持。包括但不限于 数组,Promise,class等。
下面再举一个例子:
let resp=await fetch('http://wttr.in')
if (resp.ok) {
console.log(await resp.text())
}
let hosts=[...]
await Promise.all(hosts.map(host=>
$`rsync -azP ./src ${host}:/var/www`
))
try {
await $`exit 1`
} catch (p) {
console.log(`Exit code: ${p.exitCode}`)
console.log(`Error: ${p.stderr}`)
}
总结一下,zx 的最大优点是结合了Bash和JavaScript,解决了shell脚本复杂逻辑编程的问题。同时也让对shell不熟悉的开发者也能用JS完成shell脚本的开发,而且更加灵活高效。
如果你还有更多问题,可以参考NPM仓库 zx 包的介绍,或者访问其github地址。
欢迎帮忙点赞,评论,转发~
过很多 bash 脚本的人都知道,bash 的坑不是一般的多。 其实 bash 本身并不是一个很严谨的语言,但是很多时候也不得不用。以下总结了一些鹅厂程序员在编写可靠 bash 脚本的一些小 tips。
在写脚本时,在一开始(Shebang 之后)就加上这一句,或者它的缩略版:
set -xeuo pipefail
这能避免很多问题,更重要的是能让很多隐藏的问题暴露出来。
下面说明每个参数的作用,以及一些例外的处理方式 :
-x : 在执行每一个命令之前把经过变量展开之后的命令打印出来。
这个对于 debug 脚本、输出 Log 时非常有用。 正式运行的脚本也可以不加。
-e : 遇到一个命令失败(返回码非零)时,立即退出。
bash 跟其它的脚本语言最大的不同点之一,应该就是遇到异常时继续运行下一条命令。 这在很多时候会遇到意想不到的问题。加上 -e ,会让 bash 在遇到一个命令失败时,立即退出。
如果有时确实需要忽略个别命令的返回码,可以用 || true 。如:
some_cmd || true # 即使some_cmd失败了,仍然会继续运行
some_cmd || RET=$? # 或者可以这样来收集some_cmd的返回码,供后面的逻辑判断使用
但是在管道串起多条命令的情况下,只有最后一条命令失败时才会退出。如果想让管道中任意一条命令失败就退出,就要用后面提到的-o pipefail 了。
加-e 有时候可能会不太方便,动不动就退出。但觉得还是应该坚持所谓的fail-fast 原则,也就是有异常时停止正常运行,而不是继续尝试运行可能存在缺陷的过程。如果有命令可以明确忽略异常,那可以用上面提到的 || true 等方式明确地忽略之。
-u :试图使用未定义的变量,就立即退出。
如果在 bash 里使用一个未定义的变量,默认是会展开成一个空串。有时这种行为会导致问题,比如:
rm -rf $MYDIR/data
如果 MYDIR 变量因为某种原因没有赋值,这条命令就会变成 rm -rf /data 。 这就比较搞笑了。。 使用 -u 可以避免这种情况。
但有时候在已经设置了-u 后,某些地方还是希望能把未定义变量展开为空串,可以这样写:
${SOME_VAR:-}
# bash变量展开语法,可以参考:
https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
-o pipefail : 只要管道中的一个子命令失败,整个管道命令就失败。
pipefail 与-e 结合使用的话,就可以做到管道中的一个子命令失败,就退出脚本。
在一些场景中,我们通常不希望一个脚本有多个实例在同时运行。比如用 crontab 周期性运行脚本时,有时不希望上一个轮次还没运行完,下一个轮次就开始运行了。 这时可以用 flock 命令来解决。 flock 通过文件锁的方式来保证独占运行,并且还有一个好处是进程退出时,文件锁也会自动释放,不需要额外处理。
用法 1: 假设你的入口脚本是 myscript.sh,可以新建一个脚本,通过 flock 来运行它:
# flock --wait 超时时间 -e 锁文件 -c "要执行的命令"
# 例如:
flock --wait 5 -e "lock_myscript" -c "bash myscript.sh"
用法 2: 也可以在原有脚本里使用 flock。 可以把文件打开为一个文件描述符,然后使用 flock 对它上锁(flock 可以接受文件描述符参数)。
exec 123<>lock_myscript # 把lock_myscript打开为文件描述符123
flock --wait 5 123 || { echo 'cannot get lock, exit'; exit 1; }
我们的脚本通常会启动好多子脚本和子进程,当父脚本意外退出时,子进程其实并不会退出,而是继续运行着。 如果脚本是周期性运行的,有可能发生一些意想不到的问题。
在 stackoverflow 上找到的一个方法,原理就是利用 trap 命令在脚本退出时 kill 掉它整个进程组。 把下面的代码加在脚本开头区,实测管用:
trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
不过如果父进程是用 SIGKILL (kill -9) 杀掉的,就不行了。因为 SIGKILL 时,进程是没有机会运行任何代码的。
有时候需要对命令设置一个超时时间。这时可以使用 timeout 命令,用法很简单:
timeout 600s some_command arg1 arg2
命令在超时时间内运行结束时,返回码为 0,否则会返回一个非零返回码。
timeout 在超时时默认会发送 TERM 信号,也可以用 -s 参数让它发送其它信号。
有时候我们会用到把好多条命令用管道串在一起的情况。如 cmd1 | cmd2 | cmd3 | ...这样会让问题变得难以排查,因为中间数据我们都看不到。
如果改成这样的格式:
cmd1 > out1.dat
cat out1 | cmd2 > out2.dat
cat out2 | cmd3 > out3.dat
性能又不太好,因为这样 cmd1, cmd2, cmd3 是串行运行的,这时可以用 tee 命令:
cmd1 | tee out1.dat | cmd2 | tee out2.dat | cmd3 > out3.dat
言
在这篇文章中,我们将学习谷歌的 zx 库提供了什么,以及我们如何使用它来用 Node.js 编写 shell 脚本。然后,我们将学习如何通过构建一个命令行工具来使用 zx 的功能,帮助我们为新的 Node.js 项目引导配置。
编写 Shell 脚本的问题
创建一个由 Bash 或者 zsh 执行的 shell 脚本,是自动化重复任务的好方法。Node.js 似乎是编写 shell 脚本的理想选择,因为它为我们提供了许多核心模块,并允许我们导入任何我们选择的库。它还允许我们访问 JavaScript 提供的语言特性和内置函数。
如果你尝试编写运行在 Node.js 中的 shell 脚本,你会发现这没有你想象中的那么顺利。你需要为子进程编写特殊的处理程序,注意转义命令行参数,然后最终与 stdout(标准输出)和 stderr(标准错误)打交道。这不是特别直观,而且会使 shell 脚本变得相当笨拙。
Bash shell 脚本语言是编写 shell 脚本的普遍选择。不需要编写代码来处理子进程,而且它有内置的语言特性来处理 stdout 和 stderr。但是用 Bash 编写 shell 脚本也不是那么容易。语法可能相当混乱,使得它实现逻辑,或者处理诸如提示用户输入的事情非常困难。
谷歌的 zx 库有助于让使用 Node.js 编写的 shell 脚本变得高效和舒适。
前置条件
往下阅读之前,有几个前置条件需要遵循:
本文中的所有代码都可以从 GitHub https://link.segmentfault.com/?enc=ysCUhsc%2BhqUmtqCo55t8jw%3D%3D.aWhjUaPje6eTlkcFFdhW%2FeIVYyAz5G%2FoPbGuXjsxlpcJphMKguwz3NoHWQ9o2vDb47Nfnm9kpIP6Ol5r6Euc8A%3D%3D 上获得。
zx 如何运作
Google 的 zx 提供了创建子进程的函数,以及处理这些进程的 stdout 和 stderr 的函数。我们将使用的主要函数是$函数。下面是它的一个实际例子:
import { $ } from "zx";
await $`ls`;
下面是执行上述代码的输出:
$ ls
bootstrap-tool
hello-world
node_modules
package.json
README.md
typescript
上面的例子中的 JavaScript 语法可能看起来有点古怪。它使用了一种叫做带标签的模板字符串 https://link.segmentfault.com/?enc=VUkodq5er%2Fynbhfl3MUQZA%3D%3D.%2Flx6oaDCVK4XuYyYLqvDi2QMWjCwW1jKBvNQgfaGG0AVwpl7I2CYD4sJYHuonDSA6jj1qSSypc0aGVO%2BYuBMUiibG6pBkVwusg%2Bai1hbMXetqlwMTWVUEAbtabMCbXIs 的语言特性。它在功能上与编写 await $("ls")相同。
谷歌的 zx 提供了其他几个实用功能,使编写 shell 脚本更容易。比如:
除了 zx 提供的实用功能外,它还为我们提供了几个流行的库,比如:
chalk。https://link.segmentfault.com/?enc=%2FL15Y8OQNrp05Scx6N4iaQ%3D%3D.r8AZkfcE1Ye%2BlNUznFA9RNJMhfyM0lttTiD0TmqjJQwoi7zjQjs5YiLjI%2FWDEooY 这个库允许我们为脚本的输出添加颜色。
minimist。https://link.segmentfault.com/?enc=5tMQ5d6qJ4STEHZeuiN0MA%3D%3D.x9GPOpXVZp5TGKknZCkwA10QP%2Ftw%2BW7fwnpYGg%2BnlJPJLK3RboT3jqy5WuPLFwPZ 一个解析命令行参数的库。然后它们在 argv 对象下被暴露出来。
fetch。https://link.segmentfault.com/?enc=71LC44tCu%2FZoLOY2B7MfuA%3D%3D.GO%2FMi37T0KTmj8UXau9xdFUBiIk1I%2F8M%2FHk2hZYrhRb%2Fd3Vl0bzlr05hanNxzuVb Fetch API 的 Node.js 实现。我们可以用它来进行 HTTP 请求。
fs-extra。https://link.segmentfault.com/?enc=cPZzXniTRFJdbq87FdwLWw%3D%3D.IsmhSUEjIcZAqw8FDidbUnNhhIVPT1gsFSo%2BchiXCAs9AaxUVT%2FEgbIvswUDVmK2 一个暴露 Node.js 核心 fs 模块的库,以及一些额外的方法,使其更容易与文件系统一起工作。
现在我们知道了 zx 给了我们什么,让我们用它创建第一个 shell 脚本。
zx 如何使用
首先,我们先创建一个新项目:
mkdir zx-shell-scripts
cd zx-shell-scripts
npm init --yes
然后安装 zx 库:
npm install --save-dev zx
注意:zx 的文档建议用 npm 全局安装该库。通过将其安装为我们项目的本地依赖,我们可以确保 zx 总是被安装,并控制 shell 脚本使用的版本。
顶级 await
为了在 Node.js 中使用顶级 await,也就是 await 位于 async 函数的外部,我们需要在 ES 模块的模式下编写代码,该模式支持顶级 await。
我们可以通过在 package.json 中添加 "type": "module" 怎么怎么来表明项目中的所有模块都是 ES 模块。或者我们可以将单个脚本的文件扩展名设置为 .mjs 。在本文的例子中,我们将使用 .mjs 文件扩展名。
运行命令并捕获输出
创建一个新脚本,将其命名为 hello-world.mjs 。我们将添加一个 Shebang 行,它告诉操作系统(OS)的内核要用 node 程序运行该脚本:
#! /usr/bin/env node
然后,我们添加一些代码,使用 zx 来运行命令。
在下面的代码中,我们运行命令执行 ls 程序。ls 程序将列出当前工作目录(脚本所在的目录)中的文件。我们将从命令的进程中捕获标准输出,将其存储在一个变量中,然后打印到终端:
// hello-world.mjs
import { $ } from "zx";
const output = (await $`ls`).stdout;
console.log(output);
注意:zx 文档建议把/usr/bin/env zx 放在我们脚本的 shebang 行中,但我们用/usr/bin/env node 代替。这是因为我们已经安装 zx,并作为项目的本地依赖。然后我们明确地从 zx 包中导入我们想要使用的函数和对象。这有助于明确我们脚本中使用的依赖来自哪里。
我们使用 chmod 来让脚本可执行:
chmod u+x hello-world.mjs
运行项目:
./hello-world.mjs
可以看到如下输出:
$ ls
hello-world.mjs
node_modules
package.json
package-lock.json
README.md
hello-world.mjs
node_modules
package.json
package-lock.json
README.md
你会注意到:
zx 默认以 verbose 模式运行。它将输出你传递给$函数的命令,同时也输出该命令的标准输出。我们可以通过在运行 ls 命令前加入以下一行代码来改变这种行为:
$.verbose = false;
大多数命令行程序,如 ls,会在其输出的结尾处输出一个新行字符,以使输出在终端中更易读。这对可读性有好处,但由于我们要将输出存储在一个变量中,我们不希望有这个额外的新行。我们可以用 JavaScript String#trim()函数把它去掉:
- const output = (await $`ls`).stdout;
+ const output = (await $`ls`).stdout.trim();
再次运行脚本,结果看起来好很多:
hello-world.mjs
node_modules
package.json
package-lock.json
引入 TypeScript
如果我们想在 TypeScript 中编写使用 zx 的 shell 脚本,有几个微小的区别我们需要加以说明。
注意:TypeScript 编译器提供了大量的配置选项,允许我们调整它如何编译我们的 TypeScript 代码。考虑到这一点,下面的 TypeScript 配置和代码是为了在大多数 TypeScript 版本下工作。
首先,安装需要运行 TypeScript 代码的依赖:
npm install --save-dev typescript ts-node
ts-node 包提供了一个 TypeScript 执行引擎,让我们能够转译和运行 TypeScript 代码。
需要创建 tsconfig.json 文件包含下面的配置:
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs"
}
}
创建新的脚本,并命名为 hello-world-typescript.ts。首先,添加 Shebang 行,告诉 OS 内核使用 ts-node 程序来运行我们的脚本:
#! ./node_modules/.bin/ts-node
为了在我们的 TypeScript 代码中使用 await 关键字,我们需要把它包装在一个立即调用函数表达式(IIFE)中,正如 zx 文档所建议的那样:
// hello-world-typescript.ts
import { $ } from "zx";
void (async function () {
await $`ls`;
})();
然后需要让脚本可执行:
chmod u+x hello-world-typescript.ts
运行脚本:
./hello-world-typescript.ts
可以看到下面的输出:
$ ls
hello-world-typescript.ts
node_modules
package.json
package-lock.json
README.md
tsconfig.json
在 TypeScript 中用 zx 编写脚本与使用 JavaScript 相似,但需要对我们的代码进行一些额外的配置和包装。
构建项目启动工具
现在我们已经学会了用谷歌的 zx 编写 shell 脚本的基本知识,我们要用它来构建一个工具。这个工具将自动创建一个通常很耗时的过程:为一个新的 Node.js 项目的配置提供引导。
我们将创建一个交互式 shell 脚本,提示用户输入。它还将使用 zx 内置的 chalk 库,以不同的颜色高亮输出,并提供一个友好的用户体验。我们的 shell 脚本还将安装新项目所需的 npm 包,所以它已经准备好让我们立即开始开发。
准备开始
首先创建一个名为 bootstrap-tool.mjs 的新文件,并添加 shebang 行。我们还将从 zx 包中导入我们要使用的函数和模块,以及 Node.js 核心 path 模块:
#! /usr/bin/env node
// bootstrap-tool.mjs
import { $, argv, cd, chalk, fs, question } from "zx";
import path from "path";
与我们之前创建的脚本一样,我们要使我们的新脚本可执行:
chmod u+x bootstrap-tool.mjs
我们还将定义一个辅助函数,用红色文本输出一个错误信息,并以错误退出代码 1 退出 Node.js 进程:
function exitWithError(errorMessage) {
console.error(chalk.red(errorMessage));
process.exit(1);
}
当我们需要处理一个错误时,我们将通过我们的 shell 脚本在各个地方使用这个辅助函数。
检查依赖
我们要创建的工具需要使用三个不同程序来运行命令:git、node 和 npx。我们可以使用 which 库来帮助我们检查这些程序是否已经安装并可以使用。
首先,我们需要安装 which:
npm install --save-dev which
然后引入它:
import which from "which";
然后创建一个使用它的 checkRequiredProgramsExist 函数:
async function checkRequiredProgramsExist(programs) {
try {
for (let program of programs) {
await which(program);
}
} catch (error) {
exitWithError(`Error: Required command ${error.message}`);
}
}
上面的函数接受一个程序名称的数组。它循环遍历数组,对每个程序调用 which 函数。如果 which 找到了程序的路径,它将返回该程序。否则,如果该程序找不到,它将抛出一个错误。如果有任何程序找不到,我们就调用 exitWithError 辅助函数来显示一个错误信息并停止运行脚本。
我们现在可以添加一个对 checkRequiredProgramsExist 的调用,以检查我们的工具所依赖的程序是否可用:
await checkRequiredProgramsExist(["git", "node", "npx"]);
添加目标目录选项
由于我们正在构建的工具将帮助我们启动新的 Node.js 项目,因此我们希望在项目的目录中运行我们添加的任何命令。我们现在要给脚本添加一个 --directory 命令行参数。
zx 内置了 minimist 包,它能够解析传递给脚本的任何命令行参数。这些被解析的命令行参数被 zx 包作为 argv 提供:
让我们为名为 directory 的命令行参数添加一个检查:
let targetDirectory = argv.directory;
if (!targetDirectory) {
exitWithError("Error: You must specify the --directory argument");
}
如果 directory 参数被传递给了我们的脚本,我们要检查它是否是已经存在的目录的路径。我们将使用 fs-extra 提供的 fs.pathExists 方法:
targetDirectory = path.resolve(targetDirectory);
if (!(await fs.pathExists(targetDirectory))) {
exitWithError(`Error: Target directory '${targetDirectory}' does not exist`);
}
如果目标路径存在,我们将使用 zx 提供的 cd 函数来切换当前的工作目录:?
cd(targetDirectory);
如果我们现在在没有--directory 参数的情况下运行脚本,我们应该会收到一个错误:
$ ./bootstrap-tool.mjs
Error: You must specify the --directory argument
检查全局 Git 设置
稍后,我们将在项目目录下初始化一个新的 Git 仓库,但首先我们要检查 Git 是否有它需要的配置。我们要确保提交会被 GitHub 等代码托管服务正确归类。
为了做到这一点,这里创建一个 getGlobalGitSettingValue 函数。它将运行 git config 命令来检索 Git 配置设置的值:
async function getGlobalGitSettingValue(settingName) {
$.verbose = false;
let settingValue = "";
try {
settingValue = (
await $`git config --global --get ${settingName}`
).stdout.trim();
} catch (error) {
// Ignore process output
}
$.verbose = true;
return settingValue;
}
你会注意到,我们正在关闭 zx 默认设置的 verbose 模式。这意味着,当我们运行 git config 命令时,该命令和它发送到标准输出的任何内容都不会被显示。我们在函数的结尾处将 verbose 模式重新打开,这样我们就不会影响到我们稍后在脚本中添加的任何其他命令。
现在我们添加 checkGlobalGitSettings 函数,该函数接收 Git 设置名称组成的数组。它将循环遍历每个设置名称,并将其传递给 getGlobalGitSettingValue 函数以检索其值。如果设置没有值,将显示警告信息:
async function checkGlobalGitSettings(settingsToCheck) {
for (let settingName of settingsToCheck) {
const settingValue = await getGlobalGitSettingValue(settingName);
if (!settingValue) {
console.warn(
chalk.yellow(`Warning: Global git setting '${settingName}' is not set.`)
);
}
}
}
让我们给 checkGlobalGitSettings 添加一个调用,检查 user.name 和 user.email 的 Git 设置是否已经被设置:
await checkGlobalGitSettings(["user.name", "user.email"]);
初始化 Git 仓库
我们可以通过添加以下命令在项目目录下初始化一个新的 Git 仓库:
await $`git init`;
生成 package.json
每个 Node.js 项目都需要 package.json 文件。这是我们为项目定义元数据的地方,指定项目所依赖的包,以及添加实用的脚本。
在我们为项目生成 package.json 文件之前,我们要创建几个辅助函数。第一个是 readPackageJson 函数,它将从项目目录中读取 package.json 文件:
async function readPackageJson(directory) {
const packageJsonFilepath = `${directory}/package.json`;
return await fs.readJSON(packageJsonFilepath);
}
然后我们将创建一个 writePackageJson 函数,我们可以用它来向项目的 package.json 文件写入更改:
async function writePackageJson(directory, contents) {
const packageJsonFilepath = `${directory}/package.json`;
await fs.writeJSON(packageJsonFilepath, contents, { spaces: 2 });
}
我们在上面的函数中使用的 fs.readJSON 和 fs.writeJSON 方法是由 fs-extra 库提供的。
在定义了 package.json 辅助函数后,我们可以开始考虑 package.json 文件的内容。
Node.js 支持两种模块类型:
Node.js 生态系统正在逐步采用 ES 模块,这在客户端 JavaScript 中是很常见的。当事情处于过渡阶段时,我们需要决定我们的 Node.js 项目默认使用 CJS 模块还是 ESM 模块。让我们创建一个 promptForModuleSystem 函数,询问这个新项目应该使用哪种模块类型:
async function promptForModuleSystem(moduleSystems) {
const moduleSystem = await question(
`Which Node.js module system do you want to use? (${moduleSystems.join(
" or "
)}) `,
{
choices: moduleSystems,
}
);
return moduleSystem;
}
上面函数使用的 question 函数由 zx 提供。
现在我们将创建一个 getNodeModuleSystem 函数,以调用 promptForModuleSystem 函数。它将检查所输入的值是否有效。如果不是,它将再次询问:
async function getNodeModuleSystem() {
const moduleSystems = ["module", "commonjs"];
const selectedModuleSystem = await promptForModuleSystem(moduleSystems);
const isValidModuleSystem = moduleSystems.includes(selectedModuleSystem);
if (!isValidModuleSystem) {
console.error(
chalk.red(
`Error: Module system must be either '${moduleSystems.join(
"' or '"
)}'\n`
)
);
return await getNodeModuleSystem();
}
return selectedModuleSystem;
}
现在我们可以通过运行 npm init 命令生成我们项目的 package.json 文件:
await $`npm init --yes`;
然后我们将使用 readPackageJson 辅助函数来读取新创建的 package.json 文件。我们将询问项目应该使用哪个模块系统,并将其设置为 packageJson 对象中的 type 属性值,然后将其写回到项目的 package.json 文件中:
const packageJson = await readPackageJson(targetDirectory);
const selectedModuleSystem = await getNodeModuleSystem();
packageJson.type = selectedModuleSystem;
await writePackageJson(targetDirectory, packageJson);
提示:当你用--yes 标志运行 npm init 时,要想在 package.json 中获得合理的默认值,请确保你设置了 npminit-*的配置设置 https://link.segmentfault.com/?enc=X0aVKO8sVtj0bgZhFmgjVw%3D%3D.hxYbJxX5X2odlcPpk5MdIVLkT7ESAsFMhFnFSRBftI2c2BjNTUCtV1gwdXnpxWGI 。
安装所需项目依赖
为了使运行我们的启动工具后能够轻松地开始项目开发,我们将创建一个 promptForPackages 函数,询问要安装哪些 npm 包:
async function promptForPackages() {
let packagesToInstall = await question(
"Which npm packages do you want to install for this project? "
);
packagesToInstall = packagesToInstall
.trim()
.split(" ")
.filter((pkg) => pkg);
return packagesToInstall;
}
为了防止我们在输入包名时出现错别字,我们将创建一个 identifyInvalidNpmPackages 函数。这个函数将接受一个 npm 包名数组,然后运行 npm view 命令来检查它们是否存在:
async function identifyInvalidNpmPackages(packages) {
$.verbose = false;
let invalidPackages = [];
for (const pkg of packages) {
try {
await $`npm view ${pkg}`;
} catch (error) {
invalidPackages.push(pkg);
}
}
$.verbose = true;
return invalidPackages;
}
让我们创建一个 getPackagesToInstall 函数,使用我们刚刚创建的两个函数:
async function getPackagesToInstall() {
const packagesToInstall = await promptForPackages();
const invalidPackages = await identifyInvalidNpmPackages(packagesToInstall);
const allPackagesExist = invalidPackages.length === 0;
if (!allPackagesExist) {
console.error(
chalk.red(
`Error: The following packages do not exist on npm: ${invalidPackages.join(
", "
)}\n`
)
);
return await getPackagesToInstall();
}
return packagesToInstall;
}
如果有软件包名称不正确,上面的函数将显示一个错误,然后再次询问要安装的软件包。
一旦我们得到需要安装的有效包列表,就可以使用 npm install 命令来安装它们:
const packagesToInstall = await getPackagesToInstall();
const havePackagesToInstall = packagesToInstall.length > 0;
if (havePackagesToInstall) {
await $`npm install ${packagesToInstall}`;
}
为工具生成配置
创建项目配置是我们用项目启动工具自动完成的最佳事项。首先,让我们添加一个命令来生成一个.gitignore 文件,这样我们就不会意外地提交我们不希望在 Git 仓库中出现的文件:
await $`npx gitignore node`;
上面的命令使用 gitignore https://link.segmentfault.com/?enc=V%2FIoxipE2WxmNECDRNKMqg%3D%3D.7Y4d34n%2BoUVlJkYEiJt3NLa9RHn2pYtKq%2BHpO67HIQjfjKAHIxcMynZzTudktHaV 包,从 GitHub 的 gitignore https://link.segmentfault.com/?enc=Szh56TKqT6gYmrJidM7ZXg%3D%3D.BK7Vf5F73GaplJKeN48OsobM6U4EPriatZOr3dBhsSVZIuIUiktyqfS1jhLLtQ7o 模板中拉取 Node.js 的.gitignore 文件。
为了生成我们的 EditorConfig https://link.segmentfault.com/?enc=dGD5%2BEyCTqLfqDSif7%2FHgQ%3D%3D.xgNWBjR6m%2FIUFlG1UmM3CYX2R%2BLXFN531lZhlvCRRWs%3D、Prettier https://link.segmentfault.com/?enc=n7clHFqLMZZala0zu1JjdQ%3D%3D.jivAg4cL8HkSAhuurMUHrlfhGGILOpveTQR0rjCRXyg%3D 和 ESLint https://link.segmentfault.com/?enc=J4jXEAmmBQ83%2FWSrOtTsAQ%3D%3D.b%2FKC0kT%2FZWZQ9KBItXDMQHWJBzc5aW9hUZsxWHBsMY4%3D 配置文件,我们将使用一个叫做 Mrm 的命令行工具。
全局安装我们需要的 mrm 依赖项:
npm install --global mrm mrm-task-editorconfig mrm-task-prettier mrm-task-eslint
然后添加 mrm 命令行生成配置文件:
await $`npx mrm editorconfig`;
await $`npx mrm prettier`;
await $`npx mrm eslint`;
Mrm 负责生成配置文件,以及安装所需的 npm 包。它还提供了大量的配置选项,允许我们调整生成的配置文件以符合我们的个人偏好。
生成 README
我们可以使用我们的 readPackageJson 辅助函数,从项目的 package.json 文件中读取项目名称。然后我们可以生成一个基本的 Markdown 格式的 README,并将其写入 README.md 文件中:
const { name: projectName } = await readPackageJson(targetDirectory);
const readmeContents = `# ${projectName}
...
`;
await fs.writeFile(`${targetDirectory}/README.md`, readmeContents);
在上面的函数中,我们正在使用 fs-extra 暴露的 fs.writeFile 的 promise 变量。
提交项目骨架
最后,是时候提交我们用 git 创建的项目骨架了:
await $`git add .`;
await $`git commit -m "Add project skeleton"`;
然后我们将显示一条消息,确认我们的新项目已经成功启动:
console.log(
chalk.green(
`\n?? The project ${projectName} has been successfully bootstrapped!\n`
)
);
console.log(chalk.green(`Add a git remote and push your changes.`));
启动新项目
mkdir new-project
./bootstrap-tool.mjs --directory new-project
并观看我们所做的一切。
总结
在这篇文章中,我们已经学会了如何在 Node.js 中借助 Google 的 zx 库来创建强大的 shell 脚本。我们使用了它提供的实用功能和库来创建一个灵活的命令行工具。
到目前为止,我们所构建的工具只是一个开始。这里有一些功能点子,你可能想尝试自己添加:
自动创建目标目录。如果目标目录还不存在,则提示用户并询问他们是否想要为他们创建该目录。
开源卫生。问问用户他们是否在创建一个将是开源的项目。如果是的话,运行命令来生成许可证和贡献者文件。
自动创建 GitHub 上的仓库。添加使用 GitHub CLI 的命令,在 GitHub 上创建一个远程仓库。一旦用 Git 提交了初始骨架,新项目就可以被推送到这个仓库。
本文中的所有代码都可以在 GitHub 上找到。
本文译自:https://link.segmentfault.com/?enc=5Z%2ByLQNgua%2FY5gnguxXllg%3D%3D.SeNINYETng%2BH6uZupDjhsYNRqM5GWB5jXVDlvMdMqsO9u3CmWfwe8DwT48iAvK%2Fsm8pflQ8jbxHc1sB281%2B6pA%3D%3D
作者:Simon Plenderleith
以上就是本文的所有内容。如果对你有所帮助,欢迎点赞、收藏、转发~
*请认真填写需求信息,我们会在24小时内与您取得联系。