立即登录

script脚本 一次使用 Go 语言编写脚本的经历

11/28 10:18:15

script脚本 一次使用 Go 语言编写脚本的经历

Go 语言区别于 Bash 和 Python 的地方是后者通过解释执行,既它们的脚本在读取的时候执行。而对于 Go 语言,当用户输入了 go run,Go 编译这个 Go 程序,然后再执行。因为 Go 编译时间非常短,所以看上去像是解释执行。值得提醒的是,很多人都说“go run 只是一个玩具”,但是如果我们需要脚本,同时也喜欢 Go 语言,那么这个玩具就是我们想要的。

3 所以已经支持的很好了,对吧?

我们可以编写脚本,并通过 go run 命令来执行。还有什么问题呢?问题是我很懒,希望通过类似./my-script.go 的方式来运行脚本,而不是 go run my-script.go。

这里我们讨论一个简单的脚本和 Shell 通过两种方式进行交互:它从命令行获取输入数据,并设置退出状态码。二者并非所有可能的交互方式(除此之外还可以有环境变量、信号、标准输入、标准输出、标准错误等),但是 Shell 脚本中较困难的两个。

这个脚本输出“Hello”和从命令行获取的第一个参数,并设置退出状态码为 42:

package main
import ( "fmt" "os")
func main() { fmt.Println("Hello", os.Args[1]) os.Exit(42)}

这时,使用 go run 命令结果有些奇怪:

$ go run example.go worldHello worldexit status 42$ echo $?1

这个问题我们稍后会讨论。

这时候可以使用 go build 命令。这是通过 go build 命令执行该脚本的方式:

$ go build$ ./example worldHello world$ echo $?42

此时调试该脚本的流程变成了:

$ vim ./example.go$ go build$ ./example.go worldHi world$ vim ./example.go$ go build$ ./example.go worldBye world

而我期望达到的是这样来运行脚本:

$ chmod +x example.go$ ./example.go worldHello world$ echo $?42

而对应的工作流程是:

$ vim ./example.go$ ./example.go worldHi world$ vim ./example.go$ ./example.go worldBye world

看上去很简单是吧?

4 Shebang

类 UNIX 系统支持 Shebang。Shebang 用于告诉 Shell 使用什么解释器来运行脚本。我们可以根据编写脚本使用的语言来设置 Shebang 行。

通常来说,我们会使用 env 命令最为脚本执行器,这样就无需再使用解释器的绝对路径。例如:可以设置 Shebang 为 #! /usr/bin/env python 让 Python 解释器来运行该脚本。当名称为 example.py 的脚本有上述的 Shebang 行,同时它具有可执行属性(可以通过 chmod +x example.py 命令添加)时,可以在 Shell 中输入./example.py arg1 arg2 来运行。此时 Shell 会读取 Shebang 行,然后开始链式反应:

Shell 开始运行 /usr/bin/env python example.py arg1 arg2。这实际就是 Shebang 行加上脚本名再加上额外的参数。该命令执行 /usr/bin/env,参数是 /usr/bin/env python example.py arg1 arg2。然后 env 命令调用 python 命令,执行 python example.py arg1 arg2。最后 python 运行 example.py 脚本,参数是 example.py arg1 arg2。

让我们开始尝试给 Go 脚本添加 Shebang。

第一次幼稚的尝试

我们首先设置一个幼稚的 Shebang 来使用 go run 执行这个脚本。加了 Shebang 之后的脚本看上去是这样的:

#! /usr/bin/env go runpackage main
import ( "fmt" "os")
func main() { fmt.Println("Hello", os.Args[1]) os.Exit(42)}

然后尝试运行一下,输出为:

$ ./example.go/usr/bin/env: ‘go run’: No such file or directory

发生了什么?

Shebang 机制将 go run 整体作为 env 命令的一个参数了,而实际不存在这个命令。输入 which "go run" 也会有类似的错误。

第二次尝试

一个可行的方案是将 Shebang 设置为 #! /usr/local/go/bin/go run。在我们尝试之前,就可以会发现一个问题:go 二进制文件在不同系统路径不同,写死绝对路径会导致脚本无法兼容安装在其他位置的 go。另外一个解决方案是使用 alias gorun="go run" 来创建一个别名,之后就能把 Shebang 修改成 #! /usr/bin/env gorun。使用这种方式,我们需要在运行这个脚本的系统中都设置这个别名。

输出:

$ ./example.gopackage main:example.go:1:1: illegal character U+0023 '#'

解释:从这个输出来看,我们有一个好消息,同时也有一个坏消息,你想先听哪个?我先来说好消息:-)

解决方案

