前言

在这篇文章中,咱们将学习谷歌的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`;

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

$ lsbootstrap-toolhello-worldnode_modulespackage.jsonREADME.mdtypescript

下面的例子中的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-scriptscd zx-shell-scriptsnpm 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.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

你会留神到:

  • 咱们运行的命令(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

引入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.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类似,但须要对咱们的代码进行一些额定的配置和包装。

构建我的项目启动工具

当初咱们曾经学会了用谷歌的zx编写shell脚本的基本知识,咱们要用它来构建一个工具。这个工具将主动创立一个通常很耗时的过程:为一个新的Node.js我的项目的配置提供疏导。

咱们将创立一个交互式shell脚本,提醒用户输出。它还将应用zx内置的chalk库,以不同的色彩高亮输入,并提供一个敌对的用户体验。咱们的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

咱们还将定义一个辅助函数,用红色文本输入一个错误信息,并以谬误退出代码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.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;}

你会留神到,咱们正在敞开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

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