シェルスクリプトを書くときに気をつけていること(その1)

初級者向けに『自分がシェルスクリプト書くときに気をつけていること』をまとめてみました。

@『シェルスクリプトを書く際に気を付けていること8箇条』の乗っかりエントリです。
内容は重複しないように書いてますので合わせて読んでください!


基本的にLinux/bin/sh、/bin/bashを想定しています。

テキスト処理は標準入力から受け取って標準出力に出す

テキストを扱う小さなツールを作りましょう。
引数はオプション情報を渡すのに使います。

そうすればgrep、sort、uniqなどの便利なコマンドとパイプで連携できます。

grep ERROR /tmp/test.log | my_cmd1.sh 192.168.1.1 | sort

全てを実行する1つのスクリプトを作るのはたいへんだし、応用が効かないです。

人間に伝えたいメッセージは標準エラー出力に出す

パイプでつないだ時に予期せぬ誤動作を引き起こすのを防ぎます。

echo "warning message!!!" >&2

文字列中の変数は{}でかこむ

文字列中に変数を埋め込む場合は変数名を{}で囲むようにしています。

var=abc

ls $var             # OK

ls /tmp/$var1.xml   # ダメ
ls /tmp/${var}1.xml # OK

ls /tmp/$var.xml    # OKなんですが
ls /tmp/${var}.xml  # 統一したほうが見やすいと思います

変数を""で囲む

外部入力から変数をセットする場合は特に要注意です。


シェルスクリプトを書き慣れてないプログラマがif文書くとよくこうなってます。

if [ $var = "abc" ];then
  echo true
fi

これは変数が空だった場合はこう解釈されて

if [ = abc ];then
  echo true
fi

エラーになっちゃいます。(まぁそれでもたいてい動きますが・・・)

test.sh: line 4: [: =: unary operator expected

ちゃんとクオートしましょう。

if [ "$var" = abc ];then # 逆にabcはクオートしなくてもいいです
  echo true
fi

他にも変数に「*」が入っててファイル名展開されちゃうなど、制御文字系の予期せぬ動作を防ぐことに役立ちます。

ヒアドキュメントを使う

ミドルウェアの設定ファイルをテンプレート化したい場合など、sed等でやろうとするとけっこうたいへんです。
echoの引数に埋め込んでもいいのですが、ヒアドキュメントが見やすいと思います。

DOCUMENT_ROOT=$1;shift
DOMAIN=$1;shift

cat <<END > /etc/httpd/conf.d/vhost_${DOMAIN}.conf
<VirtualHost *:80>
    ServerName ${DOMAIN}
    DocumentRoot ${DOCUMENT_ROOT}

    <Directory ${DOCUMENT_ROOT}>
        Order deny,allow
        Allow From all
    </Directory>
</VirtualHost>
END

テンプレートファイルが複雑で外出ししたい時はphpやeRubyなどを使うことが多いです。


ftpなどの対話コマンドへの流し込みなどにも便利ですし

#!/bin/sh

SERVER=$1
USER=$2
PASS=$3
FILE=$4

ftp -n <<END
open $SERVER
user $USER $PASS
cd /tmp
binary
prompt
put $FILE
END

コマンドラインからもよく使います

$ cat <<END | grep -i error
<メモ帳などから貼付け>
END

-xを使って処理を追う

デバッグ時や処理の流れを目視確認したい時はxオプションを使いましょう。
sh、bashにつけてスクリプトを実行すると処理の流れが追えます。

$ sh -x ./test.sh
+ var=abc
+ '[' abc = abc ']'
+ echo true
true


もちろんスクリプトのアタマで指定してもOKです。

#!/bin/sh -x
var="abc"

if [ $var = "abc" ];then
  echo true
else
  echo false
fi

設定ファイルを外に出す

設定切り替えなど必要そうなら変数定義を外出ししましょう。
別ファイルに変数を定義して

VAR1=aaa
VAR2=bbb

「.」かsourceで読みこみます

#!/bin/sh

. /tmp/test.conf

echo $VAR1
echo $VAR2

ファイル指定を相対パスにする

必要なファイルをディレクトリにまとめたとき、設定ファイルなどを単純に相対パスで指定すると

#!/bin/sh

. conf/test.conf

echo $VAR1
echo $VAR2

『特定のディレクトリじゃないと実行できないスクリプト』ができちゃいます。

$ ./test.sh
aaa
bbb
$ cd ..
$ test/test.sh
test.sh: line 4: conf/test.conf: そのようなファイルやディレクトリはありません

どこに置いても動くようにしたい場合は実行スクリプトを基点にしてcdしてやります。
dirnameと$0(実行スクリプトのパス)を使います。

#!/bin/sh

cd `dirname $0` || exit 1
. conf/test.conf

echo $VAR1
echo $VAR2

PATHを明示する

cronなど対話シェル以外で使う場合、~/.bash_profile等が読み込まれずにPATHが意図しないものになっている可能性があります。
あと/usr/local/binや~/binなどをコマンド検索に入れたくない場合もあるでしょう。
そういう時は明示的に指定してやります。

PATH=/bin:/usr/bin
export PATH

互換性維持のために使うコマンドを全部定義する人もいますが、

LS=/bin/ls

$LS /tmp

面倒だし見づらいので好きじゃないです・・・

``ではなく$()を使う

ネスト時するときに便利です。

# こういう場合はいいですが
count=`wc -l /tmp/test.log.20120226 | cut -d" " -f1`

# ネストすると見づらいし、挙動がわかりづらいです!
count=`wc -l /tmp/test.log.\`date +%Y%m%d\` | cut -d" " -f1`

# こういう場合は$()を使いましょう
count=$(wc -l /tmp/test.log.$(date +%Y%m%d) |cut -d" " -f1)


同じようなのに『算術演算にexprじゃなく$(())を使う』というのもあります

i=3

echo `expr $i \* 3`
echo $((i * 3))

終わりに、と最後のポイント

職業柄、シェルスクリプトはよく書いていますが、書く前に意識するのがシェルスクリプトでがんばりすぎないこと』です。
自分がシェルスクリプトを使うのはだいたいこういう目的です。

  • 普段、手でやってる作業を自動化する
  • 行単位の簡単なテキスト処理

以下のような場合にはたいてい別の言語を使うことを検討します。

  • 配列やハッシュマップなどのデータ構造を使いたい
  • 複雑なオプションを使って高度に処理を制御したい

自分の場合、Perlが多いですが、PythonもOS制御、ネットワーク系のライブラリが揃っていて良さそうだなと最近は思ってます。


あとは、perlawkワンライナーをぜひ覚えましょう。
できることがガッと広がります。


書ききれなかったTIPS系のネタがいろいろあるのでそのうち『その2』を書きます!!!