シェルスクリプトのTipsを記載します。
Table of Contents
1 for文で空白文字を区切り文字にしない
シェルスクリプトで用いる区切り文字は変数IFSに格納されており、デフォルトでは空白文字と改行文字です。これは、例えば空白文字を用いているファイル名をリネームする場合に扱いづらい場合があります。
#!/bin/sh
func_a()
{
echo "func_a"
echo "arg1 = ${1}"
echo "arg2 = ${2}"
for arg in $@; do
echo "arg = $arg";
done
}
func_b()
{
echo "func_b"
echo "arg1 = ${1}"
echo "arg2 = ${2}"
IFS="
"
for arg in $@; do
echo "arg = $arg";
done
}
func_a "This is arg1." "This is arg2."
func_b "This is arg1." "This is arg2."
上記を実行すると以下の出力が得られます。func_aのfor文は空白文字で区切るのに対し、IFSを改行文字のみにしたfunc_bのfor文は空白文字で区切りません。
func_a arg1 = This is arg1. arg2 = This is arg2. arg = This arg = is arg = arg1. arg = This arg = is arg = arg2. func_b arg1 = This is arg1. arg2 = This is arg2. arg = This is arg1. arg = This is arg2.
2 &&と||でif文を省略
&&と||を適切に用いることでif文の乱用を避けることができます。
perlのopen or dieのような構文が可能です。ただし、&&と||の順序は逆には出来ません。
$ true && echo "true" || echo "false" true $ false && echo "true" || echo "false" false
&&の後のコマンドが失敗した場合は||の後のコマンドが実行される点に注意して下さい。
$ true && false || echo "false" false
連続した&&と||も有用です。
$ true && true && echo "true" true $ false || false || echo "false" false
3 echoの出力を関数戻り値として扱う
関数内のechoで出力した文字列を変数に代入することができます。
#!/bin/sh
div()
{
[ ${2} -eq 0 ] && return 1
echo $(expr ${1} / ${2})
}
val=$(div 10 5) && echo "10 / 5 = ${val}"
val=$(div 10 0) && echo "10 / 0 = ${val}"
上記を実行すると以下の出力を得られます。div関数が成功した場合のみ結果が出力されます。
10 / 5 = 2
4 `cmd`よりも$(cmd)でコマンド実行
$(cmd)はそのまま入れ子が可能です。
$(cmd1 $(cmd2 $(cmd3)))
`cmd`はネストした数だけエスケープする必要があります。
`cmd1 \`cmd2 \\`cmd3\\` \``
5 (cmd), $(cmd), `cmd`は子プロセス
(cmd)、$(cmd)、`cmd`は子プロセスとして実行されます。&との違いはwaitする点です。
cmdの延長でexitを用いている場合は子プロセスは終了してもプログラムは終了しない点に注意が必要です。また、設定した変数も親プロセスには反映されません。
#!/bin/sh
func()
{
echo "prev exit"
exit 1
echo "post exit"
return 0
}
(echo "prev exit"; exit 1; echo "post exit")
echo "ret = $?"
msg=$(func)
echo "ret = $?, msg = ${msg}"
上記を実行すると以下の出力が得られます。
prev exit ret = 1 ret = 1, msg = prev exit
6 echoの戻り値をエラーとして扱う
以下のechoは標準エラー出力へ出力されますが、I/Oエラーのように出力が動作しないような深刻なエラーの場合を除き、echoの戻り値はtrueです。これはエラー時にエラー文をechoで出力しつつエラー用のcmdを実行する際に不都合となります(以下のcmdは実行されません)。
$ false || echo "error" 1>&2 || cmd
()を利用してechoとexit 1を実行することで不整合を回避できます。
#!/bin/sh
do_echo()
{
false || echo "error" 1>&2 || return 1
}
do_echo_and_exit()
{
false || (echo "error" 1>&2 && exit 1) || return 1
}
do_echo || echo "do_echo is failed" 1>&2
do_echo_and_exit || echo "do_echo_and_exit is failed" 1>&2
上記を実行すると以下の出力が得られます。
error error do_echo_and_exit is failed
echoを実行して戻り値をfalseとする関数を用意しても良いです。
#!/bin/sh
error()
{
echo "$@" 1>&2
return 1
}
false || error "error" || echo "failed" 1>&2
上記を実行すると以下の出力が得られます。
error failed
7 1行で関数定義
セミコロンを用いることで1行で関数定義できます。
func() { echo "func"; }
8 typeを用いたリフレクション
例えば以下のようなcase文で呼び出す関数を決める処理があり場合、func_xの定義が今後増加していくとします。
この場合、func_xの定義(callee)だけでなく、case文の処理(caller)も変更する必要があり、メンテナンス性が落ちます。calleeを追加してもcallerを変更しないで済むようにすべきです。
#!/bin/sh
func_a() { echo "func_a is called"; }
func_b() { echo "func_b is called"; }
read name
case ${name} in
a) func_a;;
b) func_b;;
*) echo "func_${name} is not defined";;
esac
typeを用いることでリフレクションなコードを記述でき、callerの変更が不要になります。
#!/bin/sh
func_a() { echo "func_a is called"; }
func_b() { echo "func_b is called"; }
read name
type func_${name} > /dev/null 2>&1 && func_${name} || \
echo "func_${name} is not defined"
9 デバッグで/bin/sh -xを用いる
sh -xオプションで実行中のステートメントが表示されます。これは.で読み込んだシェルスクリプトにも適用されます。
$ cat debug.sh
#!/bin/sh
a=1
[ ${a} -eq 1 ] && echo ${a}
$ sh -x debug.sh
+ a=1
+ [ 1 -eq 1 ]
+ echo 1
1
シェバングを/bin/sh -xにしても同様です。これは自動起動するシェルスクリプトのデバッグに極めて有効です。
#!/bin/sh -x