標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

【解説】ターミナル用データベースクライアント SQL-Bless で CSVI をページャとして使うようにしてみました

もともと SQL-Bless って、SELECT 結果を CSV 形式で出力するようになっているんですよね。

で、CSVI はファイルだけでなく、標準入力から CSV データを読み込むことができるようになっている。

これは連結すると便利じゃね?ということで、SQL-Bless に CSVI をページャとして組み込んでみました。

(1) CSVI をどのように連携させるか

CSVI を more/less のようにOSのパイプラインでつないで別プロセスとして呼び出すことも考えたんですが

  • CSVI が存在しないとき、どういう振舞いをさせるか
  • CSVI.exe のありかをどうユーザに指定させるか

という点を考えると邪魔くさいので、CSVI をGo言語のパッケージ化して静的リンクさせてしまうことにしました。

(2) CSVI をGo言語パッケージ化

CSVI の main 関数は "github.com/hymkor/csvi/cmd/csvi" へ移動して、"github.com/hymkor/csvi" は Go言語のパッケージとして利用できるようにしました。

いろいろ試行錯誤の結果、現在の呼び出し方は次のような形で落ちついています。

// github.com/hymkor/sqlbless/csvi.go より

cfg := &csvi.Config{
    CellWidth:   14,
    HeaderLines: 1,
    FixColumn:   true,
    ReadOnly:    true,
    Message:     titleBuf.String(),
}
// :
// pIn io.Reader - CSVソース
// out io.Writer - 標準出力など
//
cfg.Main(&uncsv.Mode{Comma: ','}, pIn, out)

構造体 Config の内容は、旧csvi では "flag" パッケージで外部から与えていたオプション群が主に入っています。

(3) SQL-Bless側

SQL-Bless 側では SELECT の出力を、csv.Config.Main の入力を io.Pipe によるパイプラインでつなげるだけです。

SELECTの結果を一旦 CSV で出力させて、それを CSV パーサで読みとるなんて、無駄きわまりないところですが、その分、互いのモジュール間の結合が簡単に済みます。つまり、SQL-Bless は必要以上に CSVI の内部仕様に踏み込まなくてよく、CSVI の仕様変更にふりまわされる可能性が低くなるというわけです。 ( 今のところ、入り口関数の csvi.Config.Main の仕様だけをチェックしておけばよい )

では、画面を見てみましょう。

普通の SQLコマンドラインクライアントだと、列や行が多すぎて多数の折り返しやスクロールで流れていって、まったく見られたものではないことが多いの考えると、カッケーでしょ?

しかも、何行にも長いSQLの編集も go-multiline-ny の複数行のインライン編集機能で無理なく可能。これはもう究極のコマンドラインクライアントと言っても過言ではないのでは?

csvi 以外の最近の活動(2024年4月)

go-readline-ny v1.3.1

サブパッケージ completion で、空状態で補完するとクラッシュする不具合を修正するプルリクエストをいただきました。ありがたい

あと、csvi でやった一文字キー入力の仮想化をこっちでもやりました。前から ITty という interface で go-tty への依存にワンクッション入れてたんですが、細かすぎるメソッドを要求していて微妙だったので、ざっくり Open/GetKey/Size/Close だけを必要要件にしました。これでも少なくはないですが、差し替えるものを作れなくもないレベルまで下ったかと思います。これを利用して、テストに使うオートパイロットパッケージ github.com/nyaosorg/go-readline-ny/auto を作成しました。

実はこれで互換性は破壊されてしまっているのですが、ITty をリプレースしている人なんてまずいないので、多分、大丈夫だと判断しました。

nyagos issue対応

