Linux: シェルスクリプトのTips

シェルスクリプトのTipsを記載します。

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