シェルスクリプトのexecで実行するコマンドに共通の引数を付ける方法

## はじめに

`exec date` で `exec date -u` を実行する方法。と書いた方がすぐに伝わる気がします。

## さっさと答えを知りたい人用

```bash
#!/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 がない場合は簡単

一般的にコマンド呼び出しにデフォルトで引数を付けたい場合は以下の方法のどちらかを使います。

```sh
# alias date='date -u' # こちらでもよい
date() { command date -u "$@"; }

date # date -u が実行される
```

`alias` の方がシンプルですが、関数定義の場合は引数を動的に生成（下記参照）したりと柔軟なので私はシェル関数を使う方をおすすめします。`alias` は（バグが多くて切り捨てるべき）posh で実装されていなかったり、bash ではデフォルトで無効なので少し面倒です。

シェル関数だとこのようなことが出来るという例（ShellCheck の [SC2086](https://github.com/koalaman/shellcheck/wiki/Sc2086) 回避に使えます）

```sh
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` をシェル関数で再定義することは出来ません（再定義できたという人は、それは幻ですので最後まで読んでください）。

```sh
#!/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` とかでも）。

ちなみにこれでも動作します。

```bash
#!/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` をシェル関数で再定義出来たように見えて実は出来ていません。エラーにならないだけです。

```sh
# どちらも ok ではなく日時が出力される
mksh -c 'exec() { echo ok; }; exec date'
yash -c 'exec() { echo ok; }; exec date'
```

bash と zsh では `exec` をシェル関数で再定義できます。ただし POSIX モードの bash では再定義できません。

```sh
# どちらも 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` を使わずに実装できるのか？というと出来ます。

```sh
#!/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` などを使うので私はそこまで困らないでしょう。

## さいごに

とまあ、このような~~ひねくれた~~面白い方法で実現することができますが、シェルスクリプトに詳しい人でないと何をしているのかさっぱりで、混乱するだけだと思うので、以下のように書くのが素直でよいでしょう。

```sh
#!/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` と書きたいとか、一時的に書き換えたいが変更する箇所が多くて大変だとか言う場合には冒頭のコードを使ったテクニックが使えるでしょう。

~~過剰に~~真面目にライブラリ化すれば、このくらいまで減らせるとは思いますが、そこまでする価値はなさそうです。

```sh
#!/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](https://github.com/shellspec/shellspec) で一部のシェルが抱える問題のワークアラウンドとして内部的に使用しています。