ZX 用JavaScript写bash脚本

4,304 阅读3分钟

背景

这段时间有负责项目部署升级包的制作,所以对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 globby@12.0.2: 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"
  }
 }

参考: Google 脚本工具 zx 使用教程

nodejs写bash脚本终极方案!