nyagos に inshellisense を組み込もうという人からの issue。4.4.15_0時点の機能では必要と思われる機能がいろいろ足りなさそうだったので

  • Unicode ではなく、上矢印キーをESC[1A と表現するキーシーケンスを得る関数 nyagos.getkeys
  • キーシーケンスに対応した編集機能を呼び出すコールバック関数 (callback):eval

など、いろいろ追加しました。

が、最終的には、それでも開発効率の悪さから、ちょっとダメだこりゃとなってしまったようです。まぁ、仕方ないかなと思ってます。nyagos のエコシステムを発達させるには、ユーザが少なすぎるんですよね。ユーザからのフィードバックが十分にないと、無駄に使われない機能ばかり追加したり、プロジェクトは迷走します。それを自覚したがゆえに、自分がどうしても欲しい機能 もしくは 明確にユーザからリクエストがあった機能のみを追加する保守モードへ切り替えたんですよ…

正直、ちょっとつらい話でした。

SQL-Bless

新バージョンのリリースはしていないんですが、

  • CSVI 同様に -auto オプションを追加
  • テストコードを Expect-Lua から PowerShell に書き直した

-auto オプションの文法は CSVI とはちょっと違っていて改行に相当する部分は | 、送信(Ctrl-J,Ctrl-Enter) に相当する部分は || と書きます。go-multiline-ny 側にもテストコードを追加しているので、複数行編集の細かいテストはこっちには入れていません。

-auto オプションの追加で、キーボード操作を無理矢理よこどりしてのテストは無用になったので、テストスクリプトPowerShell で書き直しました。Expect-Lua もどこが悪いということはないんですが、テストコードを走らせる要件は低いにこしたことはないので。 ( PowerShell は一応 Linux 版もあるし、Windows では一応一般的なので )

go-box

nyagos で Ctrl-O を押した時に起動するファイルセレクタ的な用途に使うパッケージです。書き方が自分的に古いままになっていたので、いろいろ直しました。

  • エラーの時に panic していたが、全関数に対して、error を返す版バージョンを用意した。
  • 関数名が直感的に分かりにくかったので、改名・集約を行なった

【解説】 CSVI v1.8.1 【これで完成?】

v1.8.0

  • まだ全行読み切っていないとき、ステータスラインの読み込み済み行数欄を毎秒4回更新するようにした

今まではキー入力直後にしか更新していなかったので「なんか全行数が少なく見えるな」と誤解することがあったわけですが、ほぼリアルタイムで行数が更新することで、たぶんそういうことがなくなったかなと。

binview が同じ動作をしていたので、それをコピーするだけで済みました(小難しいことはなく、バックグラウンドで行を読み込むところで、単に前にステイタスラインを更新してからの秒数が 1/4 秒を越えたら、更新するようにしただけ)

  • 遅い端末向けに、ERASELINE (ESC[K) を出力する回数を削減して、表示更新速度を改善

仮想マシン上で実行させてみたところ、すごく遅かったので、改善にトライしました。

今までは、列中の空白部分をクリアするのに、列ごとに ERASELINE を発行していたんですが、それを最初に一回に集約しました。

で、本当に速くなったのか、主観で判断していても怪しいので、-auto を使ったベンチマークをちゃんと用意して、それで判断しました。最初は ERASELINE よりは必要最小限の空白を出力した方が速いんちゃうかと思ってたんですが、実際ベンチで見るとそうでもなかったんですよね (しかし、それでもなお、仮想マシン上の端末では結構遅い)

v1.8.1

  • 引数なしで csvi を起動すると落ちる問題を修正

これは致命的でした。v1.6 あたりでデータの持たせ方を変えたんですが、引数なし = ドラフトとして生成したデータ行1行というケースのテストが漏れていました。

これはいかんということで、今後くりかえさないように、テストコードに引数なしで起動するものを急遽追加しました。

  • テキストがないセルにカーソルがある時にセルが反転しない問題を修正

ERASELINE の出力回数を削減した時のエンバグでした ( カーソルがあるセル+次の限っては ERASELINE を出力しないと、テキストがない部分を反転させられない)

  • カーソルの文字色が黒ではなく灰色になっていた不具合を修正

いつからこうなってましたっけ?

  • echo "foo" | csvi -auto "w|-|q|y" > foo.txt で foo.txt が foo\r\n になるように、引数ゼロ時の標準出力と標準エラー出力の使い分けを変更した。

引数なしで csvi を起動した時のテストの都合、標準出力のノイズをとっておきたかったので


うーん、v1.*.0 で機能追加して、v1.*.1 で不具合を直すということが日常化している。よくないなぁ

しかし、これで機能的にはほぼ完成で、今後だいたんな改変の必要性も特に感じない(コードで気持ち悪いところもない)ので、まぁ、よし!

Windows Terminal の内部的なコードページがおかしくなる現象

Windows Terminal、プロファイル「コマンドプロンプト」を開き、CMD.EXE のコマンドラインの中から手動で wsl.exe を起動するなりして、一度でも Linux ターミナルとして使用すると、そのあと exit で CMD.EXE に戻ってきても、どうもどこかコードページの設定がおかしくなってしまうようだ。

OSの日付設定の中に曜日文字列が入っていると、cmd /c "echo %DATE%" > date.txt としたとき、曜日文字の漢字が現在のコードページ(Shift_JIS)ではなく、UTF-8 になってしまうことを確認した。なお、リダイレクトをしない場合だと UTF-16 の方の API が呼ばれるため、問題は表面化しない。

プログラムの不具合かと思ったが、その Windows Terminal のタブを閉じて、画面を開きなおすと、期待どおり Shift_JIS で出力される。

> こんなの俺くらいしか、気づかないし、誰にも理解してもらえないヨ <

しかも、これ、最短手順だと再現しない。何かを行ったタイミングで端末の内部状態がおかしくなってしまうようだ。今のところ、それが何かがよく分からない。こまったなぁ ( まぁ、wsl.exe を起動しなければ発動しないので、実害はないのだが )

【解説】 CSVI v1.7.1 をリリースしました。

前に説明したのは v1.5.0 だったので、v1.6.0, v1.7.0 の変更点も合わせて

CSView → CSVI と名前を変えました

  • View という単語が名前に入ってると読み取り専用ビューアという印象があるが、今は編集可能になっている
  • 同カテゴリに CSView という名前のツールが結構ある

ということから改名した方がよいな思いつつ、今頃になってしまいました。

名前を変えると、変えてからしばらくすると微妙になってくることがたまにあるんですが、今回は大丈夫だったみたいです。今のところ、特に「えぇー」という声もなさげです (本当かな)

ただ、英大文字・英小文字の表記には迷いがあって、まだ、その時の気分で CSVI, Csvi, csvi と表記に揺らぎがあります(あかん)

バックグランド読み込み対応

「数百メガのファイルも読み込める」とお褒めの言葉をいただいたので、実際に開いてみたら、表示までしばらく待たされることが判明しました。

こりゃいかんということで、binview の internal パッケージ (internal/nonblock) を丸コピーしてきました。

最初の100行程度を読み込んだ時点で画面を開き、残りはキー入力待ちの間にデータを読み込ませるようにしました。これによって、十数秒間の待ち時間はほぼ一瞬になりました。

このあたりはノウハウがあったから実装は簡単だったんですが、ちょっと使い方が分かりにくいライブラリなので、もうちょっと見易くできないかなと試行錯誤もしたあげく、結局そのまま利用という形になりました。

UTF-16対応の強化

  • UTF-16 かどうかを判断する先読みバイト数を10バイト数に
  • 強制的にUTF-16と判断させるオプション -16le, -16be を追加

現在の判断ロジックは

  • オプション -16be が指定されていたら UTF-16BE
  • オプション -16le が指定されていたら UTF-16LE
  • \xFE\xFF で始まっていたら UTF-16BE
  • \xFF\xFE で始まっていたら UTF-16LE
  • 最初の10バイト以内の奇数バイト位置に \0 があれば UTF-16BE
  • 最初の10バイト以内の偶数バイト位置に \0 があれば UTF-16LE

という、「なんとしてもUTF-16 を見逃さないぞ」という体制になっています

なぜ、ここまで UTF-16対応に力を入れるかというと、 前職の時に作ったアプリケーションのデータファイルをUTF-16LEのTSVにしてしまったことに、すごい悔いがあった からかもしれません。 ( 閲覧も編集も面倒くさい)

自動処理対応

csvi -auto "<|$|a|ほげ|w|bar.csv|q|y" foo.csv

で、foo.csv の最初の行の末尾に「ほげ」というフィールドを追加して、bar.csv というファイルとしてセーブするという操作を指示できます。

これは実用を考えたものよりは、自動テストのために作りました1。前は Expect-Lua を使ってテストコードを書いていたんですが、

  • 端末画面がない自動テスト環境(CI)では使えない可能性がある
  • タイミングで失敗することもありえる
  • テストのプラットフォームも Windows だけになってしまう

という問題がありました。この -auto オプションを使えば、タイミングがずれることはありませんし、外部プロセスを呼び出せる任意の言語でテスト可能になります。2

なお、今回、端末入力の一般化/オートパイロット化の仕組みのコツがつかめたので、他のアプリにも適用してゆくかもしれません。

【不具合修正】幅の判定が難しい文字が現れても、表示位置がズレないよう対処した

今までは文字の幅を測って、次の列まで足りない桁分の空白を出力していたんですが、どうやっても幅が推定うまく推定できない文字というものもあるようです(runewidth ライブラリを使って、east-asian ambiguous width を配慮しても、なんかおかしい時がある)

( CSView v1.5 で、Windows10のコマンドプロント上から某CSVファイルを表示させた結果。ガタガタだ。Windows Terminal だと、ここまで酷くはない)

これに対し、もう空白を桁位置を調整する方式はあきらめて、ANSI エスケープシーケンスの ESC[%dG (水平方向の%d 桁目まで移動)で次の列位置まで移動させるようにしました。

(CSVI v1.7.1 で同じファイルを表示。きれいになった!)

この方式をやる場合、ESC[K(カーソル位置以降を削除)と組み合わせる都合上、画面書き変え量が増えるという問題があります。普通の環境だと問題はないんですが、仮想マシンの中のターミナルだと、かなりモッサリします。ですが、それでも全く使えないレベルでもないので、これは許容範囲としました。

【不具合修正】> の後の o で、最終行の前の改行コードが欠けてしまう不具合を修正

これは各行の改行コードは極力現状維持させるという仕様のせいでした。 最終行の末尾は EOF なので改行コードがないんですよね。そのまま次の行を追加すると、保存時に改行コードなしのまま出力するので、次の行と連結されてしまいます。

こういう例外ケースの場合は、デフォルト改行コード(そのファイルで最初に見付かった改行コード)を挿入するようにしました。

【不具合修正】 長い行から短い行に移動した後のカーソル位置が無効になることがあり、編集するとクラッシュする不具合(v1.6.0-)を修正

v1.6.0 で、それまでスライスで全行を管理していたんですが、ギガクラスに対応できるよう、binview 同様に "container/list" を使うよう、書き換えました。その際にエンバグしてしまいました。

たとえば、 5列ある行の5列目にカーソルがある時に3列しかない行に移動すると、v1.5.0 までだと3列目にカーソルが移動するんですが、v1.6.0-v1.7.0 ではカレントセルがない状態になってしまっていました。

この動作字体は気付いてはいたんですが、別に落ちたりする様子がなかったため、そういう仕様だったかと勘違いして放置していました。が、いざセルを編集してみたら即クラッシュ。とほほ ( 直接の原因は、ポインタを更新漏れ )


これで、CSVI は端末用としては、かなり完成に近い形になったと思います。

CSView からは、CSVI とは別に、Lisp を内蔵させてスプレッドシート化できないかとトライした Lispred という fork を作ったこともありました。こちらは行や列を挿入した時に Excel のように参照するセルの参照先を自動的に調整するのが難しく、これを完璧に実用化しても手間の割が合わないなということで放置状態になっています。

ということで、原点回帰で「 本番データの手直しに使える高速・軽量の端末向けCSVエディター 」というポリシーをこちらですすめてきたわけですが、どうやら正解だったようです。 ( STAR の数も18に増えました。まさか binview を抜くとは )


  1. 実用を考えるなら、GopherLua なり gmnlisp なりを内蔵して、本格的なスクリプティングをサポートすべきだが、そのような需要は無いと判断している。そこまでするなら、Go言語で、本ツールの内蔵ライブラリの github.com/hymkor/csvi/uncsv を import して、別ツールを作った方がよい
  2. とはいえ、作り直したテストコードは結局 PowerShell なんですけどね

今更だけど、Go では (具体的なレシーバー.メソッド名) を「関数ポインタ」扱い出来た件

たぶん、有識者では常識なんだろうけれども、最近まで認識できていなかった件についてのポエム

type T struct {
    // :
}

func (t *T) Method(){
    // :
}

var t T

と定義されている場合、

var f func() = t.Method

という代入が可能。これの何が嬉しいのかというと、コールバック関数を渡さないといけない時に、具体的な値をあわせて引き渡すことが可能だという点。

昔の自分はそういうのを知らなかったので、呼び出し元で context.WithValue で引き渡すべき値を context.Context に添付し、コールバック関数側は context.Value で引き出すという方法を使っていた。それが悪いというわけではないが、本来静的チェックできるものを動的にチェックとなるので、まれに漏れがあって実行時エラーになったりする。

実際、nyagos にそういうコードが残ってたので、ちょっとそういうの、ちょっとずつ直してる。

( nyagos は Lua 依存のところと、Lua 非依存のところを明確に分けているので、コールバック関数を設定するところで、Lua 引数を想定させていない箇所も多いのだ )

旧コード

type luaKeyT struct{}

var luaKey luaKeyT

// コールバック関数側
func onCommandNotFound(ctx context.Context, sh *shell.Cmd, err error) error {
     L, ok := ctx.Value(luaKey).(Lua)
     if !ok {
          return errors.New("could get lua instance(on_command_not_found)")
     }
     // : 中略
}

// コールバック関数を設定する側
shell.OnCommandNotFound = onCommandNotFound

新コード

type _LuaCallBack struct {
    Lua
}

// コールバック関数側
func (this *_LuaCallBack) onCommandNotFound(ctx context.Context, sh *shell.Cmd, err error)
error {
    L := this.Lua
     // : 中略
}

// コールバック関数を設定する側
shell.OnCommandNotFound = (&_LuaCallBack{Lua: L}).onCommandNotFound

これ、設定漏れがあると could get lua instance(on_command_not_found) という実行時エラーになるんだけど、実際、発生するケースが見付かったりした。

context.WithValue は便利そうに見えて使用事例が意外とないのは、こんな風な値の引き渡し方法が既にあるから、使うケースがレアであるべき…ということがあるのかもしれないなぁ。

最近、更新したプロダクト

CSView v1.5.0

文字コードとして UTF16 をサポートしました。

  • 最初の2バイトが \xFE\xFF、もしくは最初の1バイトが \0 の時
    → UTF16BE
  • 最初の2バイトが \xFF\xFF、もしくは二番目の1バイトが \0 の時
    → UTF16LE

と判断するようにしました。今どき、UTF16 なんて使っているところなんで、ほとんど無いとは思うんですが、昔、自分が仕事でたずさわったツールが UTF16LE の TSV なんて使ってたんですよね… 昔の自分をたすけるつもりで対応しました。

はてなブログクライアント v1.1.0

  • オプション引数を非オプション引数の後にも置けるようにした。
    (例) htnblog -n 10 listhtnblog list -n 10 と書くことが可能

書き忘れて、よくカーソルを戻したりするので

  • htnblog new: ドラフトのコメント欄に End-Point-URL を挿入するようにした

はてなブログは3つまで無料でブログを管理できるんですが、投稿先を勘違いして事故らないように…と

  • htnblog from-stdin: 標準入力から新しい記事を読み込むサブコマンドを用意した
    (フォーマットは htnblog new で表示されるものと同じ)

昔、wifky で書いた「にっき」を今サルベージして、markdown化しているんですが、それのインポートを視野に入れての対応です。

  • エディター設定に "C:/Program Files/vim/vim91/vim.exe" --literal など空白・二重引用符・オプションを含められるようにした

今までは"C:/Program Files/vim/vim91/vim.exe" --literal という名前の実行ファイルだと解釈されていました。本修正で jj や git の動作と同様にスペースは引数との区切り文字だと解釈させるようにしました。そして、jj や git と違って、二重引用符を適切に解釈するようにしていますので、空白が含まれた実行ファイルパスでも大丈夫です。

Sponge v0.2.0

sponge は Go版と Rust 版を作ってますが、Go版の方です。

  • Windowscat -n < FILE | sponge FILE 形式で実行した時、シェルが FILE をクローズしていないため、エラーになる不具合を修正 (一旦、リネームだけして、少なくとも差し替えだけは完了させるようにした)

こちらも、wifky の「にっき」のサルベージ中に気付きました。

cat -n FILE | sponge FILE の時は大丈夫だったんですが、cat -n < FILE | sponge FILE だと、FILE をクローズするタイミングが遅いので、sponge が上書きリネームする時に失敗するという問題がりました。次のように修正しました (ファイル名は例です)

  • 旧動作:
    1. Rename FILE.tmp → FILE … FILE がオープンされているのでエラー
  • 新動作:

    1. Rename FILE → FILE~ … オープンされているファイルのリネームは可能
    2. Rename FILE.tmp → FILE
  • 元ファイルを別名で残すオプションを用意(-b 接尾語)

  • 一時ファイルと同名のファイルが存在している時、エラー終了させるようにした

上だと FILE.tmp に相当する名前のファイルが既にあるとエラーになります。

  • 新ファイルのパーミッションを旧ファイルと同じになるようにした。
  • 一時ファイルのフォーマットとして (original-name)-sponge(process-id) を使うようにした

FILE.tmp とかいう名前だと、ぶつかりやすいので

  • エラーや Ctrl-C で中断された時、ゼロバイトのファイルを作成しないようにした

これはファイルを遅延openする型を作りました。最初の1バイトを書き込むまで open 処理を実施しないようにしただけです。

  • -h で、args[0], バージョン, OS, GOARCH を標準エラー出力に表示するようにした。

これは自分の標準的な動作です。

nyagos 4.4.15_0 → 少し延期

4月1日にリリースするつもりでしたが、3月31日に不具合修正をしたので、様子見期間を設けるため、一週間ほど延期です。

Ctrl-C の入力で、今入力している行を無効化していたのですが、foreach においても「それだけの動作」になっていました。Ctrl-C は内部的には readline.CtrlC というerror型で処理していたんですが、それを判断する箇所をより上位で行うような形にして、foreach 文にて CtrlC を判断できるよう修正しました。1

その他は docs/release_note_ja.md に書いています。SKK 関連の修正はかなり前に行っていたのですが、それだけでも、もっと早く 4.4.14_1 という体裁でリリースしておいた方がよかったかもしれません。


  1. これは文章では分かりにくいので、もうコミット を見た方が早い気も