背景
这段时间有负责项目部署升级包的制作,所以对shell脚本的编写有所学习,但对于书写shell脚本我并不熟悉。所以想使用一种 js 来写shell的方法,于是接触到了zx。
Bash is great, but when it comes to writing scripts, people usually choose a more convenient programming language. JavaScript is a perfect choice, but standard Node.js library requires additional hassle before using. The zx package provides useful wrappers around child_process, escapes arguments and gives sensible defaults.
Bash 很棒,但是在编写脚本时,人们通常会选择更方便的编程语言。 JavaScript 是一个完美的选择,但标准的 Node.js 库在使用之前需要额外的麻烦。 zx 包提供了围绕 child_process 的有用包装器,转义参数并提供合理的默认值。
// shell.js
const shell = require('shelljs');
# 删除文件命令
shell.rm('-rf', 'out/Release');
// 拷贝文件命令
shell.cp('-R', 'stuff/', 'out/Release');
# 切换到lib目录,并且列出目录下到.js结尾到文件,并替换文件内容(sed -i 是替换文字命令)
shell.cd('lib');
shell.ls('*.js').forEach(function (file) {
shell.sed('-i', 'BUILD_VERSION', 'v0.1.2', file);
shell.sed('-i', /^.*REMOVE_THIS_LINE.*$/, '', file);
shell.sed('-i', /.*REPLACE_LINE_WITH_MACRO.*\n/, shell.cat('macro.js'), file);
});
# 除非另有说明,否则同步执行给定的命令。 在同步模式下,这将返回一个 ShellString
#(与 ShellJS v0.6.x 兼容,它返回一个形式为 { code:..., stdout:..., stderr:... } 的对象)。
# 否则,这将返回子进程对象,并且回调接收参数(代码、标准输出、标准错误)。
if (shell.exec('git commit -am "Auto-commit"').code !== 0) {
shell.echo('Error: Git commit failed');
shell.exit(1);
}
// zx
#!/usr/bin/env zx
await $`cat package.json | grep name`
let branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`
await Promise.all([
$`sleep 1; echo 1`,
$`sleep 2; echo 2`,
$`sleep 3; echo 3`,
])
let name = 'foo bar'
await $`mkdir /tmp/${name}
复制代码
安装和使用
// 安装 node Node.js >= 14.13.1
// 但我在安装时 遇到了这个报错 npm WARN notsup Unsupported engine for [email protected]: wanted: {"node":"^12.20.0 || ^14.13.1 || >=16.0.0"}
// 因为我的 node 版本是 奇数版本
npm i -g zx
// 脚本文件 可以以 .mjs / .js 结尾
// .js 结尾的脚本文件需添加
void async function () {
... shell commander
}()
// 脚本文件开头添加
#!/usr/bin/env zx
// 声明脚本运行环境 脚本用env启动的原因,是因为脚本解释器在linux中可能被安装于不同的目录,env可以在系统的PATH目录中查找。同时,env还规定一些系统环境变量。
process.env
// 运行脚本
$ chmod +x ./script.mjs
$ ./script.mjs
# 或者使用这个命令 .js 只能使用 zx ./script.js
$ zx ./script.js
$ zx ./script.ts
复制代码
zx 脚本bash语法可以忽略很多,直接写js就行,而且它的优点还不止这些,有一些特点挺有意思的:
1、支持ts,自动编译.ts为.mjs文件,.mjs文件是node高版本自带的支持es6 module的文件结尾,也就是这个文件直接import模块就行,不用其它工具转义
2、自带支持管道操作pipe方法
3、自带fetch库,可以进行网络请求,自带chalk库,可以打印有颜色的字体,自带错误处理nothrow方法,如果bash命令出错,可以包裹在这个方法里忽略错误将脚本写入扩展名为 .mjs
的文件中,以便能够在顶层使用await
。
// 一般shell命令 $`command`
使用 child_process 包中的 spawn 函数执行给定的字符串, 并返回 ProcessPromise<ProcessOutput>
例如:
const currenBranch = await $`git branch --show-current`
console.log(currenBranch, "current")
输出:
$ git branch --show-current
dev // shell 输出
ProcessOutput {
stdout: 'dev\n', // shell 输出
stderr: '', // shell 执行错误时输出的错误信息
exitCode: 0, // 脚本返回状态码
// toString() // 自带的toString 方法 command 正确输出 stdout 错误输出 stderr
} current // console 打印
复制代码
根据上面 ProcessOutput 返回的信息 我们可以通过以下方式来捕获异常
const a:string = "a" // 声明在
// 异常捕获
try {
$`command`
} catch (p) { // catch shell 返回的 ProcessOutput 如果执行脚本返回非 0 状态码,将会抛出 ProcessOutput 对象
console.log(`Exit code: ${p.exitCode}`)
console.log(`Error: ${p.stderr}`)
}
复制代码
// cd 切换工作目录
cd('/tmp');
// node-fetch 包 发起网络请求
const res = await fetch('https://wttr.in');
if (res.ok) {
console.log(await res.text());
}
// 对应也就可以拉取远程脚本执行
// 拉取数据主要是通过 scriptFromHttp 实现,主要有以下几步:
// 使用node-fetch包拉取对应url的数据
// 拉取完数据后会通过url的pathname来命名
// 在临时文件夹中创建文件
// question 是对 readline 包的包装
const env = await question('Choose a env: ', {
choices: Object.keys(process.env),
});
// 在第二个参数中,可以指定选项卡自动完成的选项数组
// 接口定义:
function question(query?: string, options?: QuestionOptions): Promise<string>
type QuestionOptions = { choices: string[] }
// 基于setTimeout 实现的 sleep
await sleep(1000);
// nothrow() 将 $ 的行为更改, 如果退出码不是0,不跑出异常.
// 以下的包,无需导入,可直接使用
// 彩色输出 chalk
console.log(chalk.blue('This is blue'));
// fs-extra
import {promises as fs} from 'fs'
const content = await fs.readFile('package.json');
// OS
await $`cd ${os.homedir()} && mkdir example`
// 指定要用的bash
$.shell = '/usr/bin/bash'
// 传递环境变量
process.env.FOO = 'bar'
await $`echo $FOO`
// 传递数组
let files = [1,2,3]
await $`tar cz ${files}`
// 将 .ts 脚本编译为 .mjs 并执行它们
zx examples/typescript.ts
复制代码
示例:
#!/usr/bin/env zx
const UI_PATH = "/mnt/d/WorkEngine/ui";
const INSTALL_PATH = `/mnt/d/WorkEngine/deployment/install`;
void async function () {
try {
// 将一些需要预先配置的东西json形式存入文件,在脚本运行时读取
let configData = await fs.readFile('./installConfig.json');
let config = JSON.parse(configData.toString());
if (config.UI.isUpdate) {
buildUI(INSTALL_PATH);
}
}
catch (p) {
// catch shell 返回的 ProcessOutput 如果执行脚本返回非 0 状态码,将会抛出 ProcessOutput
console.log(`Exit code: ${p.exitCode}`);
}
}();
// 前端 build 可执行文件 拷贝 函数
async function buildUI(INSTALL_PATH) {
try {
cd(UI_PATH);
await $ `pnpm i` // $ `command` 执行的命令是异步命令 要注意大部分时间其实我们是需要将其转为同步的
await $ `pnpm build`;
$ `mv dist/ ${INSTALL_PATH}`;
} catch (p) {
console.log(`Exit code: ${p.exitCode}`);
}
}
// docker 打包
async function buildBff(INSTALL_PATH, bffConfig) {
cd(BFF_PATH);
let currentBranch = await $ `git symbolic-ref --short -q HEAD`;
// $``返回的数据带\n 换行符,所以要使用需要先将换行符去掉
currentBranch = currentBranch.stdout.replace(/\n/g, '');
if (bffConfig.configUpdate) {
$ `cp configs/config_release.toml ${INSTALL_PATH}/bff/config.toml`;
}
if (bffConfig.mappingUpdate){
$`cp es/mapping.json ${INSTALL_PATH}/bff/`
}
const GO_PATH = process.env.GOPATH; // 环境变量可以通过 process.env 获取
await $ `${GO_PATH}/bin/swag init -g cmd/main.go`;
await $ `go mod tidy`;
await $ `go mod vendor`;
await $ `docker build --network="host" --build-arg branch=${currentBranch} --tag ${bffConfig.HARBOR_IP}/bff:${bffConfig.VERSION} .`;
await $ `docker save -o bff.tar ${bffConfig.HARBOR_IP}/bff:${bffConfig.VERSION}`;
$ `mv bff.tar ${INSTALL_PATH}/`;
}
// installConfig.json
{
"UI": {
"isUpdate": true
},
"bff": {
"isUpdate": true,
"configUpdate": true,
"mappingUpdate": true,
"HARBOR_IP": "update.zoomeye.org",
"VERSION": "2.5.0"
}
}
复制代码