標準愚痴出力

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

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. これは文章では分かりにくいので、もうコミット を見た方が早い気も

Git 互換DVCS、jj(jujutsu) でのテキストエディター指定における空白の扱い

基本は

に記載のとおり

  1. 環境変数 JJ_EDITOR
  2. 設定ファイルの ui.editor
  3. 環境変数 VISUAL
  4. 環境変数 EDITOR

の順で参照される。この時、1,3,4 について空白は引数の絶対的な区切リ文字として機能する。つまり

  • set "EDITOR=C:/Program Files/Notepad++/notepad++.exe" → NG
  • set "EDITOR=\"C:/Program Files/Notepad++/notepad++.exe\"" → NG
  • set "EDITOR=""C:/Program Files/Notepad++/notepad++.exe""" → NG
  • set "EDITOR=^"C:/Program Files/Notepad++/notepad++.exe^"" → NG
  • set "EDITOR=notepad++.exe" → OK (%PATH% にディレクトリが登録されている場合)
  • set "EDITOR=C:\PROGRA~1\Notepad++/notepad++.exe\"" → OK
  • set "JJ_EDITOR=C:\PROGRA~1\vim\vim91\vim.exe --literal" → OK

となるようだ。なお、C:\PROGRA~1C:\Program Files のショートファイル名(DOS互換の8.3形式のファイル名) だが、常にそうとは限らない。

2についても単純な文字列として設定する場合は 1,3,4 と同じ扱いになるが、配列として定義する場合のみ、空白を設定可能になる。つまり、以前

で記載した書き方になる。

[ui]
editor = [ "C:/Program Files/vim/vim91/vim.exe", "--literal" ]

ただし、この設定は jj config edit --repo でしか設定できず、jj config get ui.editor で現在の設定内容を確認できなくなるという制限があるようだ。うーん

ちなみに Git も

$ set "GIT_EDITOR=C:/Program Files/Vim/vim91/vim.exe"

$ git commit --amend
hint: Waiting for your editor to close the file... C:/Program Files/Vim/vim91/vim.exe: line 1: C:/Program: No such file or directory
error: There was a problem with the editor 'C:/Program Files/Vim/vim91/vim.exe'.
Please supply the message using either -m or -F option.

おんなじだった。

make-scoop-manifest の引数仕様をシンプルにした

GitHub のレポジトリから scoop 用の manifest を生成するツール: make-scoop-manifest。使い方として次の3種類があると説明をしていたが、分かりにくかった。

  1. レポジトリの場所は git remote show -n で取得し、ZIPファイルがローカルにある場合(コスト最小)
  2. レポジトリの場所はオプションで指定する場合
  3. ZIP ファイルがそもそも手元にない場合

これを 3. をデフォルトにする方向に変更した。

make-scoop-manifest {options...} [REPOSTITORY] {zip-files...}
  • -D,-g は明示的に書かなくてもよいものとした。
    • 互換性は維持しているため、別に書いてもよい(動作は変わらない)
  • レポジトリの指定は基本引数で行う
    • 従来どおりの OWNER/REPOSITORY 形式で指定可能
    • URL を書いてもよい https://github.com/nyaosorg/nyagos
    • カレントディレクトリがワークディレクトリの場合は省略できる
      • その場合、git remote show で情報を得る
  • ZIPファイルは基本的にレポジトリからダウンロードする
    • ローカルディスクの ZIP ファイルを与えることで、ダウンロードを省くこともできる(という説明にする)

結果:

  • make-scoop-manifest -g benhoyt/goawk -D > goawk.json
    make-scoop-manifest benhoyt/goawk > goawk.json
  • make-scoop-manifest -license MIT -D -g mattn/bsky -64 "" > bsky.json
    make-scoop-manifest -license MIT mattn/bsky -64 "" > bsky.json

と書けるようになった。コマンドライン的には -D-g を省けるようになっただけだが、説明がシンプルで済むのが大きい。

一方、Makefile に組み込んで、ローカルディスクにある zip ベースでマニフェストを作る場合もまったく変わらない。

