Windowsでzshを使えるようにする

WindowsでもPOSIX互換シェルは使いたくて、導入・設定が簡単なGit Bashをよく使っています。zshも使いたくなってきたので手順をまとめます。

Git BashとMSYS2

Git BashはGit for Windowsを入れると、おまけでついてくるようになっていて、デフォルトでWindows向けの統合設定がされているからすぐに使えます。Git for Windowsをインストールするだけで既にGit Bashが使えるようになっています。圧倒的に簡単で、Gitという強力なツールとともに保守してもらえる安心感もあって、安定した環境が手に入ります。

ミニマルなMSYS2環境が同梱されていて、その上で動作する bashWindowsユーザにも扱いやすい形で設定されている、というようなものでもあります。環境変数 USERPROFILE で表されるユーザディレクトリがホームディレクトリになるようマッピングされ、環境変数 PATH や各コマンドに渡すパスの解釈をいい感じにWindows形式とUNIX形式で変換してくれたりする機能はMSYS2の恩恵です。 bash の設定ファイル(自動で呼ばれるシェルスクリプト)としておなじみの .bash_profile.bashrc なんかもユーザディレクトリ直下にあるので、探しやすく編集も簡単です。

ロケール周りで混乱したくないのと、シンボリックリンク使いたいので、下記のような設定は書いていたりしますね。 LC_環境変数LC_ALL による一括設定でも基本的には大丈夫だと思いますが、一応選んで個別に指定してます。多分これらは zsh でも踏襲しておいていいです。

# Locale
export LANGUAGE=ja_JP:ja
export LANG=ja_JP.utf8
export LC_CTYPE="ja_JP.utf8"
export LC_NUMERIC="ja_JP.utf8"
export LC_TIME="ja_JP.utf8"
export LC_COLLATE="ja_JP.utf8"
export LC_MONETARY="ja_JP.utf8"
export LC_MESSAGES="ja_JP.utf8"
#export LC_ALL="ja_JP.utf8"

# MSYS2環境で ln -s によりシンボリックリンクを作成できるようにする(デフォルトではコピー)
export MSYS=winsymlinks:nativestrict

ところで、Windows標準のシェルといえばコマンドプロンプトcmd )とPowerShell 5.1( powershell )ですが、独自の文法やコマンド体系のため、UNIX環境の知識がそのまま使えないことがほとんどです。最新のPowerShellpwsh )を別途導入すれば、クロスプラットフォーム対応されてはいますが、世はPOSIX互換シェルの知識を中心に動いてきたので、結局POSIX互換シェルとの使い分けにせざるを得ないというのが実情でしょう。その最も簡単な手段がGit Bashだよ、ということでした。

zshを入れたい

私がこれまでGit Bashを使い続けてこられたように、安定した環境でポータブルな知識で開発したい需要には十分に応えてくれています。

ただ、シェルのヘビーユーザとなってくると、 bash 仕様の古さも気になってくるでしょう。私も設定面で bash のことを気にしたりと細かい場面で bash の古さを意識させられることがあります。 bash の良さはUNIX環境だったらほぼ間違いなく利用できるということで、シェルスクリプトのシバンに指定される代表格ではあります。

ただ、POSIX準拠シェルで使い勝手の良さを考えると zsh が圧倒的に有名です。補完まわりの設計が整理されて強力になっているのが特徴で、 bash の上位互換的な位置づけで利用されるものです(多少の互換性のなさはある)。macOSの標準シェルが zsh に置き換わってから数年経ちました。それぐらいの実績があります。

最近はAIエージェントを活用した開発が急速に進化したので、私も本腰を入れてCLI環境を整備するようになり、環境のポータビリティと利便性をよく考えるようになりました。Windowszsh を使えるようにするのはちょっと面倒な印象はあったのですが、それなりにシンプルな手順で導入・管理できそうだったのでトライしてみました。

そこそこ需要ある気もするのでまとめておこうかな、というのが本記事の目的です。

ちなみにPOSIX互換を気にしなくていいときは、fishやNushellといった新興勢力が最近は人気のようです。使い分けたいですね。

手順書

解説を省いた手順書を先に示します。このあと、理解のためにも丁寧目な説明をしていきますが、やらなきゃいけないことは少ないです。

  • scoop install msys2
  • MSYS2で pacman -Syu
  • MSYS2再起動して pacman -S zsh
  • nano /etc/nsswitch.conf で設定ファイルを編集して db_home: の値を windows に変更
  • ユーザ環境変数 MSYS2_PATH_TYPEinherit を指定
  • Windows Terminalにプロファイル追加
  • .zshrc で一部エスケープシーケンスを処理するよう bindkey 設定を追加

環境更新したければ改めて pacman -Syu を実行。これはzshの中からでよい。

MSYS2のインストール

scoop.sh

Windowsユーザはとりあえず scoop を使えるようにしてください。これがあるだけでクリーンな環境構築のハードルが劇的に下がります。

wizaman-tech.hatenablog.com

最近、 scoop 用の独自バケットを用意する記事も書いていましたが、これも自分のWindows環境のポータビリティを改善する取り組みです。

さて、Git Bash同様に zsh もMSYS2環境の上で動かすことになります。Git Bashと同じMSYS2環境を流用すればいいじゃないか、とはじめに考えるところですが、ちょっと内容が削られていてカスタム性を損なっています。具体的には pacman というパッケージマネージャが付属しておらず、MSYS2環境そのものの更新なんかを好き勝手にできないのです。 zsh も本来は pacman で入れるものです。Git Bashが利用するMSYS2環境に無理矢理 zsh 動作に必要な各種ファイルを配置してしまう手もありますが、安定性に懸念があるのと、やり方がまったくポータブルではないですね。

というわけで、 scoopmsys2 を別途インストールしておいて、これを zsh の土台としましょう。

scoop install msys2

scoopでインストールしたコマンド類は ~/scoop/shimsエイリアスが配置されるので、 msys2 コマンドはここに存在を確認できるようになります。

