exeねばなりません←未完の別解 (Go!)

EXEでないのは万死に値する、と脅迫されている、として。

もとの python による解が「不自然」なのは、当たり前のことだが「python そのものを使わない」ことにならないからだ。「EXEであればいい」というニーズに対して「python に依存する」という要求が釣り合わない。

そうでない解を得たいのならば、一番考えやすいのは、従来であれば「C で作ってしまう」ことだった。ただのランチャだもの、簡単だろうさ、と、まぁ色んな OSS が実際にそれをやってるし、とてつもなく苦労してきた。特に「Windows 9x」が健在だった頃が本当にヒドいものだったが、今だって非常に面倒くさいことには変わりはない。というワタシのこの意見を疑うなら、たとえば java のランチャ(java.exe、javaw.exe)や CPython の Windows 版だけに含まれる py.exe のソースコードをじっくり鑑賞してみるといい。Unix でのプロセス起動のシンプルさに比して、驚くほど鬱陶しい手続きが必要なのかについて知って悶絶するがいい。(emacs なんかもこの苦労をし続けてるひとつ。)

まぁこれまでは確かにそうだった、のだけれどもさ。

Go がね、コンパイル型の、いわゆる Windows ネイティブを作れる子、なのよね。java とか .net みたいな VM 方式でもなく、python や ruby, perl のようなスクリプトでもない、そう、「みんな大好き EXE」を作る、作れる。

今ワタシにとっての Go は「試用期間」中で、実力把握してる最中なので、確定的な評価の類はあまり言わない。とにかく今は「EXE を作れる」ことだけに着目する。とするならば、別解はこんなかなぁと:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "os"
 6     "os/exec"
 7     "io/ioutil"
 8     //"syscall"  // "syscall.Exec" which i want to use does not support Windows...
 9 )
10 
11 func main() {
12     args := append([]string{"./msvcc.sh"}, os.Args[1:]...)
13     cmd := exec.Command("bash", args...)
14     cmdOut, _ := cmd.StdoutPipe()
15     cmd.Start()
16     outBytes, _ := ioutil.ReadAll(cmdOut)
17     cmd.Wait()
18     fmt.Println(string(outBytes))
19 }

(→gomsvcc-0.0.1.zip)
コメントアウトしてる「syscall」がほんとは使いたかったやつで、正直このストレスはお馴染みのやつ。python も今でこそ色々「Windows でだけ出来ないこと」が減ってきてるが、たとえば python 2.3 とかの頃って、ほんとに「Windows では使えない」ものだらけで、もちろん subprocess まわりにそうしたものがたくさんあった。そして、このおなじみの「いらり」が、C で作るよりマシか、と問われれば、もちろん C で作るよりは遥かに楽。

もちっと高レベルのラッパーがあると楽なのだがなぁ。python で言うところの「subprocess.check_call」みたいな包み方のものはどうもなさげで、標準出力・標準エラー出力のキャプチャと出力は、なんとか自力で処理しなければならなそう。見ての通り、ワタシが今やってみたやつはそこをやってない。幸い Go は多重化が簡単なので、おそらくちゃんとやったとしてもコードはそれほど膨らまないだろうとは思う。…んだけどね、まだわからんことだらけでな。もちっと鍛錬が必要だ、ワタシには。

あ、「評価はあまり書かない」といいつつ、ちょっとだけ。「面白い」。これだけは確か。面白いの内容は、「小さくて気が利くヤツだ」。C 言語並みに覚えることが少ない、とてもシンプルな言語、だと思う。それと「Windows ではまだかなり不自由」であることも言っとく。シェアドライブラリを作れる「cgo」が Windows…というか「MSVC」で全く使えない、てことまでは突き止めたとこ。(gcc は使えるので、msys2 や cygwin だと使えるんだよね。)


2021-04-30追記:
「もちっと高レベルのラッパーがあると」ではなかった。「stdout, stderr のキャプチャが不要なので」という発想が正解。というか頭では理解してはいたんだけれどもさ、正解のコード実例に辿り着けなかった。こういうこと:

 1 package main
 2 
 3 import (
 4     "os"
 5     "os/exec"
 6     //"syscall"  // "syscall.Exec" which i want to use does not support Windows...
 7 )
 8 
 9 func main() {
10     args := append([]string{"./msvcc.sh"}, os.Args[1:]...)
11     cmd := exec.Command("bash", args...)
12     cmd.Stdout = os.Stdout
13     cmd.Stderr = os.Stderr
14     cmd.Run()
15 }

(→gomsvcc-0.0.2.zip)
うん、これで済むなら、python の解の何億倍かはいくらか正解だ。デメリットがあるとすれば、Go はほとんどが静的リンクなので、exe がデカくなってしまうことくらい。それを許容できるなら、「EXE でないとは何事だ」に対する第一候補にして良かろう。

ちなみに初学者段階ではよく経験することなんだけれど、ワタシはこの正解を導く前にはこれの正解が「並列化の練習」になると思っていたので、この簡単過ぎる正解に実はかなりガッカリした。目玉だと思うんだよね、Go の並列化。まぁしょうがないので、それはそれで別途お勉強しようか。


2021-05-07追記:
「未完の」といって始めたネタが完成してしまった場合に、こうやって追記で完成させるか別ネタとして独立させるかは悩ましいところだ。まぁ所詮はどーでもいいネタだからなぁ…、追記で。

さすがに不慣れで時間がかかったが、これが目指したものである:

