51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

如何使用 Google 的 zx 库在 Node 中编写 Shell 脚本

500.jpg

在本文中,我们将了解 Google 的 zx 库提供了什么,以及我们如何使用它来使用 Node.js 编写 shell 脚本。然后,我们将通过构建一个命令行工具来学习如何使用 zx 的功能,该工具可以帮助我们为新的 Node.js 项目引导配置。

编写 Shell 脚本:问题 {#writingshellscriptstheproblem}

创建一个 shell 脚本------一个由诸如 Bash 或 zsh 之类的 shell 执行的脚本------可以是自动化重复任务的好方法。Node.js 似乎是编写 shell 脚本的理想选择,因为它为我们提供了许多核心模块,并允许我们导入我们选择的任何库。它还使我们能够访问 JavaScript 提供的语言特性和内置函数。

但是,如果您尝试编写一个在 Node.js 下运行的 shell 脚本,您可能会发现它并不像您希望的那样流畅。您需要为子进程编写特殊处理,注意转义命令行参数,然后最终弄乱stdout(标准输出)和stderr(标准错误)。它不是特别直观,并且会使 shell 脚本非常尴尬。

Bash shell 脚本语言是编写 shell 脚本的流行选择。无需编写代码来处理子进程,并且它具有用于处理stdoutstderr. 但是用 Bash 编写 shell 脚本也不是那么容易。语法可能很混乱,难以实现逻辑,或处理诸如提示用户输入之类的事情。

Google 的 zx库有助于使用 Node.js 高效且愉快地编写 shell 脚本。

跟随要求 {#requirementsforfollowingalong}

跟随本文有一些要求:

  • 理想情况下,您应该熟悉 JavaScript 和 Node.js 的基础知识。

  • 您需要能够在终端中轻松运行命令。

  • 您需要安装Node.js >= v14.13.1。

Google 的 zx 是如何工作的? {#howdoesgoogleszxwork}

Google 的 zx 提供了封装子进程的创建以及处理stdout这些stderr进程的函数。我们将使用的主要函数是$函数。这是它的一个例子:

import { $ } from "zx";await $`ls`;

这是执行该代码的输出:

$ lsbootstrap-toolhello-worldnode_modulespackage.jsonREADME.mdtypescript

上面示例中的 JavaScript 语法可能看起来有点古怪。它使用一种称为标记模板文字的语言功能。它在功能上与写作相同await $("ls")

Google 的 zx 提供了其他几个实用函数来简化 shell 脚本编写,例如:

  • cd(). 这允许我们更改当前的工作目录。

  • question(). 这是 Node.js readline模块的包装器。它可以直接提示用户输入。

除了 zx 提供的实用功能外,它还为我们提供了几个流行的库,例如:

  • chalk:这个库允许我们为脚本的输出添加颜色。

  • minimist:解析命令行参数的库。然后将它们暴露在argv物体下。

  • fetch:Fetch API 的流行 Node.js 实现。我们可以使用它来发出 HTTP 请求。

  • fs-extra:一个库,它公开了 Node.js 核心fs 模块,以及许多其他方法,可以更轻松地使用文件系统。

现在我们知道 zx 给了我们什么,让我们用它创建我们的第一个 shell 脚本。

使用 Google 的 zx 的 Hello World {#helloworldwithgoogleszx}

首先,让我们创建一个新项目:

mkdir zx-shell-scriptscd zx-shell-scriptsnpm init --yes

然后我们可以安装zx库:

npm install --save-dev zx

注意:zx文档建议使用 npm 全局安装库。通过将其安装为我们项目的本地依赖项,我们可以确保始终安装 zx,并控制我们的 shell 脚本使用的版本。

顶层 await {#toplevelawait}

为了await在 Node.js中使用顶层------在函数await之外async------我们需要在支持顶层的ECMAScript (ES) 模块await中编写代码。"type": "module"我们可以通过添加我们的 来表明一个项目中的所有模块都是 ES 模块package.json,或者我们可以将单个脚本的文件扩展名设置为.mjs. 我们将为.mjs本文中的示例使用文件扩展名。

运行命令并捕获其输出 {#runningacommandandcapturingitsoutput}

让我们创建一个名为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

我们现在应该看到以下输出:

$ lshello-world.mjsnode_modulespackage.jsonpackage-lock.jsonREADME.mdhello-world.mjsnode_modulespackage.jsonpackage-lock.jsonREADME.md

您会在我们的 shell 脚本的输出中注意到一些内容:

  • 我们运行的命令 ( ls) 包含在输出中。

  • 该命令的输出显示两次。

  • 输出末尾有一个额外的新行。

zx``verbose默认在模式下运行。它将输出您传递给$函数的命令,并输出该命令的标准输出。我们可以通过在运行ls命令之前添加以下代码行来更改此行为:

$.verbose = false;

大多数命令行程序,例如ls,将在其输出末尾输出一个换行符,以使输出在终端中更具可读性。这有利于可读性,但由于我们将输出存储在变量中,我们不想要这个额外的新行。我们可以使用 JavaScript String#trim()函数摆脱它:

- const output = (await $`ls`).stdout;
+ const output = (await $`ls`).stdout.trim();

如果我们再次运行我们的脚本,我们会看到事情看起来好多了:

hello-world.mjsnode_modulespackage.jsonpackage-lock.json

使用 Google 的 zx 和 TypeScript {#usinggoogleszxwithtypescript}

如果我们想编写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

...我们应该看到以下输出:

$ lshello-world-typescript.tsnode_modulespackage.jsonpackage-lock.jsonREADME.mdtsconfig.json

使用 TypeScript编写脚本与zx使用 JavaScript 类似,但需要对我们的代码进行一些额外的配置和包装。

构建项目引导工具 {#buildingaprojectbootstrappingtool}

现在我们已经了解了使用 Google 的 zx 编写 shell 脚本的基础知识,我们将使用它构建一个工具。此工具将自动创建一个通常很耗时的过程:引导新 Node.js 项目的配置。

我们将创建一个提示用户输入的交互式 shell 脚本。它还将使用捆绑的chalkzx来突出显示不同颜色的输出并提供友好的用户体验。我们的 shell 脚本还将安装我们的新项目所需的 npm 包,因此我们可以立即开始开发。

入门 {#gettingstarted}

让我们创建一个名为的新文件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

我们还将定义一个辅助函数,它以红色文本输出错误消息并以错误退出代码退出Node.js进程1

function exitWithError(errorMessage) {
    console.error(chalk.red(errorMessage));
    process.exit(1);
  }

当我们需要处理错误时,我们将通过我们的 shell 脚本在不同的地方使用这个辅助函数。

检查依赖项 {#checkdependencies}

我们正在创建的工具将需要运行使用三个不同程序的命令gitnodenpx. 我们可以使用该库来帮助我们检查这些程序是否已安装并可使用。

首先,我们需要安装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"]);

添加目标目录选项 {#addatargetdirectoryoption}

由于我们正在构建的工具将帮助我们引导新的 Node.js 项目,因此我们需要运行我们添加到项目目录中的任何命令。我们现在--directory要向我们的脚本添加一个命令行参数。

zx捆绑minimist包,它解析传递给我们脚本的任何命令行参数。这些已解析的命令行参数argvzx包提供。

让我们添加一个名为的命令行参数检查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 设置 {#checkglobalgitsettings}

稍后,我们将在项目目录中初始化一个新的 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.nameuser.emailGit 设置是否已设置:

await checkGlobalGitSettings(["user.name", "user.email"]);

初始化一个新的 Git 存储库 {#initializeanewgitrepository}

我们可以通过添加以下命令在项目目录中初始化一个新的 Git 存储库:

await $`git init`;

生成package.json文件 {#generateapackagejsonfile}

每个 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 支持两种模块类型:

  • 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``--yes``init-*

安装所需的项目依赖项 {#installrequiredprojectdependencies}

为了在运行我们的引导工具后更容易开始项目开发,我们将创建一个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}`;}

生成工具配置 {#generateconfigurationfortooling}

创建项目配置是我们使用项目引导工具实现自动化的完美之选。首先,让我们添加一个命令来生成.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 {#generateabasicreadme}

我们可以使用我们的readPackageJson帮助函数从项目package.json文件中读取项目名称。然后我们可以生成一个基本的 Markdown 格式的 README 并将其写入README.md文件:

const { name: projectName } = await readPackageJson(targetDirectory);
const readmeContents = `# ${projectName}...`;
await fs.writeFile(`${targetDirectory}/README.md`, readmeContents);

在上面的函数中,我们使用了fs.writeFilefs-extra.

将项目骨架提交到 Git {#committheprojectskeletontogit}

最后,是时候提交我们创建的项目框架了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.`));

引导一个新项目 {#bootstrapanewproject}

现在我们可以使用我们创建的工具来引导一个新项目:

mkdir new-project./bootstrap-tool.mjs --directory new-project

并观看我们在行动中整合的所有内容!

结论 {#conclusion}

在本文中,我们学习了如何借助 Google 的 zx 库在 Node.js 中创建强大的 shell 脚本。我们使用它提供的实用程序函数和库来创建灵活的命令行工具。

赞(1)
未经允许不得转载:工具盒子 » 如何使用 Google 的 zx 库在 Node 中编写 Shell 脚本