Shebang in Shell
按
导言
首先你需要知道的是,在计算机领域中,bang 是叹号❗️的意思,它的本意是重击,所以俚语引申为感叹号。
无论你是否知道甚至是熟练于 shell 脚本编写,你应该都听说过见到过 #!/bin/bash
的文件头。它被称作为 Shebang/Hashbang,是由一个井号和叹号构成的字符序列。
不限于 bash/zsh,类 Unix 操作系统的 Shell loader 会分析 Shebang 之后的内容并将其作为解释器指令而调用。
例如对于如下的文件:
1
2
#!/bin/bash
echo "hello"
Shell 会识别到 shebang 字头,然后将后继的 /bin/bash
作为解释器,并向 bash 传递文件的内容进行执行。所以 bash 解释了 echo "hello"
之后在控制台输出了 hello
文本。
Shebang,Hash-bang (#!)
Shebang 由一个井号和叹号开头,请注意,多数 Shell(sh/bash/zsh/fish/ksh)会期待文件开头有限字节范围内,在第一行行首能够识别到 Shebang 序列,然后进入 Shebang 解释模式。
在 #!
序列之后可以有 0 到多个空白字符,然后是解释器的绝对路径(可以为其指定参数)。从第一个非空字符开始,loader 将会期待一行完整的命令行文本,并会将这段命令行(无论有否带有参数)当作解释器执行,并为该解释器的标准输入设备中写入脚本文件的内容。
对于现代的大多数 Shell 程序来说,它们都是简单地将脚本文件本身传递给解释器,而不是将去掉 Shebang line 之后的内容传递给解释器。
这个行为也很好理解,因为 Shebang loader 的实现者就无需构建脚本文件的缓冲区来去掉 Shebang line 了。
如果我是 OS 以及 Shell 的作者,我会考虑在 file system 的支撑能力上提供一个 mmap 机制,这个增强型的 mmap 能够指定 (offset_start, offset_stop) 或者 (offset, length) 的方式来映射一个虚拟的文件句柄(虚拟的 inode),这样就可以很轻易地实现排除 shebang line 的算法了。
这样有意思吗?
有的。
对于多数编译器来说,语言的语法层面能够支持 # 作为单行注释的,并不多,例如 c++, golang, rust, scala, kotlin 统统都不行。
这就带来一个问题,把这些传统型编译语言型的编译器当作解释型的解释器,在你解决了 shebang 行加载问题之后,你会遇到不能识别的 ‘#’ 字符问题,这是很有点哭笑不得的。
所以如果 OS 在 filesystem 上提供这样的特性的话,Shell 开发者可以很轻易地解决掉 Shebang line,这样像 go 这样的编译器就能够很好地契合到 Shell 中了。
当前,最简单的 Golang 像解释器一样工作的方式是:
1 go run a.go也有一些方法试图解决这一问题。
稍后章节我们还会展开研讨这个问题。
或许这种机制 linux mmap 已经能支持了,尚未去查阅过其变迁。
理论上说,你可以指定一个 bash 脚本到这里,它会被正确地套娃。
而指定一个 ELF 可执行文件的绝对路径到这里是比较常见的选择,正如下面的例子:
1
2
3
4
5
6
7
8
9
#!/bin/bash
#!/bin/zsh
#!/bin/fish
#!/usr/bin/env bash
#!/usr/bin/env zsh
#!/usr/local/bin/my-prog
#!/usr/local/bin/my-script.sh
其中,使用 #!/usr/bin/env arg
是一种常见的在不同平台上都能正确找到解释器的办法。因为有的平台上 bash 被安置在 /bin,有的平台上可能是安置在 /usr/bin,所以 /bin/bash
可能并不是总是能找到 bash 的真身。此时借助 /usr/bin/env bash
的方式,平台会将自己的 bash 安置位置返回给 Shebang loader,这就能保证 bash 二进制执行文件的可用性。
更多例子
Perl 和 Python 通常都是 Linux 发行版中的标配。
所以直接使用它们的解释脚本做工具的例子也很多。这时候的 Shebang 可能是这样的:
1
2
#!/usr/bin/perl -w # 使用带警告的Perl执行
#!/usr/bin/python -O # 使用具有代码优化的Python执行
PHP 也支持脚本化运行,你需要用到:
1
#!/usr/bin/php # 使用PHP的命令行解释器执行
Golang 是一个比 Python 更有力的解释器候选人,不过这边的积累还远远不够发起挑战,你可以这样跑 Golang 的脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env bash
exec go run "$0" "$@"
!#
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("Hello", os.Args[1])
os.Exit(42)
}
但它会以不合适的golang 语法而告终。行得通的办法在稍后会进行讨论。
但 Scala,Scheme,Nodejs 都能够达到解释运行的目的。
使用其它执行文件而不是 bash
既然 Shebang loader 是在执行一条命令行,那么你并不一定非要使用 bash。
例如可以用 cat 试试:
1
2
#!/bin/cat
hello world
bang-pound (!#) in Scala
!#
是 Scala 专有的一个语法单位,它的作用是将 scala 编译器切换到脚本解释模式。所以 Scala 的脚本开发者能够编写1:
1
2
3
4
5
#!/bin/sh
exec scala "$0" "$@"
!#
// Say hello to the first argument
println("Hello, "+ args(0) +"!")
对于 Scala 来说,其语法分析会将 #!
.. !#
之间的内容当作是普通注释一般地略过。
golang
让 golang 工作为解释器,是个不容易的事。
hack
我们已经知道一种 hack 方法2(译文3,Stackoverflow4)可以奏效:
1
2
3
4
5
6
7
8
9
10
11
12
//usr/bin/env go run "$0" "$@"; exit "$?"
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("Hello", os.Args[1:])
os.Exit(42)
}
gorun
此外,我们可以借助 gorun
来间接地跑 .go 如同脚本:
1
2
3
4
5
6
7
8
9
10
11
12
#! /usr/bin/env gorun
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("Hello", os.Args[1:])
os.Exit(42)
}
然后:
1
./example.go world
这种方法的问题在于,.go 文件不再是合法有效的。
这会导致一系列的问题。你只能将这些 .go 脚本文件移出你的 source-tree,否则你的 Golang 项目连 gofmt,go run 都做不了。
提案
让 golang 支持 ‘#’ 单行注释是个很困难的事吗?按照 Golang 开发队那堆人的性子,这很困难,因为这需要调整编译器的词法和语法逻辑,还会影响到 golang 工具树中的一系列工具,gofmt,goyacc 等等,而且所有的第三方工具都会感觉不好了,这显然是个不能被接受的提案嘛。
让 Golang 像 Nodejs 那样专门为 Shebang line 进行一个 hack 性处理,这困难吗?想必仍然是很困难的,毕竟这会影响 go 的编译速度嘛!
结论
所以我在想,我应该设计一种语言,没有这些狗屎的事,哦,还要写个 OS,支持那些我觉得很有道理的支持。
明年就 2021 了。
梦仍然没有醒。
留下评论