manifest:
    make-scoop-manifest *-windows-*.zip > $(NAME).json

余談: 偽"flag" パッケージを作った

make-scoop-manifest -license MIT mattn/bsky -64 "" というコマンドラインは実は NG である。というのも、非オプション引数の "mattn/bsky" の後にオプション -64 を書いているからだ。標準の "flag" パッケージではこれは許されない。

だが、それだと単純に -D-g を削除して、引数の順番を変えなければいけないことを失念したユーザを戸惑わせてしまう。

ということで、非オプション引数の後にもオプション引数を取れるバージョンの「偽"flag"パッケージ」を作って対応した。

String とBool だけしか使ってなかったから比較的簡単にできた。

SAKURA Pocket を FTP over tls/rclone/restic 経由でバックアップストレージにする

コストパフォーマンス

100GB あたり

プラン 金額 月額
OneDrive(月契約) ¥260-/月 ¥260-
OneDrive(年契約) 2,440/年 ¥204
さくらのレンタルサーバ
(ライトプラン)
¥1,571-/年 ¥131

なので、FTP しか使わないなら十分安い。

自分の場合、CGIサーバを一つ維持しなければいけなかったが、ディスク容量100GBがほぼ無駄になっていたので、それを活用する意味もある。ただし、さくらのレンタルサーバーは昔からあるプランであるため、ファイル名文字コードEUC-JP だったり仕様に古いところがある

FTP over tls を使う

素のFTPだと、転送量がGBクラスのダウンロード時にエラーが発生しがちになる。信頼性確保のため、FTP over tls 接続を使う

rclone に登録する

  1. $ rclone config
  2. メニュー: Current remotes: e/n/d/r/c/s/q>n (接続先を新規作成)
  3. name> → 接続先の名前(なんでもよい。例: sakura)
  4. Storage>16 (FTP)
  5. host> → サーバ名 (*.sakura.ne.jp の方)
  6. port> → Enterのみでよい
  7. FTP username. → ユーザ名
  8. FTP password. → パスワード
  9. Use Implicit FTPS tls>false
  10. explicit_tls>true
  11. Edit advanced config? y/n>n
  12. Keep this "sakura" remote? y/e/d>y
  13. メニュー: Current remotes: e/n/d/r/c/s/q>q

適当なファイルをローカルにコピーしてテスト

$ rclone copy sakura:sakura_pocket/foo/bar .

sakura_pocket は、さくらのウェブツール さくらぽけっと で参照できるエリア。他でもよいが、こちらだとウェブブラウザでのチェックもしやすい。

restic を設定する

バックアップツールの restic から SAKURA Pocket を rclone 経由で使うには、

set "RESTIC_REPOSITORY=rclone:sakura:sakura_pocket/(さくらポケットのディレクトリ)"
set "RESTIC_PASSWORD=(restic を初期化した時のパスワード:SAKURAではない)"

と設定する。nyagos を使っている場合は .nyagos で次のように設定すればよい。

nyagos.env.RESTIC_REPOSITORY="rclone:sakura:sakura_pocket/(さくらポケットのディレクトリ)"
nyagos.env.RESTIC_PASSWORD="(restic のパスワード)"

動作確認

pushd "%APPDATA%"
restic backup .minecraft
restic check   --read-data

check サブコマンドの --read-data オプションは全データをダウンロードしてくるので、指定は初回のみにしておいた方が無難 (毎回やると転送量が大きいので迷惑になる)

check snapshots, trees and blobs
[0:04] 100.00%  5 / 5 snapshots
read all data
[34:21] 100.00%  518 / 518 packs
no errors were found

よかった。tls なしの FTP でやってたとき、ここでエラーが表示されてしまっていたのだ。--upload-limit--download-limit などで転送速度を落してもダメだったので、一時はバックアップ先を OneDrive に変更していたのだが… tls を指定することで解決してよかった。あきらめきれず、未練がましくマニュアルをグダグダ見るのも無駄ではなかった。