シェルスクリプトにGo言語を埋め込む

Go言語のコードをスクリプトとして即時実行する方法は、 go run するなり、もう少し工夫して shebang もどきを使って単体のコマンドっぽく実行するなり、いずれも簡単で情報もそれなりに見つかる。

一方で、例えば perl(1) や ruby(1) でオプション -e を使うような感じでシェルスクリプトに埋め込む方法については、情報が見つからなかった。

そこでちょっとしたツールを作ってみた。

GitHub - eel3/egos: Command line tool to embed golang script in shell script

まだ作りかけで、エラー処理が全然ダメなのだが、とりあえず動く。

使い方

書式とオプションは次の通り。

$ egos -h
usage: egos [-d] <-i packages> [-n|-p] 'script' [file ...]
  -d=false: print the compiled script but do not run it
  -i="": specify import packages
  -n=false: assume 'read line' loop around your script
  -p=false: assume loop like -n but print line also like sed

基本的には、オプション -i でimport対象のパッケージを指定した上で、コマンド引数にGo言語のコードを書いて実行すればよい。

シェルスクリプトに小コードを埋め込むことを想定しているため、package文やら「func main()」やら書かなくて済むようにしている。

$ egos -i '"fmt"' 'fmt.Println("hello, world")'
hello, world
$ _

オプション -d で実際に実行されるコードを見ることができる。

$ egos -d -i '"fmt"' 'fmt.Println("hello, world")'
// generated by egos(1)
package main

import (
        "fmt"
)

func main() {
        fmt.Println("hello, world")
}
$ _

スクリプトはGo言語の関数内に展開されるため、package文やimport文を記述したり、グローバルな変数・定数・関数を定義することは不可能だ。

関数定義については、関数リテラルクロージャ)と変数で代用できる。

$ egos -i '"fmt"' '
fn := func (s string) { fmt.Println(s) }
fn("hello, world")
'
hello, world
$ _

テキストレコード処理用に perl(1) や ruby(1) のオプション -n や -p っぽいオプションも用意してある。この動作モードでは、変数lineに1行分の文字列が詰め込まれている。

$ egos -i 'f "fmt"' -n 'f.Println(line)' <<'END'
> foo
> bar
> baz
> END
foo
bar
baz
$ egos -p 'line += line' <<'END'
> foo
> bar
> baz
> END
foofoo
barbar
bazbaz
$ _

問題点

egos.go の実装を見れば分かるが、単純にスケルトン・コードに引数指定したコードを埋め込んだ一時ファイルを作り、go run で実行しているだけだ。埋め込むコード単体での検証は行っていないので、妙なコードを埋め込めてしまう余地がある。

また現状では、go run でのビルドエラーや実行時エラーで発生したエラーメッセージをそのまま表示しているため、ファイル名やら行番号やらが滅茶苦茶になっている。いずれこの点を改善したいのだが、手付かずの状態になっている。

蛇足

備忘録。

Go言語のコードを書いたファイル先頭に「//usr/bin/env go run $0 $@ ; exit」と記述するアレは shebang ではない。 shebangシェルスクリプトの「#!」から始まる1行目を意味する言葉であり、「#!」から始まっていないGo言語でのそれは shebang とは言わない。

Unixシェルでコマンドを実行する時、そのファイルが実行ファイルか、実行権限が付いていて且つマジックナンバー「#!」で始まっているテキストファイルならば、exec(3)による実行に成功する。どちらでもなければ、exec(3)に失敗し、シェルはそのコマンドをシェルスクリプトではないかと推測して処理を開始する。

Bシェルで実行しているなら、Bシェルのスクリプトとみなし、サブシェルを生成して実行する。Cシェルで実行しているなら、ファイル先頭の1文字が「#」ならCシェルのスクリプトとして、それ以外の文字ならBシェルのスクリプトとみなして実行する。

つまり「//usr/bin/env go run $0 $@ ; exit」と書くハックでは、次のように動作する。

  1. シェルからGo言語で書いたファイル foo.go を実行する。
  2. foo.go は実行ファイルでなく、ファイル先頭が shebang でもないので、exec(3)に失敗する。
  3. シェルは foo.go を「Bシェルのスクリプトではないか?」と推測して、サブシェル上でシェルスクリプトとして実行する。
  4. サブシェルにて、「//usr/bin/env go run $0 $@」がシェルスクリプトのコードとして実行される。
  5. Go言語のコードとして foo.go が実行される。この際「//usr/bin/env go run $0 $@ ; exit」の1文はコメント文として無視される。
  6. 「go run」での foo.go の実行が完了する。
  7. サブシェルに戻り、「exit」がシェルスクリプトのコードとして実行される。
  8. サブシェルが終了する。

要するに、「//usr/bin/env go run $0 $@ ; exit」は shebang ではなく、単なるシェルコマンドだ。

個人的には「//usr/bin/env go run "$0" ${@+"$@"}; exit」の方が安全な気がする。

あと「//」で始まるのってCygwinではダメな気がする。というか手元のCygwinでは「//usr/bin/env: No such file or directory」となるのだが……。「/usr/bin/env」なら正しく認識する。