当脚本不包含 Shebang 行时,不同的 Shell 会回退到不同的解析器。Bash 会使用自己来运行脚本,而 zsh 会回退到使用 sh。这给我们提供了一种解决方案,这也是 StackOverflow 上提到的一种解决方案。

由于 // 是 Go 语言中定义的注释,而我们可以使用 //usr/bin/env 来替代 /usr/bin/env(在路径分割符中,// == /),因此第一行可以设置成:

//usr/bin/env go run "$0" "$@"

结果:

$ ./example.go worldHi worldexit status 42./test.go: line 2: package: command not found./test.go: line 4: syntax error near unexpected token `newline'./test.go: line 4: `import ('$ echo $?2

解释:

我们距离成功又近了一步:终于有了正确的输出。但是输出中还包含一些错误,同时状态码也不对。让我们来看下到底发生了什么。正如之前所说的,Bash 没有找到任何 Shebang,因此选择使用 bash ./example.go world 的方式来运行脚本(直接使用该命令会有相同输出,你也可以试下)。非常有意思,直接使用 Bash 来运行 Go 文件 :-) 下一步,Bash 读取脚本的第一行,然后运行该命令:/usr/bin/env go run ./example.go world。之前脚本中的“0”代表第一个参数,因此实际值是我们运行的脚本文件名。“@”表示命令行中的所有参数。在这个例子中会被替换成“world”。到目前位置,使用./example.go world,脚本使用了正确的命令行参数,并输出了正确的值。

输出中还有诡异的一行:“exit status 42”。这是什么?如果我们自己尝试下命令就会了解:

$ go run ./example.go worldHello worldexit status 42$ echo $?1

这是 go run 命令通过标准错误输出的。go run 命令屏蔽了状态码,然后返回了状态码 1。关于这个行为的讨论,可以参见 Github issue。

好了,那么其他几行输出呢?这是 Bash 试图解析 Go 源码,但实际失败了。

解决方案优化

这个 StackOverflow 页面建议在 Shebang 之后加上 ;exit “$?”。这会告诉 Bash 解释器不要再继续执行。

完整的 Shebang:

//usr/bin/env go run "$0" "$@"exit "$?"

结果:

$ ./test.go worldHi worldexit status 42$ echo $?1

基本上实现了:这里实现了让 Bash 使用 go run 命令执行脚本script脚本,然后立即退出,同时设置状态码为 go run 命令执行后的状态码。

更进一步,可以在 Shebang 行中添加一些命令,用于移除标准错误中的“退出状态”内容,甚至解析该文本并作为整个脚本的返回码。

然而:

5 幸运的是,我们有 gorun

gorun 就是我们想要的。我们只需在 Shebang 中写 #! /usr/bin/env gorun,并赋予脚本可执行权限。仅此而已,我们可以在 Shell 中执行,获得期望的结果!

$ ./example.go worldHello world$ echo $?42

太棒了!

警告:兼容性

当文件包含 Shebang 之后,Go 将无法编译(和我们之前看见的一样)。

$ go run example.gopackage main:example.go:1:1: illegal character U+0023 '#'

这两种选择不能兼得,我们只能二选一:

二者不可兼得!

另外一个问题,是当脚本文件被放在 Go 工程中时script脚本,编译器会发现这个 go 文件。虽然该文件并不是应用程序所需要的,也会导致编译失败。一个解决方案是移除.go 后缀,但是这样就会无法使用类似 go fmt 等工具。

6 最后一些想法

本文讨论了使用 Go 语言来编写脚本的重要性,同时介绍了几种方式来实现脚本运行。这里有一些总结。

解释:

正如上表,目前没有一种完美的解决方案。看上去最方便且问题最少的方式是使用 go run 命令。但是在我看来,这种方式太过“复杂”,而且无法“可执行”辅助卡盟,同时退出状态码也不正确。这将会导致难以区分脚本是否正确执行。

因此,我认为 Go 语言在这个领域仍然有许多工作要做。我不认为让语言支持忽略 Shebang 行会有什么问题。这将会解决执行问题,但是类似这种变化可能不会被 Go 社区采纳。

我的同事提醒我事实上 Shebang 行对于 Javascript 同样也是非法的。但是在 Node.js 中,他们增加了一个跳过 Shebang 函数,让 Node 脚本可以在 Shell 中直接运行。(译者注:由于原文时间比较久远,在 c2b01881dcb3bf302f9d83157e719cc5240a9042 版本之后 Node.js 已经对源码进行了重构,在 702331be906fe58e0ef66c7b31c7d2aeb3af3421 版本之后,原文提及的 stripShebang 函数已经被移除。)

如果 gorun 可以作为标准工具的一部分就更棒了,其他类似的还有 gofmt 和 godoc。

原文链接:

点个在看少个 bug

来源:【九爱网址导航www.fuzhukm.com】 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

最新资讯