ショートカットもいくつか用意してくれていて、下記のものがありました。

  • Clang64
  • ClangArm64
  • MinGW32
  • MinGW64
  • MSYS2
  • UCRT64

なんだかよくわからないと思いますが、 msys2 をどのCランタイム環境をメインにして動かすか、といったオプションの違いであって、 msys2 内で採用されるC/C++ツールチェインに差分があると思って良さそう。まあ zsh をインストールしたいだけの文脈では忘れていいので、ショートカットは MSYS2 を選んでおけばいいです。必要になったら、そういえばここに差分があったな、と気付ければ十分でしょう。

zshのインストール

MSYS2ショートカットからMSYS2環境のターミナルが起動し、初期状態ではシェルに bash が使われます。

MSYS2初回起動時にはSkeleton Filesの生成といった初回セットアップ処理がなんか色々走ります。 .bashrc なんかのテンプレを配置してくれる感じです。

また、MSYS2はデフォルトではWindows環境と分離されるように管理されていて、 scoop でインストールした場合、 %USERPROFILE%\scoop\persist\msys2\home\%USERNAME% がホームディレクトリになるよう設定されていると思います。ここらへんの統一的な管理が scoop のよいところです。ホームディレクトリは統一したいですが、それは後ほど。

この状態で、パッケージマネージャ pacman を使用して zsh をインストールします。

まずはその前に pacman を利用してMSYS2環境そのものをアップグレードします。

pacman -Syu

更新後、MSYS2の再起動を求められることがあるので、その場合は応じます。ちなみにこのコマンドは pacman で管理している環境更新そのものなので、 zsh を最新化したいときにも使えます。

今度こそ zsh をインストール。

pacman -S zsh

これで既にMSYS2環境では zsh コマンドを実行すれば zsh が起動するようになっていますね。認識できる .zshrc などの設定ファイルがないと、初回起動扱いになって .zshrc を作成するかどうか問われたりはします。作っておいてもいいですが、次に案内するホームディレクトリの設定を待ちたいです。

ホームディレクトリの統合

MSYS2デフォルト環境では専用のホームディレクトリが用意されていて、Windowsのユーザディレクトリとは隔離されていることを説明しました。

ただ、 .bashrc とか .zshrc とか同一ホームディレクトリ下に配置して統一的に管理したいですし、いわゆる dotfilesリポジトリ管理したい都合でも環境を分けたい理由はないです。

そこで、Windows側のユーザディレクトリをMSYS2側のホームディレクトリに対応させるため、設定ファイル /etc/nsswitch.conf を編集します。このときのエディタは好みで nanovi か使ってください。

# nanoで編集する場合
nano /etc/nsswitch.conf
# Before
db_home: cygwin desc

# After
db_home: windows

これでホームディレクトリの解釈が変わります。MSYS2を再起動して pwd コマンドでも実行すればすぐ確認できると思います。

Windows環境変数の継承設定

MSYS2をそのまま起動すると実はWindows設定の環境変数PATHを引き継ぎません。引き継ぎ挙動はオプションになっていて、環境変数 MSYS2_PATH_TYPEinherit を指定した状態で msys2 を起動するのがわかりやすいです。

システムのデフォルト設定になっていてほしいので、私はWindowsのユーザ環境変数設定として MSYS2_PATH_TYPE を登録しておきました。これでどこから msys2 を呼んでも同じ条件になります。Git Bashと大体同じ状態になったと言えますね。

Windows Terminalへの登録

Windows Terminalから直接 zsh を起動するためのプロファイルを作成します。

Windows Terminalの設定で「新しいプロファイルを追加します」を選んで、プロファイルの「名前」は適当に Zsh とかわかりやすいものを入れておきます。

コマンドライン」は下記のようにしました。

"%USERPROFILE%\scoop\apps\msys2\current\msys2_shell.cmd" -msys2 -shell zsh -defterm -no-start

引数の意味はGeminiによると、以下のようになるらしい。

  • -msys2: MSYS2ランタイム環境(POSIX互換層)を有効化
  • -shell zsh: 起動するシェルをzshに指定
  • -defterm: minttyを介さず、呼び出し元のターミナルで実行
  • -no-start: 別プロセスとして分離せず、現在のセッションを維持

scoopがパスを通すのはshimsの方なんですが、 msys2.cmd の中身が下記のような感じで色々とオプションを勝手に渡しているので、これを参考にするなら current 以下に配置された msys2_shell.cmd を呼ぶべきかなと思って、上記設定はそうしました。

@rem C:\Users\username\scoop\apps\msys2\current\msys2_shell.cmd
@"C:\Users\username\scoop\apps\msys2\current\msys2_shell.cmd" -msys2 -defterm -here -no-start %*

起動コマンドがわかってしまえば、Windows Terminal以外の環境でも使えるので参考にしてください。

「開始ディレクトリ」は %USERPROFILE% が無難そうです。Windows Terminalの再起動時に、前回セッションのディレクトリを復元できなくなりますが、私が影響範囲を正しく理解できていないので、今回は見送ります。復元させたいなら OSC 7 (Operating System Command 7) という仕組みに対応して、シェルからターミナルへカレントディレクトリを通知するよう設定するべきだそうです(Gemini情報で未精査)。これで前回セッションのカレントディレクトリをWindows Terminalが認知・保持することで次回に使えるそうな。

開始時のディレクトリが毎回リセットされると移動が面倒かもしれませんが、 zoxide を使い始めるとそのあたりは気にならなくなってきます。

Visual Studio Codeへの登録(任意)

Git Bashは特別扱いで、デフォルト設定の定義の中に含まれているから、統合ターミナルから利用するのは簡単でした。

管理外のエディタを認識させるには、プロファイル定義を追加する必要がありそうです。

