【恒例のポエム】NYAGOS 2018年総括

もうさすがにネタ切れかなと毎年思いつつも、頼んでもないのに忘れた頃に大改造してしまうニッチな和製コマンドラインシェル、それが NYAGOS (Nihongo Yet Another GOing Shell)

NYAGOS とは(復習)

Windows向けの UNIXコマンドラインシェルです。

2018年に何をやったか

(1) バッチファイルでの環境変数・カレントディレクトリの変更を取り込めるように (4.2.5)

「NYAGOS使えない!」という理由として、よく挙げられる「環境変数設定バッチが動作しない」の「環境変数設定バッチ」が source 文なしに普通に使えるはずです。やってることは、バッチファイル呼び出しを

%COMSPEC% /V:ON /S /C "
  call "@バッチファイル名" & 
  set ERRORLEVEL_=!ERRORLEVEL! &
  (cd & set) > "%TEMP%\nyagos-(PID).tmp" &
  exit /b !ERRORLEVEL_! "

みたいな形にして、終了後に nyagos-(PID).tmp を読み込んでいるだけなんですけどね。

一応「lua_e "nyagos.option.usesource = false"」で無効化も出来ます。

(2) Luaエンジンを GopherLua へ変更(4.3.0)

lua53.dll を syscall 経由で利用していたのを、GopherLua に切り替えました。lua53.dll を使った拡張モジュール開発者の皆様には作り直しを要求することになり、たいへん申し訳ないことになってしまいましたが、本体は Pure Golang になったことで安定性が向上しました。

   > スタックトレースがちゃんと出るのって、幸せだなぁ

Go言語コードC言語のDLL → Go言語コード(CallBack) なことをやるとね、クラッシュの場所がよう分からんことになりがちなんですよ。あと、

  • 同梱する lua53.dll の32bit/64bitを間違えないように注意する必要がなくなった
  • appveyor で自動ビルドする際に lua53.dll のダウンロードに失敗する障害からも解放された※

(※ 最近の MinGW/gcc で lua53.dll をビルドすると、LIBGCC_S_DW2-1.DLL を必要としてしまう。EXEファイルをビルドする際は static リンクできるが、DLLファイルには static リンクできない。そのため、luaBinariesのように、古いバージョンの MingW/gcc をベースとした TDM-gcc で lua53.dll をビルドするか、同サイトからダウンロードするか、DLLバイナリを直接 github レポジトリにコミットするかいずれかを選ばなくてはいけなかった)

など、運用も楽になりました。

なお、Luaライブラリ切り替えに伴って、Luaへの依存部をいったん分離したので、副産物として「Luaなしバージョン」もビルドできるようになっています。cd go/src/github.com/zetamatta/nyagos/ngs && go build で Ok です。

(3) キー入力を自前から mattn/go-tty

こちらはバイナリ版は未リリースで、現在 developブランチのみで実装されています。

そもそもの目的は、キー入力の呼び出し方を「UNIX/Linux に近いスタンダードな形」に寄せることで、将来的に MSYS2 系とか汎用ターミナルとかから利用できるようにしたかったのです。

キー入力を使っていたのは自前 readline と、あと少数のコマンドのYes/No問い合わせくらいなので、Lua の切り替えと違い、これは本当に「えいやっ」で2,3日で出来ました。 (Lua切り替えは前準備に半年くらいかかったのですよ)

自前版(zetamatta/go-getch) もかなり手を入れていて思い入れはあったのですが、結局、キーだけでなく、サイズ変更とか、いろいろなコンソールイベントが一つの API 経由で入ってくるのを「わかりやすく && 汎用的」になんて到底無理だし、もう偉い人のライブラリにアウトソースした方が早いんじゃねということで、思い切って切り捨てました(ここらへんの損切り感覚は大切)

(4) Linux 対応

ネタやと思いますやろ? なんと developブランチは Linux でビルドできるようになりました。