shexecuty.go というきゃわわなねーみんぐにしたのよ
 1 package main
 2 
 3 import (
 4     "os"
 5     "os/exec"
 6     "path/filepath"
 7     "strings"
 8     //"syscall"  // "syscall.Exec" which i want to use does not support Windows...
 9 )
10 
11 func main() {
12     mypath_noext := os.Args[0][:len(os.Args[0]) - len(filepath.Ext(os.Args[0]))]
13     mypath_noext = filepath.ToSlash(mypath_noext)
14     basename := filepath.Base(mypath_noext)
15     shell, luesst := os.LookupEnv(strings.ToUpper(basename) + "_SHELL")
16     if !luesst {
17         shell = "bash"
18     }
19     scrext, lueest := os.LookupEnv(strings.ToUpper(basename) + "_SCREXT")
20     if !lueest {
21         scrext = ".sh"
22     }
23     args := append([]string{mypath_noext + scrext}, os.Args[1:]...)
24     _, lookbinErr := exec.LookPath(shell)
25     if lookbinErr != nil {
26         panic(lookbinErr)
27     }
28     cmd := exec.Command(shell, args...)
29     cmd.Stdout = os.Stdout
30     cmd.Stderr = os.Stderr
31     cmd.Run()
32     os.Exit(cmd.ProcessState.ExitCode())
33 }

(→ shexecuty-0.0.1.zip)
「汎用にした」のだけが目立つと思うが、「ちゃんとした」のもかなり大事。今度のはちゃんとスクリプトの終了コードで死ぬようになってる。ここをちゃんとしないと、場合によっては全く実用にならない。

「汎用にした」は、もちろんこういうこと:

 1 [me@host: ~]$ unzip shexecuty-0.0.1.zip
 2 [me@host: ~]$ cd shexecuty
 3 [me@host: shexecuty]$ go build
 4 [me@host: shexecuty]$ ls -1
 5 go.mod
 6 shexecuty.exe
 7 shexecuty.go
 8 [me@host: shexecuty]$ # ↓全然違う名前でどこかに置く
 9 [me@host: shexecuty]$ ln shexecuty.exe /usr/local/bin/my_boguscript.exe
10 [me@host: shexecuty]$ # なぜなら、これ↓が「exeでないとは何事だ」と叱られたスクリプトだからだ
11 [me@host: shexecuty]$ cat /usr/local/bin/my_boguscript.sh
12 #! /bin/sh
13 something what we want to do
14 exit $?
15 [me@host: shexecuty]$ # ↓bash じゃなく ksh を使いたいとして
16 [me@host: shexecuty]$ MY_BOGUSCRIPT_SHELL=ksh my_boguscript "a long time tin"

みたいなこと。欲張るならまだ出来ることはあるが、一応最小限の汎用性ね。

なお、「欲張るなら」の一つに、「win32/win64 を意識する」もあるのだが、これは go だけでは無理ではないかという気がする。まぁ go でなくても別に簡単なわけではないんだけれども、純粋に C/C++ で作るならこれはクロスコンパイラ環境さえ準備出来れば可能なので…、ちょっと残念な気分にはなる。多少ね。

「EXEでないと海に飛び込むから」と言われてしまうような状況を考えると、もしかしたら環境変数じゃなくて設定ファイル方式にした方が取りまわししやすいかもしれんなぁ、とかね、色々ある。せっかく包んだのに、この包んだ外側にさらに何かしたくなってしまうような、つまり「このEXEを包むシェルスクリプト」が欲しくなってしまうようではあかんわけさね、だってそれ、たぶん「このEXEを包むシェルスクリプト、を包むEXE」が必要になるんだろうから。

ちなみに、「EXEねば」と脅かされている、というのが本当かどうか、一応確認した方がいい。典型的には「非MSYS(or cygwin)対MSYS(or cygwin)」という関係が成立すると概ね「EXEねば」という強迫が発生し得て、普通の Windows Python ユーザが選択する「公式 CPython」は MSYS/Cygwin でないので、要するに(たとえば)「subprocess」がターゲットにしているサブプロセス呼び出しがほぼ「プロセスは EXE でなければならない」ことを要求するハメになる。ただほんとに「絶対に EXE そのものでないとダメ」なのかといえば、「みんな大好き BAT ファイル」を起動出来るケースが少しばかりある:

 1 # -*- coding: utf-8 -*-
 2 import io
 3 import os
 4 import subprocess
 5 
 6 with io.open("my_nanika.bat", "w") as fo:
 7     print("@ECHO OFF", file=fo)
 8     print("ECHO Watashi am geronious.", file=fo)
 9 
10 # execute my bat by "os.system"
11 os.system("my_nanika")
12 
13 # or, by shell=True (will success if your python is CPython built with msvc)
14 subprocess.check_call(".\\my_nanika", shell=True)

すなわち、こういうケースで「EXEねばなりません」と考えずに「BATにあらずんば人にあらず」と考えることは出来て、そしてかなり多くの OSS がそう考え、Unix 由来のアプリケーションで書かれたシェルスクリプトを「みんな大好き MS DOS」に移植しようと試みるのだ。当然これは苦行でしかなく、「.bat」と特定してない限りはワタシはいつでも「EXEねばならぬ」と考えたいパターンである。そう、DOS/Windows の悪い癖「勝手に拡張子を補う」ことを、ここでは利用しない手はないと思う。せっかくこうやって EXE にするのが簡単になったんだから、ね。