長らくZshのプラグインマネージャとして使用してきたZinitからsheldonへと移行するため、GitHub Releasesから実行可能ファイルをダウンロード・インストールするCLIツール gh-rd を Deno で作成しました。
- これは何?
- 経緯
- Zinitをやめた理由
- 代替手段の検討
- Linuxbrew
- aqua
- まとめ
この2年ほど、LazygitにちまちまとPull Requestを投げています。 マージされたものもなんだかんだでほどほどの数になったので、それぞれについて振り返ってみます。
ちなみに私は英語が大の苦手であるため、PRに書かれている英語はGoogle翻訳とパッションによって構成されています。
ここ数年の間にいくつかの小さいZshプラグイン・CLIツールを作成しました。
頻繁に使用しているものもあればその実そうでないものもあります。本記事では現在の自作プラグイン・ツールの使用状況を報告します。
Almelは高速に動作するカスタマイズ性に優れたシェルプロンプトです。
かれこれ3年以上使用しており、自作Zshプロンプトを非同期対応した - 茅の下で紹介したように非同期描画に対応したことで(自作なので当たり前ではあるのですが)不満点を克服し、より手放し難いものになりました。
上記記事を書くにあたり参考のために人気の高い Powerlevel10k を使用したこともあったのですが、セットアップの容易さなどを鑑みても今後他のプロンプトに乗り換えることはおそらくありません。
今後の機能追加を積極的に行っていくつもりはない(なにせ現状に満足しているため)一方で、コンフィグファイルの構造などには反省点も多いので、別の名前で作り直すことはひょっとしたらあるかもしれません。
Zouchはテンプレートから新規ファイルを生成するコマンドです。
これは現在でもちょくちょく使っており、また-r
オプションを付けることで中間ディレクトリの作成も行えて便利なため、touchコマンドの代わりに使用することもしばしばです。
もっともよく生成しているファイルはおそらく .editorconfig
と .gitignore
で、これらはFZFを利用してインタラクティブに内容を生成できるようにしています。
一方で、一定のルールに従って複数のファイルを同時に出力したい (hoge.go
と hoge_test.go
, src/main/java/.../Piyo.java
と src/test/java/.../PiyoTest.java
など) という瞬間も多く、そのような場合のためにmdmgの導入を検討しています。
もっとも使用頻度が高く、もっとも生産性を向上させているZshプラグインです。
evaluate
やzabrzeのアイデアの根幹であるcontext
といった機能は上記記事公開後に(パクリ元である)zeno.zshにも逆輸入され、さらにzeno.zshの実行パフォーマンスも改善されたため記事中で紹介したいくつかのユースケースはzeno.zshで代替可能になったのですが、その後のzabrzeへの機能追加によってより発展的な展開が行えるようになりました。
追加された機能:
マッチする略語のパターンを正規表現で指定できるようになりました。
abbrevs: # cp を cp -i に, mv を mv -i に展開する - name: cp/mv abbr-pattern: ^(cp|mv)$ snippet: $abbr -i # マッチした略語は $abbr という変数でアクセスできる evaluate: true # .N を awk '{ print $N }' に展開する - name: .N abbr-pattern: ^\.\d+$ snippet: awk '{ print ${abbr/./$} }' evaluate: true
これにより、複数の略語を1つの設定で記述できるようになりました。
以前は入力の置換対象は略語のみに限られていましたが、コマンド全体を置換することが可能になりました。
abbrevs: # cd - を popd に展開する - name: cd - abbr: "-" snippet: popd action: replace-all # コマンド全体を置換する global: true context: ^cd\s+-$
また、上で紹介した正規表現による略語マッチと組み合わせることでsuffix aliasを実現できるようになりました。
abbrevs: # *.py を python3 *.py に展開する - name: python3 *.py abbr-pattern: \.py$ snippet: python3 $abbr evaluate: true
略語展開の条件を設定できるようになりました。
これにより「macOSでのみ展開する」、あるいは「特定のコマンドが存在している場合のみ展開する」略語が実現できるようになりました。
abbrevs: # macOSでのみ展開される - name: chrome abbr: chrome snippet: open -a 'Google Chrome' if: '[[ "$OSTYPE" =~ darwin ]]' # trashコマンドが存在するときのみ rm を trash に展開する - name: trash abbr: rm snippet: trash if: (( ${+commands[trash]} )) # trashコマンドが存在しないときは rm を rm -ri に展開する - name: rm abbr: rm snippet: rm -ri
qtmut はTmuxinatorの代替ツールです。
あんまり使っていません (ZLEに組み込んでいるため呼び出しはしているが、本来の用途では使っていない)。Tmuxを生で呼び出す場合に比べやや遅く感じるためRustなどを用いて再実装することを考えていますが、使用頻度は高くないので腰は重いです。
commitizen-deno はDeno製のcommitizenクライアントです。
たまに使用します。が、直接 lazygit で feat(Hoge): ....
のようなコミットメッセージを入力してしまうことの方が多いです。
qwy はpmyのクローンです。
紹介記事を書いたことはありませんが、半年ほど前に作成したツールです。
pmyから使用していない機能を削除し、設定ファイルの項目を整理することで設定間の再利用性を高めたものです。が、多くの人にとってはzeno.zshで十分なのでREADMEからして他人に使わせる気がありません。
現在運用している設定のほぼ全てが以前pmyで使用していたものを移植したものです。
ファイル名の補完に利用することが多く、1日に1回程度のあまり高くない頻度で利用しています。
よく使用しているものもあればそうでもないものもありますが、使っていない=存在している価値がない, 作る価値がない、ということではないですし、手札を増やすことは良いことなので今後もそんなに使わないものを作っていきたいですね。
自分の運用している設定はdotfilesリポジトリで確認できます。
C99以降向けのヘッダーオンリーで動作する単体テストフレームワークCUTEを作成しました。
C言語のコードに対する単体テストを書く際、ごく簡単なものであれば assert()
を使用することが多いと思います。
しかし、テスト対象が複雑であったり、テストの分量が多い場合には assert()
のみでは機能不足を感じることが今まで多々ありました。
そこで、自分用に必要十分な機能を備え、なおかつシンプルな使い心地の単体テストフレームワークを作成しました。
C89以前、およびMSVCはサポートしていません。
CUTEはヘッダーオンリーのライブラリであるため、適当なディレクトリに cute.h をダウンロードし、includeする だけで使用できます。
また、CMakeを使用している場合はFetchContent
を使用することで以下のようにプロジェクトをインポート出来ます。
include(FetchContent) FetchContent_Declare(cute GIT_REPOSITORY "https://github.com/Ryooooooga/cute.git" GIT_TAG "main" ) FetchContent_MakeAvailable(cute) target_link_library(your_project cute)
CUTEを使用した簡単なテストの記述例を以下に示します。
#include "cute.h" int factorial(int n) { if (n > 0) { return n * factorial(n - 1); } return 1; } TEST(factorial) { EXPECT(factorial(0), eq(1)); EXPECT(factorial(1), eq(1)); EXPECT_MSG(factorial(5), eq(120), "5! == 120 (actual %d)", factorial(5)); } TEST(string) { const char *s = "Hello, world!"; ASSERT(s, is_not_null); EXPECT(s, eq_str("Hello, world!")); EXPECT((s, 4), eq_str_n("Hell")); EXPECT(s, contains("world")); EXPECT(s, not(contains("nya"))); } int main(void) { RUN_TESTS() { RUN(factorial); RUN(string); return DUMP_RESULT(); } }
その他の例は example.c で確認できます。
上記の通り、基本的な条件は以下のような形式で記述します。
EXPECT(実際の値, 述語);
EXPECT_MSG(実際の値, 述語, メッセージ);
ASSERT(実際の値, 述語);
ASSERT_MSG(実際の値, 述語, メッセージ);
EXPECT()
は条件が満たされなかった場合も以降のテストが継続されるのに対して、ASSERT()
は即座にエラーの発生したテストを中断します *1。
現在のところ、値に対する述語として以下のようなものが用意されています。
is_true
: actual == true
is_false
: actual == false
is_null
: actual == NULL
is_not_null
: actual != NULL
eq(x)
: actual == x
ne(x)
: actual != x
lt(x)
: actual < x
le(x)
: actual <= x
gt(x)
: actual > x
ge(x)
: actual >= x
eq_str(s)
: actual == s
eq_str_n(s)
: actual[0..n] == s
※ このeq_str_n()
のみ EXPECT((ptr, len), eq_str_n(s))
のように記述します。contains(s)
: actual が s を含む
not(pred)
: 述語pred
を反転するまた、述語そのものはマクロや関数ではないため、以下のように同名の変数や関数が定義されていても問題なく動作します。
bool is_true = true; EXPECT(is_true, is_true); int eq = 42; EXPECT(42, eq(eq));
GROUP()
を使用することでテストコードをグルーピングすることが出来ます。
TEST(numeric) { GROUP("integer") { int n = 42; EXPECT(n, eq(42)); EXPECT(n, ne(41)); EXPECT(n, lt(43)); EXPECT(n, le(42)); EXPECT(n, gt(41)); EXPECT(n, ge(42)); } GROUP("floating point") { double f = 3.14159265; EXPECT(f, ne(0)); EXPECT(f, lt(3.15)); EXPECT(f, le(3.15)); EXPECT(f, gt(3.14)); EXPECT(f, ge(3.14)); } }
CUTEは実行されたテストケースを出力します。 また、テストケースやグループごとに実行時間の計測結果を出力するため、簡単なベンチマークとしても利用できます。
もしテストの実行中にエラーが発生した場合、その周囲のアサーションなどを出力します。 問題のない成功ケースの出力を省略することでエラー箇所の把握がしやすいようになっています。
CUTEの大半の機能はマクロを用いて実装されています。
メインの機能であるアサーションは以下のようなコードに展開されます。
ASSERT(x, eq(42)); ↓ CUTE_ASSERT(x, eq(42)); ↓ CUTE_ASSERT_I(CUTE_TESTING, x, CUTE_PRED_eq(42)); ↓ CUTE_ASSERT_II(cute_testing, x, CUTE_PRED_eq_COND, CUTE_PRED_eq_DESC, 42); ↓ CUTE_ASSERT_III(cute_testing, CUTE_PRED_eq_COND(x, 42), CUTE_PRED_eq_DESC(x, 42)); ↓ CUTE_ASSERT_III(cute_testing, (x == 42), "x == 42")); ↓ do { if (!cute_assert(cute_testing, (x == 42), "x == 42", __FILE__, __LINE__)) { return; } } while (0);
また、グルーピングは以下のように展開されます。
GROUP("group %d", 42) { ... } ↓ CUTE_GROUP("group %d", 42) { ... } ↓ CUTE_GROUP_I(CUTE_TESTING, "group %d", 42) { ... } ↓ CUTE_GROUP_I(cute_testing, "group %d", 42) { ... } ↓ for ( cute_testing_t *_cute_tmp = cute_testing, *cute_testing = CUTE_GROUP_START(_cute_tmp, "group %d", 42); cute_testing; CUTE_GROUP_FINISH(cute_testing), cute_testing = NULL ) { ... }
ヘッダーオンリーのシンプルな単体テストフレームワークを作成しました。
個人のCプロジェクトの単体テストに用いてみたところひとまず問題なさそうなので満足しています。
*1:このあたりの命名はGoogleTest に倣っています
自作のシェルプロンプト、almelを非同期対応させることで巨大なリポジトリ内での硬直を軽減し、使用感を向上させました。
almel はRust製の高速に動作するシェルプロンプトです。 現在はBash, Zsh, fishに対応しています*1。
以前の記事:
ryooooooga.hateblo.jp ryooooooga.hateblo.jp
Almelはlibgit2を用いることで高速にGitステータスを表示しています。
ですがこれまでは常に同期的にGitステータスを取得していたため、巨大なリポジトリ内では場合によって描画に〜1秒程度の時間がかかることがあり、発生する待ち時間がストレスになることがありました。
Powerlevel10kやagkozak Zsh Promptなどの既存のZshプロンプトの中には、非同期に描画することでスループットを向上させているものがあります。
これらは、Gitステータスなどの実行に時間の掛かるセグメントを除外した"仮のプロンプト"を同期的に表示し、その後Gitステータスを含む完全なプロンプトを非同期的に描画することでコマンド実行後の硬直を軽減し、利用体験を向上させています。
今回、Almelを非同期対応させることでこれらと同様に利用体験を向上させました*2。
非同期描画にはzsh-asyncを利用しているため、あらかじめなんらかの方法でzsh-asyncをインストールしておく必要があります*3。
初期化方法:
# .zshrc # zsh-asyncを読み込む source "${zsh-asyncのインストールディレクトリ}/async.zsh" # Almelを初期化する eval "$(almel init zsh --async)"
Zinitを用いた初期化方法:
# .zshrc zinit light-mode from'gh-r' as'program' for \ atload'eval "$(almel init zsh --async)"' @'Ryooooooga/almel' zinit wait lucid blockf light-mode for \ @'mafredri/zsh-async'
では、もともとGitに関する情報の取得にどの程度の時間がかかっていたのか測定してみます。
まず比較的小規模であるAlmel自身のリポジトリを用いて、Gitリポジトリに関する情報取得の有無による実行速度の差を確認します。
# Gitステータスあり $ time (for i in {1..100}; do almel prompt -s0 -j0 -d1 zsh >/dev/null; done) ( for i in {1..100}; do; almel prompt -s0 -j0 -d1 zsh > /dev/null; done; ) 1.16s user 0.71s system 83% cpu 2.248 total # 平均 22.48ms (N=100) # Gitステータスなし $ time (for i in {1..100}; do almel prompt -s0 -j0 -d1 zsh --no-git >/dev/null; done) ( for i in {1..100}; do; almel prompt -s0 -j0 -d1 zsh --no-git > /dev/null; ; 0.83s user 0.46s system 78% cpu 1.629 total # 平均 16.29ms (N=100)
ここで利用している almel prompt
コマンドはAlmelのプロンプトの実際の表示を行っているコマンドで、--no-git
オプションを渡すことでGit関連の機能を全て無効化できます。
差はわずかではありますが、当然ながら--no-git
オプションありのほうが速いことがわかります。
Profiling Rust - macOS で DTrace を使って FlameGraph を描画する - Qiita を参考にプロファイリングを行ってみると、Gitステータスの取得 (下図 右 緑矩形) に最も時間が掛かり、次いで Gitリポジトリの初期化 (下図 左 青矩形) に時間が掛かっていることがわかります。
Gitステータスの原理からして、リポジトリに含まれるファイルの数が多いほどより多くの時間が必要になります。
続いて、手元にあったリポジトリの中で最も体感の描画時間が長かった lazygit を用いて計測を行います。
# Gitステータスあり $ time (for i in {1..100}; do almel prompt -s0 -j0 -d1 zsh >/dev/null; done) ( for i in {1..100}; do; almel prompt -s0 -j0 -d1 zsh > /dev/null; done; ) 10.24s user 15.13s system 95% cpu 26.438 total # 平均 264.38ms (N=100) # Gitステータスなし $ time (for i in {1..100}; do almel prompt -s0 -j0 -d1 zsh --no-git >/dev/null; done) ( for i in {1..100}; do; almel prompt -s0 -j0 -d1 zsh --no-git > /dev/null; ; 0.47s user 0.42s system 73% cpu 1.207 total # 平均 12.07ms (N=100)
Gitステータスありの場合、260ms以上も掛かっていることがわかります。 これほどになると同期描画の硬直はストレスに感じます。
Almelのプロンプトの描画を非同期化することで利用体験を向上させました。
以前からGitステータス取得のパフォーマンスの低下は懸念点であったため、それが解消できたことは喜ばしいです。
また、almel init zsh --async
で出力されるスクリプト(下記)は非常に短く簡単であるため非同期プロンプトを実装する際の参考になるでしょう。
Denoでcommitizen互換のツールを作りました。
commitizenはGitのコミットメッセージをいい感じにするためのツールです。
commitizenのようなツールには公式である commitizen/cz-cli の他に、streamich/git-cz や Go製の lintingzhen/commitizen-go があります。
以前は cz-cli を使用していたのですが、これは Node.js 製であるため、グローバルに npm install
する必要があります。
しかし、私はグローバルに npm install
, gem install
, pip install
をあまりしたくないため、これに若干の不満がありました。
commitizen-go はNode.jsに依存しませんが、あまり開発が活発ではなさそうな上、入力にEmacsキーバインドが使用できないことから移行先としては若干心許ないように思えたため、似たようなものを自作することにしました。