Windows への依存が激しかった Lua とキー入力をが、Windows/Linux 双方に対応しているものに切り替えることが出来たので「もしかしていけるかな」と、残りの syscall を利用したコードを個別に *_windows.go*_unix.go とより分けてゆき、無理な機能は Windows 限定という形にしていった結果、なんかできちゃったよ!

NYAGOS on Linux
NYAGOS on Linux

DOS-Like commandline Shell for Linux, NYAGOS 誕生ですわ

まぁ、ある意味「使えるけど、使うメリットが別にない」からネタちゅーたら、ネタかもしれませんがね… なので、あんまり本格的なテストはやってません。Windows Subsystem for Linux のなかで、ぼちぼちと不具合とか暫定的に無効化している部分を潰してる感じですね。

しかし、NYAOS 2.x 時代、DOSWindowsOS/2 の 3-OSサポートで散々苦労したのに、またやるかな、俺は…

os.exec の (Command) Start で、ドライブ毎のカレントディレクトリが継承されない

カレントディレクトリは Windows/UNIX ともに1プロセスにつき一つだけ存在するということになっている。だが、DOSの頃はドライブごとにカレントディレクトリが設けられていたことを覚えている人も多いだろう。そして、現在も CMD.EXE ではあたかもドライブごとにカレントディレクトリがあるかのような振る舞いをする。親のCMD.EXE の各ドライブのカレントディレクトリは全て子プロセスの CMD.EXE にも引き継がれる。これはどういう原理だろうか。

CMD.EXE では、ドライブ毎のカレントドライブを「=C:」といったイコールで始まる名前の環境変数で、子プロセスに伝播させている。

だが、Go言語の外部プロセス実行メソッド、os.exec の (Command)Start では、内部で使われている「重複する名前の環境変数を削除する関数 dedupEnvCase」が誤動作して、2つ以上のドライブのカレントディレクトリが伝播しなくなっているようだ。

go/src/os/exec/exec.go :

func dedupEnvCase(caseInsensitive bool, env []string) []string {
    out := make([]string, 0, len(env))
    saw := map[string]int{} // key => index into out
    for _, kv := range env {
        eq := strings.Index(kv, "=")
        if eq < 0 {
            out = append(out, kv)
            continue
        }
        k := kv[:eq]
        if caseInsensitive {
            k = strings.ToLower(k)
        }
        if dupIdx, isDup := saw[k]; isDup {
            out[dupIdx] = kv
            continue
        }
        saw[k] = len(out)
        out = append(out, kv)
    }
    return out
}

これ「if eq < 0」を「if eq <= 0」にしたら直るんだけどな…

回避方法としては os.exec.Cmd クラスを使わず、os.ProcessStart 関数を直接使えばよい。os.exec.Cmd は、dedupEnvCase を読んで環境変数を整理してから os.ProcessStart を読んでいるので、同じようなことをすればよい。こういうとき、ライブラリのソースが全て公開されているのはありがたい。

.NET の Dispose メソッドの引数について(復習)

基本は implements IDisposablse を定義した時に挿入される Visual Studio のテンプレートのコメント通りにすればよい。

基本、下記のとおり

  • 常に絶対実行しないといけないもの
    • アンマネージドなリソースの解放
      • これがある時は、コメントアウトされている Finalize を復活しなくてはいけない
  • Dispose(true) … End Using や引数無しで明示的に呼ばれる時
    • マネージドなフィールドも解放する (null , Nothing を代入して解放できるようにする)
  • Dispose(false) … garbage colection で呼び出される時
    • マネージドなフィールドは別途解放中なので、そのままでよい

CMD.EXEの引数での引用符の扱いを完全に理解した

multipost →Qrunch

CMD /? で出てくるヘルプを一部引用する:

