用zx编写基于JS的Bash脚本

787 阅读6分钟

简介

Bash是一种命令语言,通常以命令行解释器程序的形式出现,用户可以通过终端软件执行命令。例如,我们可以使用Ubuntu的终端来运行Bash命令。我们还可以通过所谓的shell脚本来创建和运行Bash脚本文件。

程序员在许多自动化场景中使用shell脚本,例如用于构建过程,以及CI/CD-或计算机维护相关活动。作为一种全功能的命令语言,Bash支持管道、变量、函数、控制语句和基本算术操作。

然而,Bash并不是一种通用的开发者友好型编程语言。它不支持OOP,不支持JSON等结构,不支持除数组以外的普通数据结构,也不支持内置的字符串或数组操作方法。这意味着程序员经常不得不从Bash中调用单独的Python或Node脚本来满足这些要求。

这就是zx项目的用武之地。zx引入了一种使用JavaScript编写类似Bash脚本的方法。

相比之下,JavaScript几乎拥有开发者需要的所有内置功能。zx通过为几个关键的CLI相关的Node.js包提供封装API,让程序员用JavaScript编写shell脚本。因此,你可以使用zx来编写开发者友好的、类似Bash的shell脚本。

在这篇文章中,我将解释zx并教你如何在你的项目中使用它。

比较Bash和zx

Bash是一种单通道的解释命令语言,最初由Brian Fox开发。程序员经常在Unix或类似Unix的命令的帮助下使用它。

大多数情况下,Bash会启动独立的进程来执行不同的子任务。例如,如果你使用expr 命令进行算术运算,Bash解释器总是会产生另一个进程。

原因是expr 是一个命令行程序,需要一个单独的进程来运行。当你在脚本文件中加入更多的逻辑时,你的shell脚本可能看起来很复杂。你的shell脚本也可能由于催生了额外的进程和解释而最终表现缓慢。

zx项目实现了一个类似于Bash的shell脚本执行器,但使用JavaScript模块。它提供了一个内置的异步JavaScript API,以调用类似于Bash的其他命令。除此之外,它还为几个基于Node.js的命令行助手提供了封装函数,如chalkminimist, fs-extra,OS, 和Readline

zx是如何工作的?

每个zx shell脚本文件的扩展名是.mjs 。所有的内置函数和第三方API的封装器都是预先导入的。因此,你不需要在你的基于JavaScript的shell脚本中使用额外的导入语句。

zx接受来自标准输入、文件和作为URL的脚本。它导入你的zx命令集作为ECMAScript模块(MJS)来执行,命令执行过程中使用Node.js的子进程API

现在,让我们用zx写一些shell脚本来更好地了解这个项目。

zx脚本编写教程

首先,在开始编写zx脚本之前,你需要在全球范围内安装zx的npm包。确保你已经安装了Node.js v14.8.0或者更高。

在你的终端上运行以下命令来安装zx命令行程序。

npm install -g zx

在终端输入zx ,检查程序是否安装成功。你会得到如下的输出。

The output when zx is installed successfully

zx的基础知识

让我们创建一个简单的脚本来获取一个Git项目的当前分支。

在你的一个项目中创建get_current_branch.mjs ,并添加以下代码。

#!/usr/bin/env zx
const branch = await $`git branch --show-current`
console.log(`Current branch: ${branch}`)

第一行是shebang行,它告诉操作系统的脚本执行器挑选正确的解释器。$ 是一个执行给定命令的函数,当它与await 关键字一起使用时,返回其输出。最后,我们用console.log 来显示当前的分支。

用下面的命令运行你的脚本,可以得到项目的当前 Git 分支。

zx ./get_current_branch.mjs

因为zx默认打开了verbose模式,所以它也会显示你执行的每一条命令。按照下面的方法更新你的脚本,去掉额外的命令细节。

#!/usr/bin/env zx
$.verbose = false
const branch = await $`git branch --show-current`
console.log(`Current branch: ${branch}`)

由于最上面的shebang行,你也可以在没有zx命令的情况下运行该脚本。

chmod +x ./get_current_branch.mjs
./get_current_branch.mjs

着色和格式化

zx也暴露了粉笔库的API。因此,我们可以用它来着色和格式化,如下图所示。

#!/usr/bin/env zx
$.verbose = false
let branch = await $`git branch --show-current`
console.log(`Current branch: ${chalk
                                .bgYellow
                                .red
                                .bold(branch)}`)

更多的着色和格式化方法可以在chalk的官方文档中找到。

用户输入和命令行参数

zx提供了question 功能来捕捉来自命令行界面的用户输入。你也可以用choices 选项启用传统的Unix标签完成。

下面的脚本从用户那里捕获了一个文件名和模板。之后,它使用用户输入的配置来构建一个文件。你可以在第二个问题上使用tab完成。