ユーザ設定 settings.json を開いて下記のようにしました。ユーザ名依存なのでパスは実態に合わせてください

    "terminal.integrated.profiles.windows": {
        "Zsh": {
            "path": "C:\\Users\\username\\scoop\\apps\\msys2\\current\\msys2_shell.cmd",
            "args": ["-msys2", "-shell", "zsh", "-defterm", "-no-start"],
            "env": {
                "CHERE_INVOKING": "1"
            }
        }
    },
    "terminal.integrated.defaultProfile.windows": "Zsh",

環境変数 CHERE_INVOKING は、強制的にホームディレクトリに飛ばされる挙動を抑制するものです。

Zedへの登録(任意)

zed.dev

AI機能が統合されたVS Code派生エディタが流行していますが、フルスクラッチでイチから実装したZedが軽量動作するエディタとして注目されています。

まだちゃんと使ってみていないんですが、こちらも settings.json で似たような指定をしたら動きました。

  "terminal": {
    "font_size": 10.0,
    "font_family": "UDEV Gothic NF",
    "env": {
      "CHERE_INVOKING": "1",
    },
    "shell": {
      "with_arguments": {
        "program": "C:\\Users\\username\\scoop\\apps\\msys2\\current\\msys2_shell.cmd",
        "args": ["-msys2", "-shell", "zsh", "-defterm", "-no-start"],
        "title_override": null,
      },
    },
  },

zshの設定

あとは zsh の世界なので好きなように .zprofile.zshrc を管理したり、何らかのプラグインマネージャを導入したり、補完定義をプラグインマネージャーから導入したりしてください。

Delete, Home, EndキーおよびCtrlと左右キーやHome/Endのコンビネーションキーで送信されるエスケープシーケンス処理が中途半端となり、意図通りに動かなかったので下記のように bindkey コマンドで各入力に対する操作を指示するようにはしました。これは .zshrc に書きました。

# Delete: 1文字削除
bindkey "^[[3~" delete-char

# Ctrl+←, Ctrl+→: 単語単位で移動
bindkey "^[[1;5D" backward-word
bindkey "^[[1;5C" forward-word

# Home, Ctrl+Home: 行頭へ移動
bindkey "^[[H" beginning-of-line
bindkey "^[[1;5H" beginning-of-line

# End, Ctrl+End: 行末へ移動
bindkey "^[[F" end-of-line
bindkey "^[[1;5F" end-of-line

antidote.sh

sheldon.cli.rs

プラグインマネージャーはOh My Zshが最もよく知られていると思うんですが、今ならRust製のAntidoteかsheldonになるんでしょうか。詳しくないので私もまだ検討中。

starship.rs

プロンプトを変えたい人はStarshipがおすすめです。これも scoop install starship での導入がスッキリします(通常のインストールだと Program Files 以下に置かれてスペースが邪魔になることがある)。

github.com

StarshipNerd Font対応フォント利用を前提とした表示をするので、私は UDEV Gothic NF を使っています。 Hackgen と同じ作者によるものです。

とりあえず動かせるようにしたばかりなので、暫く使ってみて気づいたことがあったら追記するかも。

個人用scoopバケットを作った

dotfilesやパッケージマネージャを整理したりして、開発環境のポータビリティ向上を目指しているこの頃です。

github.com

環境整備の過程でscoopバケット作ってみました。引っかかったこととかあるのでその話をしようかなと。

このバケットは使ってもらっていいですが、あくまで私の個人用として割り切って気楽に運用していくつもりであることはご容赦ください。

本記事の執筆時点で、下記のツールを定義しています。

nkf は本家 nurse/nkf がビルド済みバイナリを公開していない(配布サイトが消滅して入手困難になった)のでforkされた改良版を利用しています。ありがたい。

Scoopとは

scoop.sh

scoopはWindows専用のパッケージマネージャです。ターミナルよく使うWindowsユーザにはデファクトスタンダード。管理がきれいですし、地味にWindowsでビルド済みバイナリの入手・配置が面倒な make コマンドなどの有名なツールもサクッと入れられて重宝します。wingetだと物足りないところを補ってくれているので、GUIツールもありますが、特にCLIツールの導入に関してはscoopは優先的に使いたいですね。

~/scoop/ 内にscoopが管理するファイルが集約され、各アプリはバージョン別にわけて管理されるのですが、 ~/scoop/shimsシンボリックリンクを用意してくれるので、ここにパスが通っていることですぐに利用できるようになっていて、アプリ別にパスを追加する煩わしさがありません。

バケットとは

scoopで扱えるアプリ情報(アプリマニフェスト)を管理している情報源をバケットbucket)と呼びます。いわゆるパッケージレジストリです。バケットの実体は単純にGitリポジトリであり、ツール名をファイル名にしたアプリマニフェストJSONが並んでるだけです。

バケットは複数利用することができて、デフォルトでは公式の main だけ使える状態になっています。バケットリポジトリURLに管理用の名前を与える管理となっていて、公式バケットなど一部のバケットは公式に名前つき管理がされて既知のバケット(known buckets)の扱いなので、URLを知らずに利用できるようになっています。

github.com

known buckets はscoop本体リポジトリbuckets.json として管理されています。 extras, versions, java あたりが主に追加利用することになり得ると思います。

# extras バケットの追加
scoop bucket add extras

# 追加済みバケット一覧
scoop bucket list

インストールするツール名が衝突していなければ、どこのバケット由来なのか指定は省略できますが、指定バケットからインストールしたいときは bucket/app のようにスラッシュ区切りでバケット名を先に記述することで明示できます。

# AutoHotKey のインストール(バケット名省略)
scoop install autohotkey

# AutoHotKey のインストール(バケット名指定)
scoop install extras/autohotkey

known buckets でないバケットを追加したいときは管理名とリポジトリURLを与えます。名前はユーザが好きに決められます。

scoop bucket add wizaman https://github.com/wizaman/scoop-bucket

私が用意したバケットなら上記コマンドで利用できます。指定した名前はローカル管理なので、別の名前にすることもできます。

