LoginSignup
4
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-02-10

はじめに

exec dateexec 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 datedate から始まっておらず置き換えることが出来ません。また 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 を併用します。aliasexecexec_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 で一部のシェルが抱える問題のワークアラウンドとして内部的に使用しています。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1