/C または /K が指定されている場合、スイッチの後の残りのコマンド ラインがコマンド ラインとして処理されます。次のルールが引用符 (") の処理に使われます:

  1. 次のすべての条件に一致する場合、コマンド ラインの引用符が有効になります:
    • /S スイッチがない
    • 引用符が 1 組ある
    • 引用符の中に特殊文字がない(特殊文字は &<>()@^| です)
    • 引用符の中に 1 つ以上のスペースがある
    • 引用符の中の文字列が、実行可能ファイルの名前である
  2. 最初の文字が引用符であるにも関わらず上の条件に一致しない場合は、最初の引用符とコマンド ラインの最後の引用符が削除され、最後の引用符の後のテキストが有効になります。

正直、まったく意味が分からんかったのだが、よくよく考えると、基本の動作は「2.」で、「1.」が特殊なケースということなのだろう。

では「1.」はどういうケースかというと:

CMD /C "C:\Program Files\HogeHoge\Aplication.exe"

といったケースのことだろう。これは、1. のような解釈をしないと「C:\Program」というコマンドに「Files\HogeHoge\Application.exe」という引数を渡しているとみなしてしまう。

ということで、通常、引用符が含まれているかもしれないコマンド列を CMD.EXE に渡すには /S を指定して強制的に「2.」を指定して:

CMD.EXE /S /C ""C:\Program Files\Hogehoge\Application.exe "ahaha""

とするのがよろしいようだ。さて、ここで注意すべき点がある。

  • 「二重引用符の中の二重引用符は \" に置換するなど特別扱いして書く」など配慮は無用機械的に " をコマンド列の前後にくっつけるだけでよい
  • ただし、CMD.EXE を呼び出すための、外部プロセスの呼び出しの各言語ライブラリが二重引用符を特別扱いすることが多い("\" 変換を勝手にする)。これを回避するテクが必要

Goの "os/exec" の場合

普通に "os/exec".Cmd.Args に引数パラメータを置くと、"\" が行われてしまう。Windows"os/exec" の隠しパラメータ?である SysProcAttr.CmdLine を使おう

package main

import (
    "os"
    "os/exec"
    "syscall"
)

func main() {
    cmd := &exec.Cmd{
        Path:        os.Getenv("COMSPEC"),
        Stdin:       os.Stdin,
        Stdout:      os.Stdout,
        Stderr:      os.Stderr,
        SysProcAttr: &syscall.SysProcAttr{CmdLine: `/S /C "DIR /w "C:\Program Files""`},
    }
    cmd.Run()
}

以上

nyagos でのバッチ起動方法を軽量化した

nyagos では、子プロセスのバッチファイルが変更した環境変数を取り込むのに

@call バッチファイル 引数1 引数2 …
@set ERRORLEVEL_=%ERRORLEVEL%
@(cd & set) > 一時ファイル名
@exit /b %ERRORLEVEL_%

といった踏み台バッチを毎回作っていた。

これはこれでコストなので、バッチファイルではなく、環境変数に踏み台コードを格納してみた。具体的には環境変数 NYAGOSCMDLINE に

call バッチファイル 引数1 引数2 … & set ERRORLEVEL_=!ERRORLEVEL! & (cd & set) > 一時ファイル名

といったテキストを格納し、これを

CMD.EXE /V:ON /C "%NYAGOSCMDLINE%"

という形で呼び出すようにした。

なぜ、環境変数に一旦保存するかというと、CMD.EXE に二重引用符を含むかもしれない文字列を渡す時、そのままだと簡単に誤動作するからである。

(このあたりはCMD /? で出てくるヘルプによると、オプションを調整すると回避できるはずなのだが、正直、意味不明で理解が難しい※)

ただ、この方式だと、/V:ON オプションがつくので本命のバッチファイルの中で「!環境変数名!」という語句が遅延展開されてしまう。今のところ、これ解消する手立てがないのだなぁ。まぁ、これで誤動作することは滅多にないと思うんだが…うーむ

※ (2018.11.12 追記)

オプションの意味が分かったので、最終的には環境変数ではなく、 CMD.EXEの引数での引用符の扱いを完全に理解した に記載のとおり、コマンドラインにベタに書く方法を使うようにした。

nyagos で、行番号シェルつくった

多分理解されないと思うけど、あの「入力が数字から始まっていたら、即実行中ではなく記憶しておき、runとタイプされると数字順に実行する」という「古代 8bit BASIC」のシステム、 意外とコマンドラインシェルで便利かもね。nyagos の Lua で書いてみたけど、BASIC気分が味わえた。

こんなの:

f:id:zetamatta:20181103205347p:plain

if not share.basic then
    share.basic = {}
end

nyagos.filter = function(line)
    local m = string.match(line,"^%d+ ")
    if m then
        share.basic[tonumber(m)] = string.sub(line,string.len(m)+1)
        return ""
    end
    m = string.match(line,"^%d+$")
    if m then
        share.basic[tonumber(m)] = nil
        return ""
    end
end

nyagos.alias.list = function()
    local list={}
    for lnum,_ in pairs(share.basic) do
        list[ #list + 1 ] = lnum
    end
    table.sort(list)
    for i=1,#list do
        print(list[i],share.basic[list[i]])
    end
end

nyagos.alias.run = function()
    local list={}
    for lnum,_ in pairs(share.basic) do
        list[ #list + 1 ] = lnum
    end
    table.sort(list)
    for i=1,#list do
        nyagos.exec(share.basic[list[i]])
    end
end

nyagos.alias.new = function()
    share.basic = {}
end

shellbasic.lua で保存して、lua_f shellbasic.lua でロードすれば Ok

What file is it ? - ファイル識別コマンド wfile を作った

これな> zetamatta/wfile: wfile is a command like file of UNIX.

ファイルのマジックナンバーを元にして、ファイルの種類を自動判別するツールである。

  • EXEファイルならば、
$ wfile wfile.exe
wfile.exe: Windows CUI, Executable Image, 64bit Header
  • テキストファイルならば
    • UTF8 or UTF16LE or UTF16BE or Others(MBCS)
    • LF改行 or CRLF改行
    • BOMの有無
$ wfile mbcs exe.go utf8
mbcs: ANSI(MBCS),CRLF text data
exe.go: ANSI(SBCS),LF text data
utf8: UTF8,CRLF text data
  • ZIPファイルなら
    • ファイル名が UTF8 フラグ付きか
    • パスワードありか
$ wfile hoge.zip test.zip
hoge.zip: Zip Archive,utf8-flag-off
test.zip: Zip Archive,utf8-flag-on
  • AutoCAD互換DWGファイルなら
    • 何年版の DWG か
  • その他のファイルなら、

といった情報を表示する。詳しくは引数なしで表示される説明を見よう

wfile.exe - what file is it ?

Usage: wfile.exe {filenames}

Signature database
for *.class:
  \xCA\xFE\xBA\xBE ... Java class file
for *.dwg:
  AC1003 ... AutoCAD EX-II
  AC1006 ... AutoCAD GX-III
  AC1009 ... AutoCAD 12,12,GX-5
  AC1012 ... AutoCAD 13
  AC1014 ... AutoCAD 14
  AC1015 ... AutoCAD 2000,2000i,2002
  AC1018 ... AutoCAD 2004,2005,2006
  AC1021 ... AutoCAD 2007,2008,2009
  AC1024 ... AutoCAD 2010,2011,2012
  AC1027 ... AutoCAD 2013,2014,2015,2016,2017
  AC1032 ... AutoCAD 2018
for *.exe:
  MZ ... Portable Executable
for *.gz:
  \x1F\x8B ... gzip compressed
for *.jpeg:
  \xFF\xD8\xFF\xDB ... JPEG Image
  \xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01 ... JPEG Image
  \xFF\xD8\xFF\xEE ... JPEG Image
  \xFF\xD8\xFF\xE1 ... JPEG Image
for *.jpg:
  \xFF\xD8\xFF\xDB ... JPEG Image
  \xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01 ... JPEG Image
  \xFF\xD8\xFF\xEE ... JPEG Image
  \xFF\xD8\xFF\xE1 ... JPEG Image
for *.lzh:
  -lh from 2 ... LHA Archive
for *.pdf:
  %PDF- ... PDF
for *.png:
  \x89PNG\r\n\x1A\n ... Portable Network Graphics
for *.zip:
  PK\x03\x04 ... ZIP Archive

おいおい対応タイプを増やしておく予定