特定のディレクトリ配下でのみ有効になるエイリアスのようなものを実現する
これは 天久保 Advent Calendar 2022 17日目, 天久保ではない人の記事です
tl;dr
LOC_CMD_<FOOBAR>
形式の環境変数に設定したコマンド文字列を $ loc foobar
で実行できる loc
コマンドを作り, direnv と組み合わせて便利に使っています
export LOC_CMD_SAY_HELLO='echo hello'
loc say_hello # -> echo hello
loc say_hello world # -> echo hello world
背景
シェルには alias という機能があります
alias ls='ls -v --color=always --group-directories-first'
ls foobar
# -> ls -v --color=always --group-directories-first foobar
alias gc="git commit"
gc -m 'hello world'
# -> git commit -m 'hello world'
任意のコマンドやオプションに対してその短縮表記のようなものを定義する機能で, これは広く利用されているものだと思います
筆者も多数のオプションが付いたコマンドやそのパイプなど, 覚えきれないがそれなりの頻度で実行する処理をその名前に抽象化するような用途で多用しています
ここで, この手の処理には特定のコンテクスト, もっと言えば特定のディレクトリ内でしか実行されないものが多いと感じています
ケース1
Dropbox にプレーンテキストとして保存されている Mozc の辞書を Android 版 Gboard の辞書形式に変換し, さらに直接読み込めるよう zip にする
- モチベーション: 単純にコマンドが長すぎて手打ちは面倒
- 実行するディレクトリ: 当該ファイルが保存されているディレクトリ
ケース2
壁紙に使えそうな画像を Ubuntu Desktop が壁紙として認識するディレクトリにコピーする
- モチベーション: Ubuntu Desktop の要求するディレクトリ (
~/.local/share/backgrounds
) を覚えたくない - 実行するディレクトリ: 画像/写真をまとめて保存しているディレクトリの配下
これらを shell alias として定義しても良いのですが (後述する通り2つ目は alias では実現できないと思いますが, それは無視するとして), 明らかにごく一部のディレクトリ内でしか実行しないものが global に定義され常にアクセス可能になるのはなんとなく気持ち悪さがあります
より現実的なデメリットとして, この手の alias が増えすぎると補完の邪魔になってくることもあるでしょう
そこで, alias 的なものを特定のディレクトリとその配下に限って有効化する機構があれば便利そうだと考えました
Makefile
の利用検討
あらかじめ記述したコマンドをコマンドラインから呼び出して実行でき, かつそれが特定のディレクトリ内でのみ可能になるものと考えると, 素朴な解としては Makefile
/make
が思い浮かびます
実際に上記のケース1では以下のような target を記述し,
dic_to_gboard:
mkdir -p GboardDictionary && echo "# Gboard Dictionary version:1" > ...
必要なときに $ make dic_to_gboard
を実行すれば十分です
しかし, ケース2は以下のような理由で Makefile
では実現できません
- 素の
make
はMakefile
のあるディレクトリ以外では実行できないため, 画像ディレクトリの奥の方では使えなくなってしまう - 実行されるコマンドは
cp <渡されたファイル群> ~/.local/share/backgrounds
となる必要があるが,make
には第2引数をコマンド中に挿入する綺麗な方法がない (環境変数で渡すなどが必要)
これらは最近のイケてるタスクランナーなら解決されていたりもしそうですが, これだけのために1つツールを導入するのは可能なら避けたいところです (make
を使っているのは UNIX 環境なら普通はセットアップされていることが大きな理由です)
direnv の利用と自前スクリプト
結論としてはここに至りました
direnv を使えば .envrc
に記述した環境変数(など)をそのディレクトリ配下でのみ load し, 外に出た際自動的に unload することができます
これを利用し, タスクを特定の形式の環境変数から認識するタスクランナーのようなものがあれば目的は達成できるのではないかと考えました
loc[al commands] のイメージから loc
と命名し, シェルスクリプトの勉強を兼ねて bash スクリプトとして実装しました
核心部分は以下のようなものです(適当に抜き出しているため, そのままでは動かないかもしれません)
#!/usr/bin/env bash
set -euo pipefail
get_cmd_body() {
local cmd_name cmd_body
cmd_name=${1:-}
printenv | grep "^LOC_CMD_$cmd_name=" | sed "s/^LOC_CMD_$cmd_name=//" || echo ''
}
command_body=$(get_cmd_body "${1:-}")
if [[ "$command_body" =~ ^-I\{\} ]]; then
command_body_without_keyword=$(echo $command_body | sed -e 's/-I{}\s*//')
command_head=${command_body_without_keyword%\{\}*}
command_tail=${command_body_without_keyword#*\{\} }
command="$command_head $(printf '%q ' "$@") $command_tail"
elif [[ $# -eq 0 ]]; then
command="$command_body"
else
command="$command_body $(printf '%q ' "$@")"
fi
echo "$command"
eval "$command"
tl;dr の説明のような挙動に加え, コマンドが -I{}
で始まる場合, 第2(以降)引数がコマンド末尾ではなくコマンド中の {}
部分に入るようになっています
xargs の同名オプションのイメージです
これにより, 上記のケース2も解決されました
export LOC_CMD_DEPLOY_WALLPAPER='-I{} cp {} ~/.local/share/backgrounds'
loc deploy_wallpaper foo.jpg bar.png
# -> cp foo.jpg bar.png ~/.local/share/backgrounds
その他, eval
しているため make
では動かなかったりする bash の shell substitution などが図らずもそのまま動きました
また, make
においては target 名 ($ make <here>
) に Tab completion が効くことが利便性を大きく高めていると感じるため, loc
コマンドに対しても同様の補完が効くよう zsh の補完関数 を書きました
これでコマンド(やエイリアスのようなもの)の名前を暗記する必要もなくなり, 「ここ(このディレクトリ)では loc コマンドを定義していた気がするな…」というときに $ loc <Tab>
するだけで目的の処理を呼び出すことができるようになり, かなり便利に使っています
安全性について
ファイルに記述された任意のコマンドが簡単に実行できる機構ですから当然安全性は多少気になりましたが, direnv は明示的に信頼 ($ direnv allow
) した .envrc
しか load しませんから, .envrc
の中身を適切に確認する限り問題はないでしょう (確認せずに実行するなら make
だって危険ですし)
Discussion