2024年4月18日木曜日

Vim の実行バイナリからその Vim のバージョンを確認したい

今使っている Vim のバージョン(e.g., 9.1.0342) を、 Vim の実行バイナリから取得する方法のメモ。

1 行で

vim -Es +"let vl=v:versionlong | put =printf('%d.%d.%04d', str2nr(vl[0]), str2nr(vl[1:2]), str2nr(vl[3:])) | print | q!"

目的

「最新リリースと比較して、新しいものがあれば再ダウンロードする」というのをやりたい。

Vim を、Windows で使ったり、 dev container に転送して使ったりしているため、ポータブルな形で配布されている Vim をよく利用しており、 利用している Vim のバージョンを更新しようとしたときに、「今ここにあるこのバイナリのバージョンはいくつだ?」をシェルスクリプトから確認したくなった。

そのためにはコマンドラインから Vim を起動し、バージョン番号を標準出力へ出力してもらわなければいけない。

ポータブルな形で配布されている Vim の例:

前提

動作確認環境は以下。

バージョン取得方法の説明

vim -Es +"let vl=v:versionlong | put =printf('%d.%d.%04d', str2nr(vl[0]), str2nr(vl[1:2]), str2nr(vl[3:])) | print | q!"
  • Vim の起動オプションの話
    • -E: Vim を「拡張 Ex モード」で起動
    • -s: サイレントモード。「Ex モード」「拡張 Ex モード」での実行時に、一部コマンドの実行結果以外を標準出力へ出力しないようにする
      • 標準出力されるコマンドは以下
        • print
        • list
        • number
        • set
  • Vim script の話
    • v:versionlong: パッチレベルを含めたバージョン文字列(9.1.0000 の場合 9010000 となる)
    • printf: C 言語と同じフォーマットで整形できる関数。前述の v:versionlong の桁を切り取ってフォーマットするのに利用
    • str2nr: 文字列を数値に変換する関数
    • put: バッファに文字を出力。 put =print...(略) で、 print の戻り値をバッファへ出力している
    • print: バッファの、現在のカーソルがある行を標準出力する
    • q!: 現在のバッファを破棄して Vim を終了する

参考資料

2024年4月16日火曜日

Vim の CUI 版に描画テストを追加して実行する

以下 3 点を忘れないようにメモ。

  • ビルド環境構築方法
  • CFLAGS へのワーニングフラグ追加
  • 一部のみテスト実行方法
  • ターミナルの描画テスト実装方法

前提

  • OS: Ubuntu 22.04

ビルド環境構築方法

ビルドパッケージ取得用に deb-src ソースを追加

後述の apt build-dep を利用するために、 /etc/apt/sources.list のコメントアウトされている deb-src を有効にする。

sed -i -e 's/^# deb-src /deb-src /' /etc/apt/sources.list

vim-gtk3 のビルドに必要なパッケージのインストール

apt build-dep を利用し、Debian が vim-gtk3 のパッケージをビルドするために使っているパッケージをインストールする。

sudo apt build-dep vim-gtk3

CFLAGS へのワーニングフラグ追加

CI では、以下のようにワーニングフラグの追加を行っているので、 Pull request を送るならローカルでもこれらフラグを追加してビルドすること。

ci.yml#L242-L248 - vim/vim

テスト実行方法

全テスト実行

src ディレクトリ内で make test する。

cd src
make test

部分的なテスト実行

src/testdirtest_*.vim が複数あるので、ファイル名の拡張子を除いたものを make に渡すと、その中のテストのみを実行できる。

cd src/testdir
make test_utf8

ターミナルの描画テスト実装方法

CUI の描画テストを行う場合には、Vim が提供している、ターミナル出力をダンプ・比較する仕組みを使う。

5. 画面ダンプの差分 - terminal - Vim日本語ドキュメント の「Vimの画面ダンプテストを書く」の通りにやればよい。

  1. ターミナルダンプの正解ファイルを生成する(最初は空ファイル)
    • src/testdir/dumps に入れる
  2. 「1.」で作ったファイルを正解ファイルとして読み込むテストを実装する
    • テストの実装は後述
  3. 「2.」で作ったテストを実行する
    • テストが失敗して、失敗時のターミナルダンプが src/testdir/failed に出力される
  4. 新しい vim を立ち上げ、失敗時のターミナルダンプを読み込み、想定通りになっているかを確認
    • vim -u NONE -N して :call term_dumpload("./failed/<「2.」で出力された失敗時のターミナルダンプ>")
  5. 「4.」の結果が想定通りであれば、失敗時のターミナルダンプで「1.」で作った正解ファイルを上書きする
    • cp failed/<「2.」で出力された失敗時のターミナルダンプ> dumps/<「1.」で作った空ファイル>

テスト実装

