前言
在这篇文章中,咱们将学习谷歌的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脚本变得高效和舒服。
前置条件
往下浏览之前,有几个前置条件须要遵循:
- 现实状况下,你应该相熟JavaScript和Node.js的基础知识。
- 你须要适应在终端中运行命令。
- 你须要装置
Node.js >= v14.13.1
。
本文中的所有代码都能够从GitHub上取得。
zx如何运作
Google的zx提供了创立子过程的函数,以及解决这些过程的stdout
和stderr
的函数。咱们将应用的次要函数是$
函数。上面是它的一个理论例子:
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脚本在各个中央应用这个辅助函数。
查看依赖
咱们要创立的工具须要应用三个不同程序来运行命令: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.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.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反对两种模块类型:
- 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
以上就是本文的所有内容。如果对你有所帮忙,欢送点赞、珍藏、转发~