はじめに
exec date
で exec date -u
を実行する方法。と書いた方がすぐに伝わる気がします。
さっさと答えを知りたい人用
# !/bin/sh
set -eu
# bash: シェルスクリプトで alias を使えるようにするために必要
[ "${BASH_VERSION:-}" ] && shopt -s expand_aliases
alias exec='exec_wrapper'
exec_wrapper() {
case ${1:-} in
date) shift; 'exec' date -u "$@" ;;
*) 'exec' "$@" ;;
esac
}
exec date # exec date -u が実行される UTC で日付が出力される
※ 複雑だと思う方は「さいごに」を参照してください。また後述の「注意点」も参照してください。上記のコードは使用した実績が十分あるわけではないので私が気づいていない問題が他にもあるかもしれません。
解説
exec がない場合は簡単
一般的にコマンド呼び出しにデフォルトで引数を付けたい場合は以下の方法のどちらかを使います。
# alias date='date -u' # こちらでもよい
date() { command date -u "$@"; }
date # date -u が実行される
alias
の方がシンプルですが、関数定義の場合は引数を動的に生成(下記参照)したりと柔軟なので私はシェル関数を使う方をおすすめします。alias
は(バグが多くて切り捨てるべき)posh で実装されていなかったり、bash ではデフォルトで無効なので少し面倒です。
シェル関数だとこのようなことが出来るという例(ShellCheck の SC2086 回避に使えます)
date() {
# 変数の値によって引数を組み立てたりすることが出来る
[ "$UTC_MODE" -eq 1 ] && set -- -u "$@"
command date "$@"
}
UTC_MODE=1
date
# SC2086 回避とは?
DATE_OPTS="-u --rfc-email"
date $DATE_OPTS # ダブルクォートで括れないので SC2086 となる
exec がある場合に動かない理由
しかし exec
経由で date
コマンドを実行する場合、これではうまくいきません。なぜなら alias
は前方一致で関数名を置き換えるという動作をするので exec date
は date
から始まっておらず置き換えることが出来ません。また exec
は実行ファイルがあるものだけを実行するので、exec
から date
関数を呼び出せません。
余談ですが、誰もやろうとしないと思いますが、環境変数 PATH
の前に別のディレクトリを追加し、そのディレクトリに偽物の date
コマンドを作成して、ごちゃちゃごちゃやれば exec date
でデフォルトの引数を追加することは可能です。私はこの方法を使って外部コマンドをモックしてテスト可能にするというような処理を書いたりしましたが、まあ面倒でした。
話を戻すとdate
をエイリアスまたは関数に出来ないのであれば、対象は exec
ということになります。ただし exec
をシェル関数で再定義することは出来ません(再定義できたという人は、それは幻ですので最後まで読んでください)。
# !/bin/sh
set -eu
exec() {
case ${1:-} in
date) shift; 'exec' date -u "$@" ;;
*) 'exec' "$@" ;;
esac
}
exec date
上記のコードが動かない理由は 2 つあります。一つは exec
が特殊ビルトインコマンドであるため関数で再定義が出来ません。これはシェルの仕様です。もう一つは exec
関数の中で exec
関数を呼び出してるので無限に再帰呼び出しを行ってしまいます。
alias による問題の回避
そこで alias
を併用します。alias
は exec
を exec_wrapper
に置き換えるので問題なく動作します。exec_wrapper
関数の中で 'exec'
のようにシングルクォートで括っているのは、エイリアス展開で exec_wrapper
に置き換えられなくするためです。一般的にはエイリアス展開されないようにするには \exec
のようにバックスラッシュを入れると書いてあることが多いのですが、シンタックスハイライターが \e
部分に色を付けやがるので私はシングルクォートを使っています。エイリアス展開を防ぐには exec
が前方一致しない状態になればなんでも良いです(ex""ec
とかでも)。
ちなみにこれでも動作します。
# !/bin/sh
set -eu
# bash で alias を使えるようにするために必要
[ "${BASH_VERSION:-}" ] && shopt -s expand_aliases
alias exec='exec_wrapper'
exec() { # 違いはここだけ。exec_wrapper ではありません。
case ${1:-} in
date) shift; 'exec' date -u "$@" ;;
*) 'exec' "$@" ;;
esac
}
exec date # exec date -u が実行される UTC で日付が出力される
alias
は前方一致でコマンド呼び出しをエイリアス展開しますが、実は**関数定義の関数名もエイリアス展開してくれます。**コード上は本来不可能な特殊ビルトインコマンドの exec
をシェル関数で再定義しているように見えますが、実際には exec_wrapper
関数を定義しているだけです。便利ですね(?)
別解(または幻を見た人へ)
さて上記のように alias
を使わないでも exec
を再定義できてしまったという幻を見た人は、bash または zsh を使っています。もしかしたら mksh か yash かもしれません。
mksh と yash の場合 exec
をシェル関数で再定義出来たように見えて実は出来ていません。エラーにならないだけです。
# どちらも ok ではなく日時が出力される
mksh -c 'exec() { echo ok; }; exec date'
yash -c 'exec() { echo ok; }; exec date'
bash と zsh では exec
をシェル関数で再定義できます。ただし POSIX モードの bash では再定義できません。
# どちらも ok が出力される
bash -c 'exec() { echo ok; }; exec date'
zsh -c 'exec() { echo ok; }; exec date'
# POSIX モードの bash では再定義できずエラーになる
bash -c 'set -o posix; exec() { echo ok; }; exec date'
では bash と zsh では alias
を使わずに実装できるのか?というと出来ます。
# !/bin/bash
# bash と zsh の場合はこれで動く
set -eu
exec() {
case ${1:-} in
date) shift; builtin exec date -u "$@" ;;
*) builtin exec "$@" ;;
esac
}
exec date
再帰呼び出しをさせないための鍵は builtin
コマンドです。その名の通りシェル関数が定義されていたとしてもビルトインコマンド(のexec
)を呼び出します。builtin
コマンドは POSIX で標準化されているものではなく bash と zsh の拡張機能です。bash または zsh だけの対応でよければこの方がシンプルでしょう。POSIX に準拠したい場合は冒頭の alias
を使ったコードとなります。
補足 builtin
コマンドは bash、zsh 以外に mksh、FreeBSD sh、OpenBSD sh でも使えます。ksh にも builtin
コマンドはあるのですが全く異なる機能です。dash、yash、NetBSD sh では使えません。
注意点
一つ注意点があります。それは exec >/dev/null
のようなコードが動かなくなるということです。このコードは現在の環境の標準出力をリダイレクトして /dev/null
に捨てるという処理ですが、これが機能しなくなります。
この場合、冒頭の alias
を使った場合はエイリアス展開されないように 'exec' >/dev/null
を使うことで回避することができます。exec
をシェル関数で再定義した場合は builtin
を使っても bash は動きませんね。zsh は動くようです。個人的には exec
を使ったリダイレクト先の変更は戻すのが面倒で { ... } >/dev/null
などを使うので私はそこまで困らないでしょう。
さいごに
とまあ、このようなひねくれた面白い方法で実現することができますが、シェルスクリプトに詳しい人でないと何をしているのかさっぱりで、混乱するだけだと思うので、以下のように書くのが素直でよいでしょう。
# !/bin/sh
set -eu
# もちろん alias を使っても良い
# [ "${BASH_VERSION:-}" ] && shopt -s expand_aliases
# alias exec_date='exec date -u'
exec_date() {
exec date -u "$@"
}
exec_date
どうしても exec date
と書きたいとか、一時的に書き換えたいが変更する箇所が多くて大変だとか言う場合には冒頭のコードを使ったテクニックが使えるでしょう。
過剰に真面目にライブラリ化すれば、このくらいまで減らせるとは思いますが、そこまでする価値はなさそうです。
# !/bin/sh
set -eu
. ./lib.sh # この中で eval とか使ったいろんなコードを書く
# alias 風インターフェースの場合
# define_exec_alias date='exec date -u'
exec_date() {
exec date -u "$@"
}
# 関数名を固定にしたりすれば以下の行はなくせる
define_exec_wrapper date 'exec_date'
exec date
alias
を使ったシェルスクリプトのメタプログラミングは結構面白いことが出来そうなのですが、なかなか実用的な例が見つかりませんね。このテクニックに似たものをシェルスクリプト用のテストフレームワーク ShellSpec で一部のシェルが抱える問題のワークアラウンドとして内部的に使用しています。