在本文中,我们将了解 Google 的 zx 库提供了什么,以及我们如何使用它来使用 Node.js 编写 shell 脚本。然后,我们将通过构建一个命令行工具来学习如何使用 zx 的功能,该工具可以帮助我们修复新的 Node.js 项目引导配置。
编写 Shell 脚本:问题
创建一个 shell 脚本——一个由诸如 Bash 或 zsh 之类的 shell 执行的脚本——可以是自动化重复任务的好方法。Node.js 似乎是编写 shell 脚本的理想选择,因为它为我们提供了许多核心模块,并允许我们导入我们选择的任何库。它还使我们能够访问 JavaScript 提供的语言特性和内置函数。
但是,如果您尝试编写一个在 Node.js 下运行的 shell 脚本,您可能会发现它并不像您希望的那样流畅。您需要为子进程编写特殊处理,注意转义命令行参数,然后最终弄乱stdout(标准输出)和stderr(标准错误)。它不是特别直观,并且会使用 shell 脚本非常尴尬。
Bash shell 脚本语言是编写 shell 脚本的流行选择。无需编写代码来处理子进程,并且它具有用于处理stdout和stderr. 但是用 Bash 编写 shell 脚本也不是那么容易。语法可能很混乱,难以实现逻辑,或处理诸如提示用户输入之类的事情。
Google 的 zx库有助于使用 Node.js 高效且愉快地编写 shell 脚本。
跟随要求
跟随本文有一些要求:
- 理想情况下,您应该熟悉 JavaScript 和 Node.js 的基础知识。
- 您需要能够在终端中轻松运行命令。
- 您需要安装Node.js >= v14.13.1。
本文中的所有代码都可以在 GitHub 上找到。
Google 的 zx 是如何工作的?
Google 的 zx 提供了封装子进程的创建以及处理stdout这些stderr进程的函数。我们将使用的主要函数是$函数。这是它的一个例子:
import { $ } from "zx";await $`ls`;
这是执行该代码的输出:
$ ls
bootstrap-tool
hello-world
node_modules
package.json
README.md
typescript
上面示例中的 JavaScript 语法可能看起来有点古怪。它使用一种称为标记模板文字的语言功能。它在功能上与写作相同await $("ls")。
Google 的 zx 提供了其他几个实用函数来简化 shell 脚本编写,例如:
- cd(). 这允许我们更改当前的工作目录。
- question(). 这是 Node.js readline模块的包装器。它可以直接提示用户输入。
除了 zx 提供的实用功能外,它还为我们提供了几个流行的库,例如:
- 粉笔。这个库允许我们为脚本的输出添加颜色。
- 极简主义。解析命令行参数的库。然后将它们暴露在argv物体下。
- 取。Fetch API的流行 Node.js 实现。我们可以使用它来发出 HTTP 请求。
- fs-额外。一个库存,它公开了 Node.js 核心fs 模块,以及许多其他方法,可以更轻松地使用文件系统。
现在我们知道 zx 给了我们什么,让我们用它创建我们的第一个 shell 脚本。
使用 Google 的 zx 的 Hello World
首先,让我们创建一个新的项目:
mkdir zx-shell-scripts
cd zx-shell-scriptsnpm init --yes
然后我们可以安装zx库:
npm install --save-dev zx
注意:zx文档建议使用 npm 全局安装库。通过将其安装为我们项目的本地依赖项,我们可以确保始终安装 zx,并控制我们的 shell 脚本使用的版本。
顶层await
为了await在 Node.js中使用顶层——在函数await之外async——我们需要在支持顶层的ECMAScript (ES) 模块await中编写代码。"type": "module"我们可以通过添加我们的 来表明一个项目中的所有模块都是 ES 模块package.json,或者我们可以将单个脚本的文件扩展名设置为.mjs. 我们将为.mjs本文中的示例使用文件扩展名。
运行命令并捕获其输出
让我们创建一个名为hello-world.mjs. 我们将添加一个shebang 行,它告诉操作系统 (OS)内核使用程序运行脚本node:
#! /usr/bin/env node
现在我们将添加一些使用 zx 运行命令的代码。
在下面的代码中,我们正在运行一个命令来执行ls程序。该ls程序将列出当前工作目录(脚本所在的目录)中的文件。我们将捕获命令进程的标准输出,将其存储在变量中,然后将其记录到终端:
// hello-world.mjsimport { $ } 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
您会在我们的 shell 脚本的输出中注意到一些内容:
- 我们运行的命令 ( ls) 包含在输出中。
- 该命令的输出显示两次。
- 输出末尾有一个额外的新行。
zxverbose默认在模式下运行。它将输出您传递给$函数的命令,并输出该命令的标准输出。我们可以通过在运行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
使用 Google 的 zx 和 TypeScript
如果我们想编写zx在 TypeScript 中使用的 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 行,告诉我们的操作系统内核使用ts-node程序运行脚本:
#! ./node_modules/.bin/ts-node
为了await在我们的 TypeScript 代码中使用关键字,我们需要将其包装在一个立即调用的函数表达式 (IIFE) 中,如zx 文档中所建议的那样:
// hello-world-typescript.tsimport { $ } 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 类似,但需要对我们的代码进行一些额外的配置和包装资料。
构建项目引导工具
现在我们已经了解了使用 Google 的 zx 编写 shell 脚本的基础知识,我们将使用它构建一个工具。此工具将自动创建一个通常很耗时的过程:引导新 Node.js 项目的配置。
我们将创建一个提示用户输入的交互式 shell 脚本。它还将使用捆绑的chalk库zx来突出显示不同颜色的输出并提供友好的用户体验。我们的 shell 脚本还将安装我们的新项目所需的 npm 包,因此我们可以立即开始开发。
入门
让我们创建一个名为的新文件bootstrap-tool.mjs并添加一个 shebang 行。我们还将从zx包中导入我们将使用的函数和模块,以及 Node.js 核心path模块:
#! /usr/bin/env node// bootstrap-tool.mjsimport { $, argv, cd, chalk, fs, question } from "zx";import path from "path";
与我们之前创建的脚本一样,我们想让我们的新脚本可执行:
chmod u+x bootstrap-tool.mjs
我们还将定义一个辅助函数,它以红色文本输出错误消息并以错误退出代码退出Node.js进程1:
function exitWithError(errorMessage) {console.error(chalk.red(errorMessage));process.exit(1);
}
当我们需要处理错误时,我们将通过我们的 shell 脚本在不同的地方使用这个辅助函数。
检查依赖项
我们正在创建的工具将需要运行使用三个不同程序的命令git:node和npx. 我们可以使用该库来帮助我们检查这些程序是否已安装并可使用。
首先,我们需要安装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包,它解析传递给我们脚本的任何命令行参数。这些已解析的命令行参数argv由zx包提供。
让我们添加一个名为的命令行参数检查directory:
let targetDirectory = argv.directory;
if (!targetDirectory) {exitWithError("Error: You must specify the --directory argument");
}
如果directory参数已传递给我们的脚本,我们要检查它是否是存在的目录的路径。我们将使用fs.pathExists提供的方法资料fs-extra:
targetDirectory = path.resolve(targetDirectory);if (!(await fs.pathExists(targetDirectory))) {exitWithError(`Error: Target directory '${targetDirectory}' does not exist`);
}
如果目标目录存在,我们将使用cd提供的函数zx来更改我们当前的工作目录:
cd(targetDirectory);
如果我们现在在没有参数的情况下运行我们的脚本--directory,我们应该会收到一个错误:
$ ./bootstrap-tool.mjsError: 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;
}
您会注意到我们正在关闭verbosezx 默认设置的模式。这意味着,当我们运行git config命令时,命令及其发送到标准输出的任何内容都不会显示。我们在函数结束时重新打开详细模式,因此我们不会影响稍后在脚本中添加的任何其他命令。
现在我们将创建一个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.`));}}
}
让我们调用 add a call tocheckGlobalGitSettings并检查user.name和user.emailGit 设置是否已设置:
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.writeJSONfs-extra
定义了package.json辅助函数后,我们可以开始考虑package.json文件的内容了。
Node.js 支持两种模块类型:
- CommonJS 模块(CJS)。用于module.exports导出函数和对象,并将require()它们加载到另一个模块中。
- ECMAScript 模块(ESM)。用于export导出函数和对象并将import它们加载到另一个模块中。
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;
}
上面的函数使用了questionzx 提供的函数。
我们现在将创建一个getNodeModuleSystem函数来调用我们的promptForModuleSystem函数。它将检查输入的值是否有效。如果不是,它会再次问这个问题:s
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文件。我们将询问项目应该使用哪个模块系统,将其设置为对象中type属性的值packageJson,然后将其写回到项目的package.json文件中:
const packageJson = await readPackageJson(targetDirectory);
const selectedModuleSystem = await getNodeModuleSystem();packageJson.type = selectedModuleSystem;await writePackageJson(targetDirectory, packageJson);
提示:要在使用标志package.json运行时获得合理的默认值,请确保设置 npm配置设置。npm init--yesinit-*
安装所需的项目依赖项
为了在运行我们的引导工具后更容易开始项目开发,我们将创建一个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包从GitHub 的 gitignore 模板中拉入 Node.js.gitignore文件。
为了生成我们的EditorConfig、Prettier和ESLint配置文件,我们将使用一个名为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.writeFile由fs-extra.
将项目骨架提交到 Git
最后,是时候提交我们创建的项目框架了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
并观看我们在行动中整合的所有内容!
结论
在本文中,我们学习了如何借助 Google 的 zx 库在 Node.js 中创建强大的 shell 脚本。我们使用它提供的实用程序函数和库来创建灵活的命令行工具。
到目前为止,我们构建的工具仅仅是个开始。以下是一些您可能想尝试自己添加的功能创意:
- 自动创建目标目录。如果目标目录不存在,提示用户并询问他们是否愿意为他们创建它。
- 开源卫生。询问用户他们是否正在创建一个开源项目。如果是,请运行命令以生成许可证和Contributor Convenant文件。
- 在 GitHub 自动创建存储库资料。添加使用GitHub CLI在 GitHub上创建远程存储库的命令。一旦使用 Git 提交了初始框架后,新项目就可以被推送到这个存储库。
如果本文对你有帮助,别忘记给我个3连问 ,点赞,转发,评论,,咱们下期见。
收藏 等于白嫖,点赞才是真情。
学习更多JAVA知识与技巧,关注与私信博主
免费学习领取JAVA 课件,源码,安装包等等