これも 5. 画面ダンプの差分 - terminal - Vim日本語ドキュメント の「Vimの画面ダンプテストを書く」の通りにやればよい。

例えば以下のように実装する。

func Test_setcellwidths_with_non_ambiwidth_character_dump()
  CheckRunVimInTerminal

  " テスト開始時の初期状態までもっていくためのスクリプトを定義
  let lines =<< trim END
      call setline(1, [repeat("\u279c", 60), repeat("\u279c", 60)])
      set ambiwidth=single
  END
  call writefile(lines, 'XCellwidthsWithNonAmbiwidthCharacter', 'D')

  " vim を開いてテスト開始時の初期状態までもっていく(`-S` は「Vim を起動した後に指定したファイルを Vim script として実行する」オプション)
  let buf = RunVimInTerminal('-S XCellwidthsWithNonAmbiwidthCharacter', {'rows': 6, 'cols': 50})

  " 初期状態から、「1.」で作った正解ファイルの状態になるまでの操作を実行
  call term_sendkeys(buf, ":call setcellwidths([[0x279c, 0x279c, 1]])\<CR>")
  call term_sendkeys(buf, ":echo\<CR>")

  " 「1.」で作った正解ファイルと比較する。差分があった場合テストが失敗する。
  call VerifyScreenDump(buf, 'Test_setcellwidths_with_non_ambiwidth_character_dump_1', {})
endfunc

参考資料

変更履歴

日付 内容
2024/4/16 新規作成
2024/4/17 CFLAGS へのワーニングフラグ追加について追記

2024年4月7日日曜日

Go で JSON をマージする(darccio/mergo 編)

mikoto2000/devcontainer.vim: コンテナ上で Vim を使った開発をするためのツール。 VSCode Dev Container の Vim 版を目指しています。 の中で、 JSON をマージしたいという要求が出てきたので darccio/mergo: Mergo: merging Go structs and maps since 2013 を試す。

正確に言うと、「構造体・マップをマージするライブラリ」だが、 Marshal 後の JSON でも使えるので試す。

開発環境起動

docker run -it --rm -v "$(pwd):/work" --workdir /work -v "$HOME/.vim:/root/.vim" golang:1.22.1-bookworm

環境構築

go mod init github.com/mikoto2000/golang/json/mergo/firststep
go get dario.cat/mergo

JSON ファイル作成

json/base.json:

{
  "name":"Go",
  "image":"mcr.microsoft.com/devcontainers/go:1-1.22-bookworm",
  "mounts": [
    {
      "type": "bind",
      "source": "${localEnv:HOME}/.gitconfig",
      "target": "/home/vscode/.gitconfig"
    }
  ],
  "features":{},
  "remoteUser":"vscode"
}

json/additional.json:

{
  "mounts": [
    {
      "type": "bind",
      "source": "${localEnv:HOME}/.vim",
      "target": "/home/vscode/.vim"
    }
  ]
}

プログラム実装

JSON のスキーマファイル定義

devcontainer/schema.go:

package devcontainer

import (
        "encoding/json"
)

type Devcontainer struct {
        Name       string
        Image      string
        Mounts     Mounts
        Features   interface{}
        RemoteUser string
}

type Mounts []Mount

type Mount struct {
        Type   string
        Source string
        Target string
}

func UnmarshalDevcontainer(data []byte) (Devcontainer, error) {
        var d Devcontainer
        err := json.Unmarshal(data, &d)
        return d, err
}

主処理実装

main.go:

package main

import (
        "fmt"
        "os"

        "dario.cat/mergo"

        "github.com/mikoto2000/golang/json/mergo/firststep/devcontainer"
)

const baseJsonPath = "json/base.json"
const additionalJsonPath = "json/additional.json"

func main() {
        baseJson, err := parseJsonFile("json/base.json")
        if err != nil {
                panic(err)
        }
        fmt.Printf("=== %s ===\n", baseJsonPath)
        fmt.Printf("%+v\n", baseJson)
        fmt.Println()

        additionalJson, err := parseJsonFile("json/additional.json")
        if err != nil {
                panic(err)
        }
        fmt.Printf("=== %s ===\n", additionalJsonPath)
        fmt.Printf("%+v\n", additionalJson)
        fmt.Println()

        // マージオプションは以下を参照
        // https://github.com/darccio/mergo/blob/cde9f0ea26cccb1168ee3900cf8ca457bb928c3c/merge.go#L329-L372
        mergo.Merge(&baseJson, additionalJson, mergo.WithAppendSlice)
        fmt.Printf("=== Merged JSON %s and %s ===\n", baseJsonPath, additionalJsonPath)
        fmt.Printf("%+v\n", baseJson)
        fmt.Println()

}