こういう仕組みなので、適切にアプリマニフェストを配置したバケットリポジトリを用意すれば誰でも公開して利用できるようになります。scoopが内部的にgitコマンド使ってくれるみたいなので、gitコマンドで取得できるよう認証情報がセットアップされている状態であれば、プライベートリポジトリでも大丈夫だったはず。

バケットをつくる

github.com

scoop自体のドキュメントは本体リポジトリWikiページにあります。

github.com github.com

特にバケットとアプリマニフェストを必要に応じて参照すればいいと思います。まあ私も真面目に読んでないですが。

github.com

バケットのドキュメントには、独自にバケット作りたければ ScoopInstaller/BucketTemplate というテンプレートリポジトリを用意してあるから、これを使うとGitHub Actionsによるテストや自動アップデート(アプリマニフェスト内のバージョン情報更新)がはじめから揃っていておすすめだよ、という案内があります。素直に従いました。ライセンスはUnlicenseが設定されていますね。テンプレートリポジトリのREADMEに必要な作業が一通り書いてあるので、こちらは目を通しておきたいですね。

bin/ 以下にはPowerShellスクリプトとしてユーティリティが揃ってます。 bin/auto-pr.ps1 は使わなくてもいいですが、 <username>/<bucketname>:main と記述されているので、リポジトリやブランチの指定が正しくなるよう置き換えてね、ということになっています。

実際に使うユーティリティは bin/test.ps1 だけで最低限十分だと思います。

アプリマニフェストをつくる

基本的には bucket/ ディレクトリ以下に、 アプリ名.json となるアプリマニフェストを配置するだけでよくて、アプリマニフェストのテンプレートが app-name.json.template として用意されています。

とはいえ、ドキュメントとにらめっこしながら真面目に全部吟味するのも面倒だったので、私はGemini CLIに任せました。だから、ドキュメントやテンプレを細部まで確認したわけではないのです。

github.com

例えば、Android開発でお世話になる bundletool というGoogle製のJARツールがあります。Windows環境だとReleasesからJARファイルをダウンロードしてどこかに置いて、 java -jar bundletool.jar のようにして java コマンドにJARファイルのパスを与えて起動することになるんですが、かなり面倒です。これを .bashrc などでエイリアス定義して bundletool コマンドとして利用できるようにしたりするわけです。複雑ではないけど、管理されてないゆえに丹精込めた温かみのある手作業です。ところで、macだったら brew install bundletoolbundletool コマンドとしてすぐ使えるようになります。ぐぬぬ・・・。

scoopはどうやらJARファイルに対して、 java -jar に渡して実行してくれるスクリプトを配置してくれるようで、単純に bundletool コマンドとして使えるようになります。素晴らしい。公式バケットで配布されているJARツールだと plantuml なんかがこのパターンですね。 bundletool も入れてくれ。勝手にPR出していいなら気が向いたときにやっておこうかな。コントリビューションの案内見てないや。

もちろんJARツールの実行には別途 java の導入が必要です。本家を選ぶなら openjdk を入れればいいですが、Oracle信用してないので各ベンダーが用意したバイナリがあります。特にこだわりがなければ一般的なのは temurin-jdk になるので、これを私は使ってます。

# java バケットを追加(ここで各種JRE/JDKが管理されている)
scoop bucket add java

# temurin-jdkをインストール
scoop install temurin-jdk

ベンダー別にJDKがあるのがややこしいので、ファイルの衝突を避けるためか ~/scoop/shims 直下にはシンボリックリンクが置かれる設定になってません。現在導入しているバージョンのシンボリックリンクcurrent として ~/scoop/apps/temurin-jdk/current/bin/ にパスが追加されていました。初回はパス追加を伴うのでシェルを再起動すれば認識するはずです。

さて、アプリマニフェストを作る話に戻ります。Gemini CLIには下記のような指示を与えました。

@bucket/app-name.json.template をテンプレートにして https://github.com/google/bundletool を追加して

これで下記の作業をやってくれました。

  • リポジトリのdescriptionを取得して認識
  • リポジトリのライセンスを取得して認識
  • Releasesを見に行ってダウンロードするファイルを把握
  • 最新リリースをダウンロードしてハッシュ値を取得
    • .zipファイルなどのアーカイブであれば中身を展開して実行ファイルを探しに行く
  • 集めた情報を元にアプリマニフェストをいい感じに生成

ダウンロードファイルのURLが https://github.com/google/bundletool/releases/download/1.18.3/bundletool-all-1.18.3.jar#/bundletool.jar となっていて、末尾に #/bundletool.jar と記載されているのは何なんだ?と思ったんですが、そのままダウンロードすると bundletool-all-1.18.3.jar となるところを bundletool.jar にリネームして保存する、という指示のようです。なるほど。

マジでドキュメント読まなくていいな。

アプリマニフェストの内容を見てもらうとわかるんですが、ひとつのバージョン情報しか提供しません。CI用の設定項目もあり checkver 項目で最新バージョンを探すのに使える情報を記述して、 autoupdate 項目にバージョンを埋め込んだダウンロードURLのテンプレを記述します。これで最新のバージョンとダウンロードURLを特定できるので、 versionurl を書き換えてアプリマニフェストを最新にする、というワークフローが成り立ちます。GitHub Actionsワークフローファイルはテンプレートリポジトリから既に用意されているので、勝手に動きます(ただし、テンプレートリポジトリから作成しているので一度は手動実行しておかないとcron実行対象になりません)。

更新が自動化されているとして、アプリマニフェストでは最新版だけを提供していることになりますね。バージョン履歴を持っていないことは知っておいていいと思います。

github.com

versions バケットで、各バージョンを固定したアプリマニフェストを公開されています。バージョン名とセットになったアプリ名を指定することで、固定バージョンでインストールしてもらう、という方針のようです。まあ、個人でここまで保守する必要はないかな・・・。