#!/usr/bin/env zx
$.verbose = false
let filename = await question('What is the filename? ')
let template = await question('What is your preferred template? ', {
  choices: ["function", "class"] // Enables tab completion.
})
let content = ""

if(template == "function") {
    content = `function main() {
    console.log("Test");
}`;
}
else if(template == "class") {
    content = `class Main {
    constructor() {
        console.log("Test");
    }
}`;
}
else {
    console.error(`Invalid template: ${template}`)
    process.exit();
}
fs.outputFileSync(filename, content)

一个被解析的命令行参数对象可以作为全局argv 常量。解析工作是使用minimist Node.js模块完成的。

看看下面的例子,它捕获了两个命令行参数值。

#!/usr/bin/env zx
$.verbose = false
const size = argv.size;
const isFullScreen = argv.fullscreen;
console.log(`size=${size}`);
console.log(`fullscreen=${isFullScreen}`);

运行上述脚本文件,如下图所示,检查命令行参数的支持情况。

./yourscript.mjs --size=100x50 --fullscreen

网络请求

程序员经常使用curl 命令来用Bash脚本进行HTTP请求。zx为node-fetch模块提供了一个封装器,它将特定模块的API暴露为fetch 。其优点是zx不会像Bash使用curl那样为每个网络请求生成多个进程--因为node-fetch包使用Node的标准HTTP API来发送网络请求。

让我们做一个简单的HTTP请求来熟悉一下zx的网络请求API。

#!/usr/bin/env zx
$.verbose = false
let response = await fetch('https://cheat.sh');
if(response.ok) {
    console.log(await response.text());
}

上面的zx脚本将在node-fetch模块的帮助下下载并显示特定URL的内容。它并不像Bash的网络调用那样产生一个单独的进程。

构建命令流水线

在shell脚本中,管道指的是多个按顺序执行的命令。我们经常在shell脚本中使用著名的管道字符(| )来将输出从一个进程传递到另一个进程。

我们可以使用类似于Bash脚本的命令集的| 字符--或者我们可以使用zx内置API的.pipe() 链式方法。在下面的例子脚本中查看两种方式的管道是如何实现的。

#!/usr/bin/env zx
$.verbose = false
// A pipeline using |
let greeting = await $`echo "Hello World" | tr '[l]' [L]`
console.log(`${greeting}`)
// The same pipeline but with the .pipe() method
greeting = await $`echo "Hello World"`
    .pipe($`tr '[l]' [L]`)

console.log(`${greeting}`)

高级用例

除了基于JavaScript的shell脚本支持外,zx还支持其他几个有用的功能。

默认情况下,zx使用一个Bash解释器来运行命令。我们可以通过修改$.shell 这个配置变量来改变默认的shell。下面的脚本使用sh shell而不是bash

$.shell = '/usr/bin/sh'
$.prefix = 'set -e;'

$`echo "Your shell is $0"` // Your shell is /usr/bin/sh

你可以使用zx命令行程序来执行一个特定的Markdown文件中用JavaScript编写的代码片段。如果你提供一个Markdown文件,zx命令行程序将解析并执行代码块。

让我们看一个例子。从zx的GitHub上下载这个例子的Markdown文件,并将其保存为markdown.md 。之后,运行下面的命令来执行代码块。

zx markdown.md 

zx命令行程序也可以从一个URL中运行脚本。像提供文件名一样,提供一个指向你的zx脚本的链接。下面的远程脚本将显示一个问候信息。

zx https://raw.githubusercontent.com/shalithasuranga/zx-scripting-examples/main/greeting.mjs

你也可以从你的基于Node的Web应用程序中导入$ 功能。然后,就可以从你的网络应用程序的后端运行命令。

如下图所示,导入zx的$ 函数,从其他JavaScript源文件中调用操作系统的命令。

import { $ } from 'zx'
await $`whoami`

用TypeScript使用zx

zx也有TypeScript的定义,尽管完全支持还没有到来。因此,程序员可以用TypeScript来使用所有zx的内置API。我们可以直接将TypeScript文件作为zx文件提供给zx命令行程序。然后,zx将转译和执行提供的TypeScript源文件。

此外,还可以在你的基于TypeScript的Web应用程序中使用zx来执行操作系统的命令。

总结

Bash脚本是自动化开发过程的一个好方法。但是,当你的Bash脚本变得复杂时,有时你可能不得不用其他编程语言编写单独的脚本。

zx项目提供了一种简单的方法来用JavaScriptTypeScript编写类似Bash的脚本。它提供了类似Bash的最小API,让我们在做的时候有一种shell-scripting的感觉--即使我们在写一个JavaScript源文件。

此外,zx激励开发者编写基于JavaScript的shell脚本,不使用分号,以使zx脚本和Bash脚本在语法上相似。

然而,zx并不是Bash的替代品--无论如何,它在内部使用一个命令行解释器(默认是Bash)来执行命令。

The postWriting JS-based Bash scripts with zxappeared first onLogRocket Blog.