func parseJsonFile(jsonFilePath string) (devcontainer.Devcontainer, error) {
        jsonContent, err := os.ReadFile(jsonFilePath)
        if err != nil {
                return devcontainer.Devcontainer{}, err
        }

        d, err := devcontainer.UnmarshalDevcontainer(jsonContent)
        if err != nil {
                return devcontainer.Devcontainer{}, err
        }

        return d, nil
}

実行

root@a67c959ef4b8:/work# go run main.go
=== json/base.json ===
{Name:Go Image:mcr.microsoft.com/devcontainers/go:1-1.22-bookworm Mounts:[{Type:bind Source:${localEnv:HOME}/.gitconfig Target:/home/vscode/.gitconfig}] Features:map[] RemoteUser:vscode}

=== json/additional.json ===
{Name: Image: Mounts:[{Type:bind Source:${localEnv:HOME}/.vim Target:/home/vscode/.vim}] Features:<nil> RemoteUser:}

=== Merged JSON json/base.json and json/additional.json ===
{Name:Go Image:mcr.microsoft.com/devcontainers/go:1-1.22-bookworm Mounts:[{Type:bind Source:${localEnv:HOME}/.gitconfig Target:/home/vscode/.gitconfig} {Type:bind Source:${localEnv:HOME}/.vim Target:/home/vscode/.vim}] Features:map[] RemoteUser:vscode}

参考資料

Go で JSON をマージする(Jeffail/gabs 編)

mikoto2000/devcontainer.vim: コンテナ上で Vim を使った開発をするためのツール。 VSCode Dev Container の Vim 版を目指しています。 の中で、 JSON をマージしたいという要求が出てきたので Jeffail/gabs: For parsing, creating and editing unknown or dynamic JSON in Go を試す。

開発環境起動

docker run -it --rm -v "$(pwd):/work" --workdir /work -v "$HOME/.vim:/root/.vim" golang:1.22.1-bookworm

環境構築

go mod init github.com/mikoto2000/golang/json/gabs/firststep
go get github.com/Jeffail/gabs/v2

JSON ファイル作成

json/base.json:

{
  "name":"Go",
  "image":"mcr.microsoft.com/devcontainers/go:1-1.22-bookworm",
  "mounts": [
    {
      "type": "bind",
      "source": "${localEnv:HOME}/.gitconfig",
      "target": "/home/vscode/.gitconfig"
    }
  ],
  "features":{},
  "remoteUser":"vscode"
}

json/additional.json:

{
  "mounts": [
    {
      "type": "bind",
      "source": "${localEnv:HOME}/.vim",
      "target": "/home/vscode/.vim"
    }
  ]
}

プログラム実装

package main

import (
        "fmt"

        "github.com/Jeffail/gabs/v2"
)

const baseJsonFilePath = "json/base.json"
const additionalJsonFilePath = "json/additional.json"

func main() {
        parsedBaseJson, err := gabs.ParseJSONFile(baseJsonFilePath)
        if err != nil {
                panic(err)
        }
        fmt.Printf("=== %s ===\n", baseJsonFilePath)
        fmt.Println(parsedBaseJson.StringIndent("", "  "))
        fmt.Println()

        parsedAdditionalJson, err := gabs.ParseJSONFile(additionalJsonFilePath)
        if err != nil {
                panic(err)
        }
        fmt.Printf("=== %s ===\n", additionalJsonFilePath)
        fmt.Println(parsedAdditionalJson.StringIndent("", "  "))
        fmt.Println()

        parsedBaseJson.Merge(parsedAdditionalJson)
        fmt.Printf("=== Merged JSON %s and %s ===\n", baseJsonFilePath, additionalJsonFilePath)
        fmt.Println(parsedBaseJson.StringIndent("", "  "))
        fmt.Println()
}

実行

root@696abd662a3a:/work# go run main.go
=== json/base.json ===
{
  "features": {},
  "image": "mcr.microsoft.com/devcontainers/go:1-1.22-bookworm",
  "mounts": [
    {
      "source": "${localEnv:HOME}/.gitconfig",
      "target": "/home/vscode/.gitconfig",
      "type": "bind"
    }
  ],
  "name": "Go",
  "remoteUser": "vscode"
}

=== json/additional.json ===
{
  "mounts": [
    {
      "source": "${localEnv:HOME}/.vim",
      "target": "/home/vscode/.vim",
      "type": "bind"
    }
  ]
}

=== Merged JSON json/base.json and json/additional.json ===
{
  "features": {},
  "image": "mcr.microsoft.com/devcontainers/go:1-1.22-bookworm",
  "mounts": [
    {
      "source": "${localEnv:HOME}/.gitconfig",
      "target": "/home/vscode/.gitconfig",
      "type": "bind"
    },
    {
      "source": "${localEnv:HOME}/.vim",
      "target": "/home/vscode/.vim",
      "type": "bind"
    }
  ],
  "name": "Go",
  "remoteUser": "vscode"
}

参考資料