私のバケットでは、もうひとつ nkf のfork版も対応しています。こちらは最新版のタグ v2.1.5.1-2B に対して nkf-2.1.5.1.zip が配布ファイルです。本家 v2.1.5.1 修正版としてタグにサフィックス追加されてるのは多分大丈夫なんですが、配布ファイルもそれに合わせた命名になってないのは困ります。バージョンタグとファイル名のルールが一致しないから、バージョン埋め込みが前提の autoupdate で表現できません。対応不可能なので、これに関しては autoupdate 項目は削除しました。どうせもう更新されないだろうし、まあええじゃろ。

テストする

これが意外と苦労した。

bin/test.ps1 スクリプトリポジトリに問題ないか一通りのテストを実行してもらえます。これがすべてパスしていれば多分保守できてます。

GitHub Actionsとしては、 .github/workflows/ci.yml によってPR作成時や特定ブランチ( mainmaster )へのpush時にテストが実行されるようになっています。

テストスクリプトの先頭にいくつか実行要件チェックが記述されています。これらを満たせばローカル実行できそうです。

#Requires -Version 5.1
#Requires -Modules @{ ModuleName = 'BuildHelpers'; ModuleVersion = '2.0.1' }
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.2.0' }

PowerShell 5.1 はWindows標準で組み込まれて利用できるバージョンですね。特に気にする必要はないでしょう。

大事なのは、 BuildHelpersPester というPowerShellモジュールに依存しているということです。

Windows標準PowerShellは最新バージョンではなくて、起動すると多分最新版を使いたければ別途導入するよう案内メッセージが出ると思います。Windowsユーザみんなが使える以外に旧バージョンにメリットはないので、最新版を入れています。最新版は 7.x 系バージョンでありコマンド名は標準 powershell と区別して pwsh が使われます。 pwsh のほうが新しいと思ってもらっていいです。

どうやらモジュールの管理は powershellpwsh では分離されているようで、モジュールのインストールと利用をするなら新旧どちらを利用しているか理解しておく必要があります。

今回は pwsh を使います。 pwsh を起動した状態で、下記コマンドで依存モジュールをインストールしておきます。

Install-Module -Name BuildHelpers
Install-Module -Name Pester -Force

これでテストスクリプトを実行できる準備が整いました。

ただ、PowerShellスクリプトに対してセキュリティソフトが過敏で、私が実行しようとすると動的に生成される何かが隔離されたりして散々な目に遭いました。

そこで、下記をセキュリティソフト側で除外設定にしたら、うまくいくようになりました。

  • %USERPROFILE%\scoop\apps\scoop\current\test
    • scoopテスト中に利用する領域
  • %USERPROFILE%\.gemini\tmp
    • Gemini CLIが作業用に使う一時ファイル保存場所
    • Gemini CLIスクリプトを生成して実行しようとするときに使われるので、ここがブロックされると困る

これでようやくテストスクリプトが使えるようになりました。私はGit Bashを普段使っているので、下記コマンドで呼び出してました。

pwsh -File bin/test.ps1

また、Gemini CLIは通常、Windowsでは内部シェルに powershell を使います。えぇ、 pwsh じゃないんです。 pwsh 使えるときは勝手にそっち使ってほしいんですが。 powershellpwsh でモジュール管理が別になっているということを説明しましたが、私は pwsh 側にインストールしたので、Gemini CLIの内部シェル powershell.\bin\test.ps1 をそのまま実行を試みると当然モジュールが見つからずに失敗しますね。

Gemini CLIの内部シェルを pwsh にするには環境変数 COMSPECpwsh を指定するという方法があります。Gemini CLIの内部処理でチェックして処理分岐がされているのです。ただ、 COMSPEC の用途・影響範囲を考えるとバッチファイル解釈に副作用があるので、無批判に設定していいものではないです。一時的に割り当てるなら例えばbashなどのシェルであれば、

COMSPEC=pwsh gemini

のようにして、そのときだけ適用される環境変数を与えた状態で gemini を呼び出すことで、内部シェルを pwsh にすることができます。うーん、めんどくさい。

なので、私はGemini CLIpwsh で実行するよう指示を加えました。内部シェル powershell から下記コマンドを呼んでもらう、ということです。

pwsh -ExecutionPolicy Bypass -File .\bin\test.ps1

これなら環境変数の小細工なしにGemini CLIでも実行できます。テストまで含めて全部作業をお願いできるようになりました。

問題なくテストにパスすると嬉しいですが、実際は失敗しがちなので、Gemini CLIにはエラー内容から判断して修正までお任せします。

