⌨️

特定のディレクトリ配下でのみ有効になるエイリアスのようなものを実現する

2022/12/17に公開

これは 天久保 Advent Calendar 2022 17 日目, 天久保ではない人の記事です

https://adventar.org/calendars/8233

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 では実現できません

  1. 素の makeMakefile のあるディレクトリ以外では実行できないため, 画像ディレクトリの奥の方では使えなくなってしまう
  2. 実行されるコマンドは 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