👾

Nushell向けgit-sh、git-aliases.nuを作った

2024/12/02に公開

この記事はNushell Advent Calendar 2日目の記事です。

git-aliases.nuという、NushellでのGitの使用を少し便利にするスクリプトを書いたので、Nushellをお使いの方は試してみてください。Nushellをお使いでない方はNushell - 型付きシェルの基本とコマンド定義などを参考に、Nushellと一緒に使ってみてください!

🌇背景

git statusとかgit branchとかいちいちgitと打つのが面倒なので、gstとかgbとか、そんな感じのエイリアスを作っている人は多いかと思います。私はそうした問題に対応するために、これまでgit-shというアプリケーションと次のようなGit組み込みのaliasを併用していました:

~/.gitconfig
# git config --global alias.bra branch などと書くことで設定できる
# 以下は特によく使うもの
[alias]
  amend    = commit --verbose --amend
  bra      = branch
  ci       = commit
  co       = checkout
  d        = diff
  dc       = diff --cached
  disc     = diff --ignore-space-change
  discc    = diff --ignore-space-change --cached
  # ... 以下略 ...

git-shは、Gitの多くのサブコマンドをgit抜きで入力できるようaliasを設定したり、プロンプトをGitリポジトリー向けにカスタマイズしたりしたbashです。もう何年も前からメンテされていませんが、私はこの単純かつ大胆なアプローチを気に入り、Nushellに出会って今回紹介するスクリプトを作るまでずっとgit-shを使い続けていました。WindowsでPowerShell中心で開発していたときも似たようなスクリプトを作っていたほどです。

そして、Nushell - 型付きシェルの基本とコマンド定義という記事をきっかけにNushellを使うようになったので、今回いわばNushell版git-shとも言うべきスクリプトを作りました。

igrep/git-aliases.nu: Launch Nushell with aliases for all git commands, inspired by git-sh.

これと先程のaliasと併せれば、次のように実行することでGitが楽に使えます:

> git-aliases.nu

> st
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

> bra
* main

使い方などは上記のリポジトリーのREADMEをご覧ください。単一の.nuファイルのみで動作するので、PATHの通ったディレクトリーに置いてchmod +xする[1]だけで使えます。名前から察せられるかも知れませんが、元ネタのgit-shと異なりあくまでgitのサブコマンドに対するエイリアスを提供するだけなので、プロンプトのカスタマイズは行いません。プロンプトをGit向けに書き換える機能は他が既に提供していますし、「一つのことをうまくやる」方が良いだろうと考えた結果です[2]

⚡️少し苦労した点

最後に、このスクリプトを作るにあたって少し苦労した点を共有させてください。最初にこのスクリプトを作る際、私は先程触れたPowerShell向けgit-shみたいなスクリプト、pwsh-git-aliasesと同様に、aliasを設定するスクリプトを実行時に生成した上でeval(PowerShellなので実際にはInvoke-Expression)することで実現しようと考えていました。ところが、設計思想によりNushellにはevalがないのです!

Nushell公式のドキュメントより:

An important part of Nushell's design and specifically where it differs from many dynamic languages is that Nushell converts the source you give it into something to run, and then runs the result. It doesn't have an eval feature which allows you to continue pulling in new source during runtime. This means that tasks like including files to be part of your project need to be known paths, much like includes in compiled languages like C++ or Rust.

For example, the following doesn't make sense in Nushell, and will fail to execute if run as a script:

"def abc [] { 1 + 2 }" | save output.nu
source "output.nu"
abc

The source command will grow the source that is compiled, but the save from the earlier line won't have had a chance to run. Nushell runs the whole block as if it were a single file, rather than running one line at a time. In the example, since the output.nu file is not created until after the 'compilation' step, the source command is unable to read definitions from it during parse time.

要するにNushellは、usesourceするものも含め、実行前にソースコードをチェックした上でコンパイルし、実行するという仕様なので、実行中に組み立てたソースコードをevalしたり、引用した例のようにsaveで書き込んだソースコードを直後にsourceしたりするような、動的にソースコードを生成・評価して環境を書き換える術がないのです。

これはNushellの安全性を高めたり、時には最適化したりするのに役に立つ特徴でしょう。もしpwsh-git-aliasesのようなスクリプトが悪さして、alias cp = rm -rfみたいなコードをこっそり生成してevalさせようとしても、Nushellならばそもそもそれが不可能になっています。自分で明示的にaliassource, use foo *など[3]と書かない限りcpはあくまでcpのままですし、仮にそうした悪意を持ってコマンドの定義を書き換えるスクリプトがあったとしても、sourceしたりuseしたりするファイルのコードを読めば事前に分かるようになっています。

以上の通りNushellは、evalなどによる動的に生成したソースコードの実行を設計上不可能にしています。ところが、git-aliases.nuのようにgitのサブコマンドを全て列挙[4]してそこからaliasするコードを生成し、実行するアプリケーションでは、その性質が障害となりました。何度か試行錯誤してもうまく行かず、悩んでいたその日の夜、床に就く直前に解決策が閃きました💡。

その解決策とは、Nushellの本体、nuコマンドにある--executeというオプションを使った方法です。--execute(短縮形は-e)オプションは、シェルのセッションを始める前に指定したコードを実行します。例えば次のように--interactiveオプションと組み合わせれば、--executeに渡したコードをevalのように実行した上でNushellを起動することが出来ます:

> nu --interactive --execute "alias hello = print 'hello'"
> hello

前作のpwsh-git-aliasesと異なり、必ずnuを子プロセスとして起動することになるので、終了したら--executeで定義したコードは失われてしまいますが、それで困る人はまずいないでしょう。

そんなわけで「これは、Nushellにマクロみたいな機能がないとダメなのでは?」などと悩んだりもしましたが、全くその必要なく期待したとおりの実装に出来ました。良かったらNushellと一緒に使ってみてください!👋

脚注
  1. Windowsの人は.nuファイルの関連付けを変えるなど、運用上少しやりづらいやり方を採らないといけないかも知れません。かく言う私はLinux・Mac含め別の方法を採用しています。詳しくは明日投稿する「Nushellを導入したのでこんな風にプロンプトとかをカスタマイズした」をご覧ください。 ↩︎

  2. 結局、私は既存の方法があまり好きになれそうになかったので、git-shのコードを参考に自前で作りました。詳しくはこちらも明日の記事で。 ↩︎

  3. 後、def --envを使って定義された関数の実行や、defを使いcpという名前でコマンドを定義することも該当します。まだあるかな? ↩︎

  4. 「わざわざgitのサブコマンド全部にaliasを設定しなくても、よく使うものだけ設定すればよいのでは?」と思った方もいらっしゃるかも知れません。ぶっちゃけその通りです。git-shも実際そうしているし全くもってその通りなのですが、まあ私がやりたかったということでどうかお許しください😅。 ↩︎

GitHubで編集を提案

Discussion