このテストスクリプト、ちょっと癖があって、 bucket/*.json だけをチェックするようなフィルタがかかっていなくて、ほぼすべてのファイルをフォーマットチェックの対象にするという厄介な挙動を示します。

  • .git は無視してくれるがハードコーディングなので .gitignore に従って無視してくれない
  • 原則として全ファイルが対象なので、ダウンロードしたバイナリを一旦置いてたりすると絶対に失敗する
  • BOMなしでないといけない
  • 改行コードCRLFでないといけない
  • ファイル末尾に改行がないといけない

全ファイルが対象ということは、もちろん README.md だって対象だし、AIエージェントのために任意で追加する GEMINI.mdAGENTS.md だって対象です。

なんというか、歴史を感じる趣深い実装ですね・・・。

可能ならテストスクリプトを直したいところですが、ローカルにインストールしたscoopに内蔵されているテストスクリプトも利用するという、やや複雑な構成になっていて、問題があるのはそっちなので、残念ながらバケットリポジトリ側ではロジック修正ができません。

改行コードのCRLF統一ルールは .gitattributes に記述されています。

# Since Scoop is a Windows-only tool, we can safely use CRLF line endings for all text files.
# If Git decides that the content is text, its line endings will be normalized to CRLF in the working tree on checkout.
# In the Git index/repository the files will always be stored with LF line endings. This is fine.
* text=auto eol=crlf

丁寧にコメントで説明されているように、リモートリポジトリはLFで管理し、チェックアウト時に作業コピーがCRLFになります。

Geminiは基本的に改行コードLFで出力するので、CRLFに置換してくれ、とか後処理の指示を付け加えることになりました。

github.com

ということで、いくつかの引っ掛かりポイントがあって、毎回Gemini CLIが失敗するのも手間なので、 GEMINI.md に指示をまとめています。

そして、私はたまたま別の地雷も踏んでいました。

github.com

最近、gitリポジトリと互換性のあるモダンツール jj (Jujutsu) が注目されているので、少しずつ使い始めています。つまり git コマンドを使わなかったんですね。

現状で jj はすべての git 仕様に対応しているわけではなくて、 .gitattributes を参照して改行コードを変換する機能がないようです。作業コピーはすべてLFでした。わからんって。

jj では対応できないことが明らかになったので、LFでcloneされた作業コピーもすべて捨てた方が良いし、 git でcloneし直して事なきを得ました。不服。

動作確認

テストはあくまでテスト。一度はローカルでインストールを試してみて動作確認してからリモートにpushしたいですよね。

scoopはアプリマニフェストのパスを与えることでもインストールできます。この場合、アップデートには対応しません。

# インストール
scoop install .\bucket\bundletool.json

# アンインストール
scoop uninstall bundletool

Windows形式のパスしか受け付けないみたいで、Git Bash(MSYS2環境) では相対パスで通すのが難しかったので、実際には次のようにしていました。

scoop install "C:\development\scoop-bucket\bucket\bundletool.json"

これでインストールが通って、ちゃんと bundletool コマンドとして認識されるようになりました。おぉ~。

ローカルインストールが動作確認できたので、アンインストールしてリポジトリをpushします。これでバケットで公開できました。GitHub Actionsのテストも通っています(ローカルで通していればまず通るはず)。

あとは独自バケットからインストールして動作確認しておしまい。

# バケット追加
scoop bucket add wizaman https://github.com/wizaman/scoop-bucket

# 明示的なバケット更新
# searchやinstallではタイムスタンプを見て一定時間はリポジトリ取得をスキップするので強制取得させる手段
scoop update

# 検索
scoop search bundletool

# インストール(バケット名省略)
scoop install bundletool

# インストール(バケット名指定)
scoop install wizaman/bundletool

成果物がシンプルな割には苦戦させられましたね・・・。

土台は固まったので、導入めんどくさいなぁと思ったものは今後追加していくかもしれません。

Alt空打ちで日本語入力を切り替えるツールをAutoHotKey v2で実装

github.com

こんなん作りました。作った経緯とか説明します。

IME切り替えのめんどくささ

日本語入力(IME)のオンオフを切り替えるのめんどくさいですよね。

めんどくさい理由は2つで、

  • 全角半角キーが遠くてアクセスが悪い
  • 1個の物理キーでトグル方式だと、日本語入力の有効状態を意識して適切に切り替えられないと最小手数にならないが現実的ではない

こんなところです。

トグル方式をやめたい

JISキーボードでは、スペースキーの左右に「無変換」「変換」「カタカナひらがなローマ字」という使い所も使い方もよくわからない謎キーに場所を取られています。しかし、Mac向けJISキーボードではアレンジされて「英数」「かな」キーが配置され、それぞれIMEの無効/有効に対応しています。トグル方式の煩わしさもなく、親指ですぐに押せる位置にあるのが使い勝手が良いため、Windowsでも同様の使い方ができるよう無変換キーでIMEオフ、変換キーでIMEオンに設定する人は多いです。自分がそうです。Google 日本語入力の設定で実現できるので誰でも簡単にできます。

USキーボードでは、Alt+`(バッククォート)が全角半角キーの代わりになって、IMEのオンオフを切り替えるトグル方式となります。スペースキーはものすごく横に長くなって、JISキーボードで存在していた無変換キーや変換キーのような存在はなく、ぶっちゃけボタン数が足りません。前述の英数/かなキーによる明示的で素早い切り替えを知っていると、コンビネーションキーで誤魔化すのは利便性に劣っていて抵抗があります。

そういうわけで、英数/かなキーによる切り替えに似た方式を実現したい需要は強く、スペースキーの左右に隣接する2個のAltキーを単体で空打ちしたときにIMEオンオフ処理を走らせよう、という方法がそれなりに市民権を得たと思います。Windowsユーザは alt-ime-ahk というAutoHotKeyスクリプトが有名です。MacユーザはKarabiner-Elementsで実現するそうですが割愛。

alt-ime-ahkが古い

www.karakaram.com

github.com

こちらが有名な alt-ime-ahk になります。

実装内容としては、

  • Altダウンイベントはそのまま流すことで最速でAltコンビネーションを入力できることを許容
  • Alt空打ち時にメニューバーへのフォーカス機能が発火しないように、アップ時に空打ち判定(直前のキーがAlt単体ダウン)のときはIME切り替え処理を走らせて、Altキーのアップイベントを握りつぶす

といった感じです。

ところで、近年のキーボードはVIAによるカスタマイズができるモデルが登場していて、私の使っている NuPhy Air75 V3 でも NuPhyIO で柔軟なカスタムができ、キーボード自体に単押し・長押し時に入力されるキー割当を覚えさせることができます。これを利用すれば空打ちだけに特殊な操作(F13とか)を割り当てるのは簡単では?と思ったんですが、Altなどの修飾キーはラグなしで最速入力してもらわないとコンビネーション時に間に合わなくなることが多発することがわかって、不自由なく使えません。なので、Altダウンイベントを最速で流すという挙動はかなり大事なポイントです。

さて、2022年にはAutoHotKey v2がリリースされて、色々と見直されてv1との互換性のない仕様になりました。上記スクリプトはv1実装なので、今そのまま使うのはちょっと怪しい部分があります。 問題なく使えるならそれでいいんですが、この記事を書いているということは、そうはいかなかったということです。

git-fork.com

例えばGitクライアントアプリであるForkで、コミットメッセージなどのテキストフィールドをフォーカスしている状態で、Alt空打ちの度にメニューバーにフォーカスが移動して困ってしまいました。一部アプリではうまくいかないというのがWindowsの深淵を覗いた気分です。

Geminiに相談したところ、

alt-ime-ahk を使用中に Fork などの特定のアプリケーションでメニューバーにフォーカスが奪われてしまう現象は、Windows の「Altキー単押しでメニューをアクティブにする」という標準挙動と、AutoHotkey のキーイベント送出のタイミングが干渉することで発生します。

特に Fork のような WPF 系(またはそれに近い UI フレームワーク)のアプリでは、このアクセラレータの判定がシビアなことがあります。

このような説明を受けました。これが正しいかは知りませんが、Windowsでデスクトップアプリを動かすフレームワークは色々あるので、その違いだと思えば納得はできます。

AutoHotKey v2対応スクリプトを書き直す

Geminiは原因説明と同時に、AutoHotKey v2向けの最小実装を提示してくれました。これをベースに使いながら気付いた問題を解決できるよう調整していったのが、冒頭で紹介した alt-ime-mapper になります。

従来の alt-ime-ahk では、IME制御に IME.ahk という複雑なスクリプトを利用していて、これをAutoHotKey v2に移植した人も見かけたのですが、それに頼らず、よりシンプルな方法で実装できました。 alt-ime-ahk をforkしてv2対応させたものも存在は確認していますが、移植実装自体が遠回りでv2に最適な実装ではないのだろうな、と思ったので試してません。

実装がスッキリしていれば保守も簡単そうでいいですね。

大まかに下記3系統のアプリで、左右Alt空打ちが意図通りに動作するか確認・調整しました。

  • ブラウザなどの通常アプリ(?)
  • ForkなどのWPFアプリ
  • メモ帳などのUWPアプリ

もともと alt-ime-ahk で困っていたWPFアプリの問題は一発で解決して、細かな調整をして完成!と思ったのですが、なんとなく起動したメモ帳でまったく効果がなくてナンデ!?となったものですが、これもGeminiに聞いたら、

Windows 11 以降の「メモ帳」は、従来のシンプルな Win32 アプリから、UWP (Modern App) ベースの新しい設計に変わりました。

そのため、単に WinExist("A") で取得した「トップレベルウィンドウのハンドル」に対して ImmGetDefaultIMEWnd を実行しても、内部のテキストエディタ部分(フォーカスされているコントロール)と正しく通信できないケースがあります。

これを解決するには、「現在フォーカスが当たっているコントロール」のハンドルを正確に取得するロジックを追加するのが正解です。

ということで、これまでアクティブウィンドウのハンドルを取得する WinExist("A") だけで制御してきたものがうまくいかないことがわかりました。 ControlGetFocus("A") でフォーカスを持つUIコントロールを取得し、ここから ImmGetDefaultIMEWnd を取得できるなら優先採用する、というロジックに直したら、各アプリで想定通りの挙動になりました。めんどくさいですね。

ちなみに、 alt-ime-ahk では主要なキーをパススルーさせるおまじない的な定義が大量にありましたが、AutoHotKey v2では InstallKeybdHook() だけでより安定した動作ができるようになったみたいです。

また、Altダウン時にWindows未定義の仮想キーコードも同時に送信することで、空打ちではないように見せかけてメニューバーへのフォーカスが発火しないようにしているんですが、今回は仮想キーコードを vkE8 としたのも alt-ime-ahk との違いです。この目的ではこちらのほうが主流っぽいことをGeminiが言ってたので従っただけです。

(2026-01-17追記)やっぱりWPFアプリでは仮想キー方式ではAlt空打ちメニューフォーカスを騙しきれていなかったので、空打ち判定時にShiftキーを割り込ませることで空打ちではないと認識させるというやや強引な手法に落ち着きました。

一晩で解決しましたが、自力でAutoHotKey v2や各種Windowsアプリの仕様調査をしていたら、ここまで辿り着くのにかなり時間がかかっていたと思うのでAI様々です。

目に付く問題は解決したつもりですが、他にも不都合があったら教えてくれると嬉しいです。

技術ブログの動機

技術ブログやろうと思って場所だけつくって放置してました。最初に意図は示しておきたいと思ったので、そんな話をします。一度書いておけば、それがスナップショットになってあとで「このときはこう考えてたんだな」と振り返れていいんですよ。

書くことに集中したい

ブログはマイペースに文章のアウトプットと向き合えるので好きです。うっかり時間をかけてしまうため、サボってましたが本質的には執筆は好きです。

記事を書くという目的において、書くことにだけ集中できたほうがいいのは明らかです。

学生時代、サーバにも触れてみようと思えばまずXAMPP環境でしたので、レンタルサーバApache上でWordPress構築してある程度カスタムしてたんですが、本体やプラグインの更新で簡単に壊れてしまいます。閲覧不能なまま放置するわけにもいかず、差し込みで1日つぶして原因調査・修正することを何度か経験するうちに、こんなに不毛なことってある?と冷静になります。それまでに培ってきた不信から、5.0メジャーアップデートのタイミングでついに手を出す気が失せました。就職して、まとまった時間が確保しづらくなったこととも釣り合ってません。

DB依存であるなどローカルで完全に再現して、動作保証できているものを本番反映するっていう作業を簡単にできるわけでもないし、ロールバックだって簡単じゃないし、PHPという言語にだって付き合いたくない。おまけに肝心の成果物が激重で閲覧するのにも数秒待たされるわけです。セキュリティ面でも不安しかないですね。

自分が管理したいのはコンテンツであって、執筆環境そのもののカスタム・保守にコストをかけたいわけではないため、はてなブログがちょうどいいかな、と思い至ることになります。

そして、普段使いのブログを作ったのが4年前(そんな前?)。この時点で技術ブログは分けるべきだと考えていて、少し遅れてこの技術ブログも場所だけ作っていました。書く気があるからブログ作ってます。本当だよ?

技術ブログの分離

技術ネタは多岐に渡るので、一緒くたに管理しないほうがスッキリすると思いました。

また、ブログであるということは、同時に個人であることを閲覧者に意識させやすいと考えていて、「この人は他に何を書いてるのかな?」と気になったときに、導線がまとまっていたほうが嬉しいと思います。技術ネタの鮮度・信用度は、個人の力量に行き着くと考えています。どういうことに興味を持って、どんなスキルセットを持っていて、いつ・どんな取り組みをしているのか。総合的に気にかけて個人を評価するのなら、他に積み上げてきたものから思考プロセスまでなんとなく目を通すことになります。そこで個性を打ち出すことでブログらしい価値を発揮できるのだと思います。

このような技術ネタの性質を考えると、雑多な趣味ブログとは棲み分けたほうが、自分にとっても読者にとってもいいだろう、と思って分けています。全然更新してないから説得力ないですが、書く気はあるからこうしてるんですよ。本当だよ?

技術ナレッジコミュニティ

技術ネタを書くならば、それに特化したQiitaやZennといったプラットフォームも選択肢に挙がります。それはわかっていてあえてブログを選んでいるのにも理由があります。

まず、Qiitaというプラットフォームを信用していません。もうだいぶ古い話です。質の悪い記事によるGoogle検索汚染で印象が悪かったのもあるのですが、度々炎上してます。決定的だったのは、各ユーザの興味傾向を勝手に分析して公開したプライバシー問題で、数ある退会ラッシュのなかでも大規模だったと思います。食べログにブックマークを勝手に公開されて居住地域が推測できてしまった問題よりかわいいものですが、ユーザの同意なく勝手に個人に紐づく情報を公開する点で同質であり、正常な思考を持ち合わせているとは思えません。私の中では、アカウントを所持すること自体がリスクという評価になりましたね。

その点、ZennはQiitaとは違って明確な哲学を持って設立・設計・運営がなされていて好感が持てます。選択肢としてはアリです。それでもやっぱりプラットフォームが育てば、Qiitaに溢れたような質の悪い人はどうしても集まってきます。悪貨が良貨を駆逐するのも時間の問題でして、悪名高い人が移っているだけでも同じ空気を吸いたくないですね。それに、コミュニティの過剰反応で集団リンチみたいになってるのを見かけたときも、大人げないなぁと思っています。結局のところ、人が悪いんですよ。

そもそも、私はこれらプラットフォーム内で技術を検索しようと思いません。信用できそうな個人を追ったほうが情報の質も量もよいことを知っています。具体的なトピックを調べたいときだって、そのままGoogle検索にかけるか、Stack Overflowを見るか、各コミュニティのフォーラムやIssueを見に行くほうが答えに辿り着けます。もし興味のある強力な個人がいたとしても、それだけ魅力ある人ならプラットフォームに価値を依存させておらず、好きなように振る舞っているでしょう。

分野の偏りもあります。こういうものはWeb系の人が好んで使う傾向にありますが、私の職業はゲームクライアントエンジニアです。自分のメイン分野の情報を眺めてみるとまあ悲惨なものですよ。とても作品をリリースできるとは思えない情報で溢れていますね。ほとんどみんなリリース経験ないんじゃないでしょうか。ここに私が参加する理由はないですね。Gitで管理・デプロイしたいかっていうと、文章を書くのにcommit/pushも煩わしくない?って思っちゃうので、Markdownで書ければいいです。

総合的に考えて、ひとつのナレッジコミュニティに帰属する必要性を感じていません。余計なことは考えずにのびのびと自分が書きたいと思うものをアウトプットするのなら、枯れた方法がふさわしいです。価値があれば勝手に人は来ます。価値がそんなになくても自分が書きたければそれでいいのです。

まー、ブログでじゅうぶんだ。

コンテンツの保守

今後、はてなブログ中心でいい、という結論ではあるのですが、過去に書いた記事を捨てたくもないワガママさんです。

価値のある記事は本当に限られるんですが、それでも私が時間をかけて精度を高めた結果、たくさん参照されて、今でも通用するか歴史的価値を持つものもあるっちゃあるんです。技術ネタに限らず、ですが。

レンタルサーバはやめたいけど、コンテンツの保守はしたい。さらに言えば、参照されているからにはリンク切れで辿れない、なんてことも許容しがたい。もし場所を移すなら移動先を網羅してリダイレクトさせてやる、と考えてました。

環境は一度ぶっ壊したほうが簡単なんですが、どうにも私は一度リリースしたものについて責任持って保守する意思が強いです。10年以上前の記事を問題なく閲覧可能な状態で残します。自分で言うのもなんですが、これってなかなかすごいことです。10年前のWebサイト、記事、どれだけ残ってますか?

放置しても大丈夫な、ごく一部の長生きなサービスに、たまたま乗っかっているかどうかが寿命をほぼ決するんじゃないでしょうか。独自ドメインで自分のコンテンツをずっと抱えてるのはおそらく狂気に近い。でも、私は消えたくない、消したくない。消えるのは簡単だからこそ明確な意思が必要になる。

たとえ長らく放置していたとしても、残すために少しお金を払い続けていたのは惰性じゃなくて私の意思なんですよ。

そして、2024年10月16日、ついに重い腰を上げて旧ブログの移行作業を完了させました。はてなブログ開設当初は、全部はてなブログに移してリダイレクトさせるつもりでした。これには解決すべき課題が多くて保守的に考えると難しかったのですが、時間ができて静的サイトジェレネータを検証していたおまけでブログを静的サイト化する算段が立てられたので、思い立ったが吉日。フロントエンド門外漢ですが1ヶ月かけずにきれいに終わりました。元のURLを維持したまま置き換えているので望みが叶いました。レスポンスも素晴らしくポチポチ触りたくなりますね。ついでに、ちょうどいい技術ネタができたとも言えます。

ということで、最後に予告。Astroでブログを移行した話を書きます。

長々と書きましたが、こうした個性的なエピソードを安心して書けるのも、ブログだからじゃないですか?それって事例ベースで示唆があって、一定の面白さはあるんじゃないのかな?