- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
在这篇文章中,我们将学习谷歌的zx库提供了什么,以及我们如何使用它来用Node.js编写 shell 脚本。然后,我们将学习如何通过构建一个命令行工具来使用zx的功能,帮助我们为新的Node.js项目引导配置.
创建一个由Bash或者zsh执行的shell脚本,是自动化重复任务的好方法。Node.js似乎是编写shell脚本的理想选择,因为它为我们提供了许多核心模块,并允许我们导入任何我们选择的库。它还允许我们访问JavaScript提供的语言特性和内置函数.
如果你尝试编写运行在Node.js中的shell脚本,你会发现这没有你想象中的那么顺利。你需要为子进程编写特殊的处理程序,注意转义命令行参数,然后最终与 stdout (标准输出)和 stderr (标准错误)打交道。这不是特别直观,而且会使 shell 脚本变得相当笨拙.
Bash shell脚本语言是编写shell脚本的普遍选择。不需要编写代码来处理子进程,而且它有内置的语言特性来处理 stdout 和 stderr 。但是用Bash编写shell脚本也不是那么容易。语法可能相当混乱,使得它实现逻辑,或者处理诸如提示用户输入的事情非常困难.
谷歌的 zx库 有助于让使用Node.js编写的shell脚本变得高效和舒适.
往下阅读之前,有几个前置条件需要遵循:
Node.js >= v14.13.1
。 本文中的所有代码都可以从 GitHub 上获得.
Google的zx提供了创建子进程的函数,以及处理这些进程的 stdout 和 stderr 的函数。我们将使用的主要函数是 $ 函数。下面是它的一个实际例子:
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提供的实用功能外,它还为我们提供了几个流行的库,比如:
argv
对象下被暴露出来。 fs
模块的库,以及一些额外的方法,使其更容易与文件系统一起工作。 现在我们知道了zx给了我们什么,让我们用它创建第一个shell脚本.
首先,我们先创建一个新项目:
mkdir zx-shell-scripts
cd zx-shell-scripts
npm init --yes
然后安装zx库:
npm install --save-dev zx
注意:zx的文档建议用npm全局安装该库。通过将其安装为我们项目的本地依赖,我们可以确保zx总是被安装,并控制shell脚本使用的版本.
为了在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中编写使用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脚本在各个地方使用这个辅助函数.
我们要创建的工具需要使用三个不同程序来运行命令: 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.mjs
Error: You must specify the --directory argument
稍后,我们将在项目目录下初始化一个新的 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 仓库:
await $`git init`;
每个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支持两种模块类型:
module.exports
来导出函数和对象,在另一个模块中使用 require()
加载它们。 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 中获得合理的默认值,请确保你设置了npm init-* 的 配置设置 .
为了使运行我们的启动工具后能够轻松地开始项目开发,我们将创建一个 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包。它还提供了大量的配置选项,允许我们调整生成的配置文件以符合我们的个人偏好.
我们可以使用我们的 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 上找到.
以上就是本文的所有内容。如果对你有所帮助,欢迎点赞、收藏、转发~ 。
最后此篇关于如何使用zx编写shell脚本的文章就讲到这里了,如果你想了解更多关于如何使用zx编写shell脚本的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我有 powershell 脚本。通过调度程序,我运行 bat 文件,该文件运行 PS1 文件。 BAT文件 Powershell.exe -executionpolicy remotesigned
什么更快? 或者 $.getScript('../js/SOME.js', function (){ ... // with $.ajaxSetup({ cache: true });
需要bash脚本来显示文件 #!/bin/bash my_ls() { # save current directory then cd to "$1" pushd "$1" >/dev/nu
我有一个输入 csv 文件,实际上我需要在输入文件中选择第 2 列和第 3 列值,并且需要转换两个值的时区(从 PT 到 CT),转换后我需要替换转换后的时区值到文件。 注意: 所有输入日期值都在太平
我正在使用/etc/init.d/httpd 作为 init.d 脚本的模板。我了解文件中发生的所有内容,但以下行除外: LANG=$HTTPD_LANG daemon --pidfile=${pid
我有以下选择: python runscript.py -O start -a "-a "\"-o \\\"-f/dev/sda1 -b256k -Q8\\\" -l test -p maim\""
我对 shell 脚本完全陌生,但我需要编写一个 shell 脚本来检查文件是否存在,然后移动到另一个位置 这是我写的: 一旦设备崩溃,我就会在/storage/sdcard1/1 中收集日志 #!/
我正在使用 bash 脚本从文本文件中读取数据。 数据: 04:31 Alex M.O.R.P.H. & Natalie Gioia - My Heaven http://goo.gl/rMOa2q
这是单击按钮时运行的 javascript 的结尾 xmlObj.open ('GET', /ajax.php, true); xmlObj.send (''); } 所以这会执行根目录中的php脚本
关闭。这个问题需要debugging details .它目前不接受答案。 编辑问题以包含 desired behavior, a specific problem or error, and th
我需要将文件转换为可读流以通过 api 上传,有一个使用 fs.createReadStream 的 Node js 示例。任何人都可以告诉我上述声明的 python 等价物是什么? 例子 const
我有一个 shell 脚本 cron,它从同一目录调用 python 脚本,但是当这个 cron 执行时,我没有从我的 python 脚本中获得预期的输出,当我手动执行它时,我的 python 脚本的
如何使 XMLHttpRequest (ajax) 调用的 php 脚本安全。 我的意思是,不让 PHP 文件通过直接 url 运行,只能通过脚本从我的页面调用(我不想向未登录的用户显示数据库结果,并
我正在尝试添加以下内容 我正在使用经典的 asp。但我不断收到的错误是“一个脚本 block 不能放在另一个脚本 block 内。”我尝试了此处的 document.write 技术:Javasc
如何从另一个 PHP 脚本(如批处理文件)中运行多个 PHP 脚本?如果我了解 include 在做什么,我认为 include 不会起作用;因为我正在运行的每个文件都会重新声明一些相同的函数等。我想
我想创建具有动态内容的网页。我有一个 HTML 页面,我想从中调用一个 lua 脚本 如何调用 lua 脚本? ? ? 从中检索数据?我可以做类似的事情吗: int xx = 0; xx
我删除了我的第一个问题,并重新编写了更多细节和附加 jSfiddle domos。 我有一个脚本,它运行查询并返回数据,然后填充表。表中的行自动循环滚动。所有这些工作正常,并通过使用以下代码完成。然而
我尝试使用 amp 脚本,但收到此错误: “[amp-script] 脚本哈希未找到。amp-script[script="hello-world"].js 必须在元[name="amp-script
我有一个读取输入的 Shell 脚本 #!/bin/bash echo "Type the year that you want to check (4 digits), followed by [E
我正在从 nodejs 调用 Lua 脚本。我想传递一个数组作为参数。我在 Lua 中解析该数组时遇到问题。 下面是一个例子: var script = 'local actorlist = ARGV
我是一名优秀的程序员,十分优秀!