概述
本文是笔者的系列博文 《Bun技术评估》 中的第十四篇。
在本文的内容中,笔者主要想要来探讨一下bun提供的一个有趣,而且在特定场景下也可能比较实用的特性:
bun shell。
简单理解,Bun Shell是一个bun内置的,跨平台的“类似于”bash的“外壳”。在这个外壳中,可以执行有限的指令,来达成和操作系统shell程序类似的功能。和操作系统提供的标准命令相比,bun shell可以提供一定的跨平台的功能,开发者不需要为这些简单的操作来达成不同平台的实现,此外,相对而言bun shell更加简单直接,容易实用,并且启动性能更好一点。
bash shell的一般操作
bun shell的一般应用和操作流程,可以参考下面的代码:
// 引用shell
import { $ } from "bun";
// 执行shell命令
await $`echo Hello, world!`; // => "Hello, world!"
// 获取执行结果
const output = await $`ls`.text();
// 变量结果
for await (const line of $`ls`.lines()) {
console.log(line);
}
这个shell在bun中的名字叫“$”,使用前需要引用。然后使用一个字符字面量模板就可以执行,执行结果是一个response promise,然后按照普通输出方式来处理就可以了。
但是笔者觉得,直接使用$,看起来很酷(这不就是jquery嘛),但有时候代码看起来很奇怪,也容易出问题,需要再看看在应用程序里面使用的效果再来评价吧。
输出处理
和response一样,bun shell提供了很多方便的输出处理和处理方式:
// 输出 json
const result = await $`echo '{"foo": "bar"}'`.json();
console.log(result); // { foo: "bar" }
// 行处理
for await (let line of $`echo "Hello World!"`.lines()) {
console.log(line); // Hello World!
}
// blob
const result = await $`echo "Hello World!"`.blob();
console.log(result); // Blob(13) { size: 13, type: "text/plain" }
错误处理
bun shell使用try-catch模式,来处理可能的错误:
import { $ } from "bun";
try {
const output = await $`something-that-may-fail`.text();
console.log(output);
} catch (err) {
console.log(`Failed with code ${err.exitCode}`);
console.log(err.stdout.toString());
console.log(err.stderr.toString());
}
当然,bun shell可选不抛出或者处理错误:
const { stdout, stderr, exitCode } = await $`something-that-may-fail`
.nothrow()
.quiet();
if (exitCode !== 0) {
console.log(`Non-zero exit code ${exitCode}`);
}
console.log(stdout);
console.log(stderr);
重定向和流水线
bun shell支持非常丰富灵活的输入输出重定向方式。而且使用简单强大的定向操作符就可以实现:
- < 标准输入(stdin) 重定向
- > 或 1> 标准输出重定向
- 2> 重定向标准错误(stderr)
- &> 重定向标准输出和标准错误
- >> or 1>> 重定向stdout, 扩展(追加)模式
- 2>> 重定向stderr, 扩展模式
- &>> 扩展模式,stdout和stderr
- 1>&2 将stdout重定向到stderr (都输出到stderr)
- 2>&1 重定向stderr到stdout (都输出到stdout)
和linux系统下的定义相同,1是标准输出文件描述符,2是标准错误文件描述符(但好像没使用0文件描述符)。下面是一些简单的例子,涉及到常用的操作方式:
import { $ } from "bun";
// 输出到buffer
const buffer = Buffer.alloc(100);
await $`echo "Hello World!" > ${buffer}`;
console.log(buffer.toString()); // Hello World!\n
// 处理输入
const response = new Response("hello i am a response body");
const result = await $`cat < ${response}`.text();
console.log(result); // hello i am a response body
// 从文件输入
await $`cat < myfile.txt`;
// 输出错误到
await $`bun run index.ts 2> errors.txt`;
// err合并到标准输出
await $`bun run ./index.ts 2>&1`;
看起来,合理的使用重定向,在某些场景下,可以大幅度简化开发工作(如输出到文件)。
环境变量和工作目录
bun shell执行时,可以设置和使用环境变量:
// 设置和引用环境变量
await $`FOO=foo bun -e 'console.log(process.env.FOO)'`; // foo\n
// 环境变量操作
const foo = "bar123";
await $`FOO=${foo + "456"} bun -e 'console.log(process.env.FOO)'`; // bar123456\n
// 使用 $.env方法操作环境变量
$.env({ FOO: "bar" });
// the globally-set $FOO
await $`echo $FOO`; // bar
在bun shell执行时,也可以控制切换和使用工作目录:
// 切换并使用工作目录
await $`pwd`.cwd("/tmp"); // /tmp
// 使用 .cwd方法
$.cwd("/tmp");
// the globally-set working directory
await $`pwd`; // /tmp
局限性
按照bun技术文档的说法,这个shell是一个内部的简化实现,所以支持的标准命令有限,而且使用的方式也是有限的,包括了以下跨平台的shell命令:
cd: change the working directory, 变更当前文件夹
ls: list files in a directory, 列表文件
rm: remove files and directories, 删除文件或文件夹
echo: print text,打印文本
pwd: print the working directory, 打印当前工作文件夹
bun: run bun in bun, 运行bun
cat 输出信息
touch 创建文件
mkdir 创建文件夹
which 查找命令
mv 移动文件
exit 退出
true
false
yes
seq
dirname
basename
笔者理解,这些命令,其实是跨平台的限制。在不同的平台上,还是可以使用对应平台的标准化的shell命令的。当然这就需要开放过程中充分测试和验证了。
笔者在测试这个功能时,还发现有另外一些限制。比如如果增加参数,"ls -l"就是没法正确执行的,只能用简单的方式执行:
// 获取执行结果
8 | const output = await $`ls -l`.text();
^
ShellError: Failed with exit code 1
exitCode: 1,
stdout: "",
stderr: "usage: ls [-@ABCFGHILOPRSTUWabcdefghiklmnopqrstuvwxy1%,] [--color=when] [-D format] [file ...]\n",
at new ShellError (13:16)
at new ShellPromise (75:16)
at BunShell (191:35)
at /home/yanjh/l.ts:8:2
杂项特性
bun shell还有一些有趣的杂项特性,值得了解一下:
- 嵌套执行
bun shell支持一种“嵌套”的命令执行方式,形式是“$(...)”,如下面的代码示例:
// Prints out the hash of the current commit
await $`echo Hash of current commit: $(git rev-parse HEAD)`;
- brace expansion(大括号扩展)
笔者觉得,这好像是python的一个语法糖吧,方法是调用 $.braces方法,它就会按照规则,将命令展开为多个类似的命令来执行,这个操作在很多批处理的场合非常有效(好像JS中,没有类似的机制??):
await $.braces(`echo {1,2,3}`);
// => ["echo 1", "echo 2", "echo 3"]
- pipeline 流水线
在bun shell命令中,也是支持linux风格的流水线操作符的:
const result = await $`echo "Hello World!" | wc -w`.text();
console.log(result); // 2\n
- 转义
可以使用.escape方法,来定义转义;也可以使用raw对象,指定不转义的内容:
console.log($.escape('$(foo) `bar` "baz"'));
// => \$(foo) \`bar\` \"baz\"
await $`echo ${{ raw: '$(foo) `bar` "baz"' }}`;
// => bun: command not found: foo
// => bun: command not found: bar
// => baz
- sh加载器
其实,bun除了执行js程序之外,也可以作为一个简单的sh执行器来使用:
script.sh
echo "Hello World! pwd=$(pwd)"
bun ./script.sh
Hello World! pwd=/home/demo
有意思的是,bun执行sh脚本,还可以是跨平台的。这对于提高程序的一致性是非常有益的。
OS Shell
如果不用ban shell,或者说bun shell的功能或者执行有限制,无法达成业务需求的时候,可能就需要退回到标准的shell命令执行方式了。
关于执行外部命令行程序,我们已经在前面的章节讨论过了。简单而言,就是需要使用Bun.spawn来封装一个命令行应用。但是这类需要注意的情况就是在不同的操作系统之上,可能需要进行不同的处理,才能达成类似的任务。因为它们确实就是运行在不同的操作系统环境当中。
下面是一个简单的例子,在不同的操作系统平台上,完成一些文件的合并工作:
const mergeFile = async(files,target)=>{
let cmdList;
if (process.platform.startsWith("win")) {
cmdList = [ "cmd", "/c"];
cmdList.push(`copy /b ${files} ${target}`);
} else {
cmdList = [ "sh","-c"];
cmdList.push(`cat ${files} > ${target}`);
}
const proc = Bun.spawn(cmdList, { stdout: "pipe", stderr: "pipe" });
const rtext = await new Response(proc.stdout).text();
if (rtext) console.log("执行结果:\n", rtext);
const err = await new Response(proc.stderr).text();
if (err) console.error("错误输出:\n", err);
};
// mergeFile("*.bin", "all.bin");
原理也不复杂,就是利用process.platform属性,判断当前程序所在的运行平台,然后决定选用不同的shell程序和需要执行的命令行指令。后续spawn的调用和处理,都是bun提供的机制,都是一样的。
这里也可以看到,由于需要使用操作系统提供的shell环境,它必然是先启动shell,然后再来执行shell命令,多了一个shell加载的过程,需要付出额外的启动时间和资源,效率上而言,应该是不如bun shell的。
小结
本文探讨了bun内置的shell特性,包括它的一般形式、输出输出、错误处理、重定向、环境变量和工作目录,以及局限性等方面的内容,最后还简单回顾了标准的OS Shell操作方面的内容。