关于shell:如何使用zx编写shell脚本

45次阅读

共计 11471 个字符,预计需要花费 29 分钟才能阅读完成。

前言

在这篇文章中,咱们将学习谷歌的 zx 库提供了什么,以及咱们如何应用它来用 Node.js 编写 shell 脚本。而后,咱们将学习如何通过构建一个命令行工具来应用 zx 的性能,帮忙咱们为新的 Node.js 我的项目疏导配置。

编写 Shell 脚本的问题

创立一个由 Bash 或者 zsh 执行的 shell 脚本,是自动化反复工作的好办法。Node.js 仿佛是编写 shell 脚本的现实抉择,因为它为咱们提供了许多外围模块,并容许咱们导入任何咱们抉择的库。它还容许咱们拜访 JavaScript 提供的语言个性和内置函数。

如果你尝试编写运行在 Node.js 中的 shell 脚本,你会发现这没有你设想中的那么顺利。你须要为子过程编写非凡的处理程序,留神本义命令行参数,而后最终与 stdout(规范输入)和stderr(规范谬误)打交道。这不是特地直观,而且会使shell 脚本变得相当蠢笨。

Bash shell 脚本语言是编写 shell 脚本的广泛抉择。不须要编写代码来解决子过程,而且它有内置的语言个性来解决 stdoutstderr。然而用 Bash 编写 shell 脚本也不是那么容易。语法可能相当凌乱,使得它实现逻辑,或者解决诸如提醒用户输出的事件十分艰难。

谷歌的 zx 库有助于让应用 Node.js 编写的 shell 脚本变得高效和舒服。

前置条件

往下浏览之前,有几个前置条件须要遵循:

  • 现实状况下,你应该相熟 JavaScript 和 Node.js 的基础知识。
  • 你须要适应在终端中运行命令。
  • 你须要装置Node.js >= v14.13.1

本文中的所有代码都能够从 GitHub 上取得。

zx 如何运作

Google 的 zx 提供了创立子过程的函数,以及解决这些过程的 stdoutstderr的函数。咱们将应用的次要函数是 $ 函数。上面是它的一个理论例子:

import {$} from "zx";

await $`ls`;

上面是执行上述代码的输入:

$ ls
bootstrap-tool
hello-world
node_modules
package.json
README.md
typescript

下面的例子中的 JavaScript 语法可能看起来有点乖僻。它应用了一种叫做带标签的模板字符串的语言个性。它在性能上与编写 await $("ls") 雷同。

谷歌的 zx 提供了其余几个实用功能,使编写 shell 脚本更容易。比方:

  • cd()。容许咱们更改当前工作目录。
  • question()。这是 Node.js readline 模块的包装器。它使提醒用户输出变得简单明了。

除了 zx 提供的实用功能外,它还为咱们提供了几个风行的库,比方:

  • chalk。这个库容许咱们为脚本的输入增加色彩。
  • minimist。一个解析命令行参数的库。而后它们在 argv 对象下被裸露进去。
  • fetch。Fetch API 的 Node.js 实现。咱们能够用它来进行 HTTP 申请。
  • fs-extra。一个裸露 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

你会留神到:

  • 咱们运行的命令(ls)被蕴含在输入中。
  • 命令的输入显示两次。
  • 在输入的开端多了一个新行。

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 脚本在各个中央应用这个辅助函数。

查看依赖

咱们要创立的工具须要应用三个不同程序来运行命令:gitnodenpx。咱们能够应用 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.nameuser.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.readJSONfs.writeJSON办法是由 fs-extra 库提供的。

在定义了 package.json 辅助函数后,咱们能够开始思考 package.json 文件的内容。

Node.js 反对两种模块类型:

  • CommonJS Modules (CJS)。应用 module.exports 来导出函数和对象,在另一个模块中应用 require() 加载它们。
  • ECMAScript Modules (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;
}

下面函数应用的 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-*的配置设置。

装置所需我的项目依赖

为了使运行咱们的启动工具后可能轻松地开始我的项目开发,咱们将创立一个 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-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://www.sitepoint.com/goo…
  • 作者:Simon Plenderleith

以上就是本文的所有内容。如果对你有所帮忙,欢送点赞、珍藏、转发~

正文完
 0