“Bash很好,所以我选择JavaScript”
前端日常开发中我们大部分时间都是使用Javascript,在编写shell脚本时,写bash没有像js那般熟悉,如果能选择一种更方便的编程语言来写它,何乐而不为呢? Google zx 工具实现了这种可能,官方用给base发“好人卡”的方式,成功介绍了它,作为一个更方便、更友好帮助开发者写脚本的工具 ,短短时间就在GitHub上火爆起来了,那我们一起来瞧一瞧它的庐山真面目。
它怎么使用呢?
安装初始化( npm i -g zx
),node版本限制( >= 14.8.0
)
先看一个简单的例子,创建一个 demo.mjs 文件,内容如下:
// demo.mjs
await Promise.all([
$`sleep 1; echo 3`,
$`sleep 2; echo 2`,
$`sleep 3; echo 1`,
])
let tName = 'google/zx'
await $`echo '/hello/${tName}'`
命令行执行 zx demo.mjs
伴随着3秒倒计时,可以看到 /hello/google/zx
以上就是一个简单的使用例子,接下去我们进入正题
来看看提供了哪些能力?
三种使用方式
zx << 'EOF'
if (typeof firstArg === 'undefined' || firstArg === '-') { ... }
判断输入命令没有firstArg则会通过 scriptFromStdin
函数处理逻辑:
-
通过
process.stdin.isTTY
判断是否在终端条件下 -
如果不是的话设置utf8字符编码
-
根据输入的逐行内容循环并拼接
-
在临时文件夹创建一个随机命名的mjs文件
Math.random().toString(36).substr(2) + '.mjs'
举个例子:
zx <<'EOF'
await $`pwd`
EOF
从远端拉取脚本执行
if (firstArg.startsWith('http://') || firstArg.startsWith('https://')) { ... }
拉取数据主要是通过 scriptFromHttp
实现,主要有以下几步:
-
使用node-fetch包拉取对应url的数据
-
拉取完数据后会通过url的pathname来命名
-
在临时文件夹中创建文件
本地脚本文件执行
如果没有进入到上面两个判断,则会执行下方逻辑,以 / 开头则直接使用该链接,file开头则调用url.fileURLToPath获取绝对路径字符串,其他逻辑的通过path.resolve获取路径
if (firstArg.startsWith('/')) {
filepath = firstArg
} else if (firstArg.startsWith('file:///')) {
filepath = url.fileURLToPath(firstArg)
} else {
filepath = resolve(firstArg)
}
支持多种文件格式 importPath
空后缀
zx 遇到这样的情况会直接readFile读取文件信息,然后获取当前文件所在目录以及文件名,重新写一个mjs文件,再次进入 importPath
方法并import执行
Markdown文件
zx 对于markdow编写的脚本同样可以执行,只有code包裹的部分会被解析,通过 transformMarkdown
方法用 \n 切割为数组后逐行扫描并用正则做匹配,匹配命中的信息会push到另一个数组,最后将数据通过换行符\n进行拼接转化成字符串作为script数据,生成文件的方式同上
Markdown格式举例:
# Markdown Scripts
It's possible to write scripts using markdown. Only code blocks will be executed
by zx.
> You can run this markdown file:
>
> ```
> zx docs/markdown.md
> ```
```js
await $`whoami`
await $`echo ${__dirname}`
The __filename
will be pointed to markdown.md:
console.log(chalk.yellowBright(__filename))
We can use imports here as well:
await import('chalk')
A bash code (with bash
or sh
language tags) also will be executed:
VAR=$(date)
echo "$VAR" | wc -c
Other code blocks are ignored:
body .hero {
margin: 42px;
}
##### Ts文件
核心部分是执行 `compile` 方法进行tsc编译出一个js,然后对js进行重命名为mjs
```javascript
let tsc = $`npm_config_yes=true npx -p typescript tsc --target esnext --lib esnext --module esnext --moduleResolution node ${input}`
await tsc
这里有个loading,用的是setInterval实现
let i = 0
let interval = setInterval(() => {
process.stdout.write(' '
+ ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'][i++ % 10]
+ '\r'
)
}, 100)
...
clearInterval(interval)
$函数
首先是使用了标签函数的能力处理args
如果传入了数组,会对这个数组进行拼接,例如:
let flags = [
'--oneline',
'--decorate',
'--color',
]
await $`git log ${flags}`
// zx 处理逻辑,先格式化,再用一个空格拼接
if (Array.isArray(args[i])) {
s = args[i].map(x => $.quote(substitute(x))).join(' ')
}
cmd与将转译后的args拼接,然后创建一个ProcessPromise类的实例 调用then的时候会触发_fun方法
then(onfulfilled, onrejected) {
if (this._run) this._run()
return super.then(onfulfilled, onrejected)
}
_run方法里核心是使用child_process.spawn
简单介绍一下child_process的其它能够执行bash命令的api spawn:启动一个子进程来执行命令 exec:启动一个子进程来执行命令,与spawn不同的是,它有一个回调函数能知道子进程的情况 execFile:启动一子进程来执行可执行文件 fork:与spawn类似,不同点是它需要指定子进程需要需执行的模块
child_process监听exit和close事件,然后输出一个output,code为0即成功,失败则会通过code码在 exitCodeInfo
方法中返回对应的错误信息
function exitCodeInfo(exitCode) {
return {
2: 'Misuse of shell builtins',
126: 'Invoked command cannot execute',
127: 'Command not found',
128: 'Invalid exit argument',
129: 'Hangup',
130: 'Interrupt',
131: 'Quit and dump core',
132: 'Illegal instruction',
133: 'Trace/breakpoint trap',
134: 'Process aborted',
135: 'Bus error: "access to undefined portion of memory object"',
136: 'Floating point exception: "erroneous arithmetic operation"',
137: 'Kill (terminate immediately)',
138: 'User-defined 1',
139: 'Segmentation violation',
140: 'User-defined 2',
141: 'Write to pipe with no one reading',
142: 'Signal raised by alarm',
143: 'Termination (request to terminate)',
145: 'Child process terminated, stopped (or continued*)',
146: 'Continue if stopped',
147: 'Stop executing temporarily',
148: 'Terminal stop signal',
149: 'Background process attempting to read from tty ("in")',
150: 'Background process attempting to write to tty ("out")',
151: 'Urgent data available on socket',
152: 'CPU time limit exceeded',
153: 'File size limit exceeded',
154: 'Signal raised by timer counting virtual time: "virtual timer expired"',
155: 'Profiling timer expired',
157: 'Pollable event',
159: 'Bad syscall',
}[exitCode]
}
其他Funtions
cd()
更改当前工作目录,同时会设置$.cwd
if (!fs.existsSync(path)) {
let __from = (new Error().stack.split(/^\s*at\s/m)[2]).trim()
console.error(`cd: ${path}: No such directory`)
console.error(` at ${__from}`)
process.exit(1)
}
fetch()
实际上是使用node-fetch拉取数据
nodeFetch(url: RequestInfo, init?: RequestInit): Promise<Response>
question()
readline的语法糖 通过设置options.choices传入一个数组可以支持select模式
sleep()
使用 util.promisify
对setTimeout包了一层,返回一个promise,例如await sleep(1000) 这样的回调风格更优美一些
nothrow()
设置_nothrow的值为true,child process监听close事件,返回的code !== 0 也能 resolve
使用举例:
await nothrow($`grep something from-file`)
可以直接使用的Packages
chalk package
console.log(chalk.blue('Hello world!'))
fs package ( fs-extra )
let content = await fs.readFile('./package.json')
globby package
这个包提供了一些方法,用于遍历文件系统,并根据UnixBashshell使用的规则返回与已定义的指定模式集匹配的路径名,同时以任意顺序返回结果,使用起来快速、简单、有效。
let packages = await globby(['package.json', 'packages/*/package.json'])
let pictures = globby.globbySync('content/*.(jpg|png)')
// Also, globby available via the glob shortcut:
await $`svgo ${await glob('*.svg')}`
os package
提供了与操作系统相关的方法和属性
await $`cd ${os.homedir()} && mkdir example`
minimist package
// 使用举例
$ node example/parse.js -x 3 -y 4 -n5 -abc --beep=boop foo bar baz
{ _: [ 'foo', 'bar', 'baz' ],
x: 3,
y: 4,
n: 5,
a: true,
b: true,
c: true,
beep: 'boop' }
参数配置
$.quote
这里主要是实现了一个quote方法,主要用于特殊字符转译
function quote(arg) {
if (/^[a-z0-9/_.-]+$/i.test(arg) || arg === '') {
return arg
}
return `$'`
+ arg
.replace(/\\/g, '\\\\')
.replace(/'/g, '\\\'')
.replace(/\f/g, '\\f')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
.replace(/\v/g, '\\v')
.replace(/\0/g, '\\0')
+ `'`
}
$.verbose(--quiet)
设置静默模式,默认为true会打印出log,通过参数 --quiet可以设置$.verbose = false 目前会打印的位置有:
-
cd命令,提示当前路径
-
远程拉取脚本,提示当前fetch的url
$.cwd
默认为undefined,$.cwd可以通过cd方法来设置,拿到当前的工作目录
$.shell(--shell)
指定使用的shell 例如:
$.shell = '/usr/bin/bash'
$.prefix
设置所有运行命令的前缀的命令 默认设置为-euo pipefail 或者使用 --prefix 来设置,例如:--prefix='set -e;'
Polyfills
Nodejs 在 ESM 中不提供 __dirname 和 __filename,以及没有定义require()函数,所以zx在global挂了这几个变量,方便使用者自取
let __filename = resolve(origin)
let __dirname = dirname(__filename)
let require = createRequire(origin)
Object.assign(global, {__filename, __dirname, require})
这次的介绍就到这里了