更新:$Date:: 2014-07-02 20:32:16 +0900#$, $Rev: 284 $

シェルスクリプト(sh, bash) Tips集

sh, bash, csh, tcsh でのテクニックや小ネタなど。

すぐ忘れてしまうのでサンプルコードを集めたメモを作りました。基本的に環境はLinuxという前提で書いていますが、SolarisやFreeBSDの話も時々混じります。

なお、このページの一番最後↓に本の紹介もしていますので、良かったら見てください。

あるディレクトリから、n日前からm日前までに更新されたファイルを全て列挙したい

findコマンドのmtimeオプションを使えば良い。ファイル名が返るので、それをxargs(1)で受け取ってお好きなコマンドで処理する。mtimeには+-で日数を指定し、プラスは「より前(より古い)」、マイナスは「より後(より新しい)」を意味する。

またオプションとして -type f と指定することにより、ディレクトリやシンボリックリンクを除外して通常ファイルのみを選択することができる。

# 前日更新ファイル一覧:
# 「+0:0日前(今日)よりも古く」かつ「-2:2日前(おととい)よりも新しい」という条件で挟めば、
# 前日ぶんだけを取得できるという寸法。
find . -name "*.txt" -type f -daystart -mtime +0 -mtime -2
# 90日以上前のファイルを削除する
find .  -name "*.txt" -type f -daystart -mtime +89 | xargs rm -f

上の例では -daystart というオプションを付けているので、これを解説する。普通は「前日のファイル」などの扱いをしたいだろうけど、findコマンドは「n日前」を「n * 24時間前」と扱ってしまうので、夜10時にコマンドを叩くと「前日夜9時」は「前々日」扱いになってしまう。これを防ぐために、Linuxのfindには -daystart というオプションが用意されており、これを付けると0時0分から数えるようにしてくれる。

findコマンドで見つかったファイルリストに対して、コマンドを実行したい場合は、以下のようにxargs(1)を利用すれば良い。

# 3日前から90日前までのファイルを選びだし、中身を一意にして表示
find .  -name "*.txt" -type f -daystart -mtime -91 -mtime +2 | xargs cat | sort | uniq

xargsは、cpコマンドやmvコマンドなど、複数の引数を取るコマンドも扱うことができる。この場合は、iオプションを付けて引数部分に {} を渡してやれば良い。

# 3日前から前日までに更新されたファイルを選びだし、まとめて /home/ozuma/tmp にコピーする
find . -name "*.txt" -type f -daystart -mtime -4 -mtime +0|xargs -i cp {} /home/ozuma/tmp

ディレクトリを再帰的に潜ってgrepしたい

カレントディレクトリの下にあるファイル全てにgrepしたい場合、どうするか。

Linuxならば、grepコマンド自体に -r オプションがあるのでこれを使うだけで良い。Solarisのgrepには -r オプションが無いので、findを組み合わせる。

# Linuxの場合
$ grep -r -i "PATTERN" *

# Solarisの場合
$ find . -print | xargs grep -l -i "PATTERN"

if文に空行を入れたい

あまり知られていない(?)が、shやbashでは空っぽのif文を書くとエラーになる。

#!/bin/sh

if [ 'abcdefg' = 'abcdefg' ]; then
fi

[ozuma@macbook ozuma]$ ./go.sh
./go.sh: line 4: syntax error near unexpected token `fi'
./go.sh: line 4: `fi'

他にも、if文の中身がコメント行だけでもエラーになる。

#!/bin/sh

if [ 'abcdefg' = 'abcdefg' ]; then
#echo 'hoge'
fi

[ozuma@macbook ozuma]$ ./go.sh
./go.sh: line 5: syntax error near unexpected token `fi'
./go.sh: line 5: `fi'

例えばif文の中身にロジックを書いていて、そこが要らなくなったからと言って安易にコメントアウトするとハマることがある。こういう場合は、if文の中に、sh/bashのbuiltinコマンドである ":"(ヌルコマンド)を書いておけば良い。

#!/bin/sh

if [ 'abcdefg' = 'abcdefg' ]; then
#echo 'hoge'
:
fi

スクリプトの置かれているディレクトリを取得したい

スクリプトの設定ファイルを読み込む場合、カレントディレクトリで叩かれたりcronで絶対パス指定して叩かれたりしてもちゃんと動くように、スクリプトの最初で自分自身の置かれているディレクトリにcdして移動するというのはよくあるパターンである。

この際、自分のディレクトリを取得するには dirname コマンドを使えば良い。

#!/bin/sh

cwd=`dirname $0`

cd $cwd
cat test.txt

これで、スクリプトの置かれたディレクトリから ./hoge.sh で実行しても、フルパスで /home/ozuma/hoge.sh で実行しても、相対パスで ../hoge.sh で実行しても、正しく動いてくれる。

別のスクリプトの成功/失敗の結果を取得したい

終了ステータス(returnの戻り値)が $? に入っているので、これを見れば良い。

#!/bin/sh

script/hoge.pl >> log/out.log 2>&1
if [ $? = 0 ]; then
  echo "hoge.pl : Successed"
else
  echo "hoge.pl : Failed"
fi

tcpdumpした出力をWiresharkで見たい

サーバのCUIでtcpdumpコマンドだけでも生パケットの中身は見られるが、やはり手元のPCのWiresharkなGUI上で見た方が圧倒的に分かりやすい。こういう場合は、Wiresharkで見られる形式でtcpdumpコマンドの結果を出力してやれば良い。

# tcpdump -i インタフェース名 -s 0 -w dump.cap

-iオプションでパケットを取得するインタフェースを指定する。Linuxサーバならeth0とかだろう。-sオプションはパケット収集の上限サイズで、デフォルトでは68バイトになっている。Ethernetなら普通は1500が最大だろうが、ゼロを指定しておくと無制限なのでそうしておく。最後の-wオプションで出力ファイル名を指定する。パケットが収集できたらCtrl+Cでコマンドを止め、出来上がったファイルをWiresharkで読み込めば良い。

なお、余計なお節介を無くすために私はいつも-nオプションを付ける。デフォルトではホスト名やポート番号を文字列にして出力しようとするので、数字のまま出してやるにはこの-nオプションを付ければ良い。

# tcpdump -n -i eth0 -s 0 -w dump.cap

ちなみにSolarisの場合は、snoopを使えば良い。-oオプションで出力したファイルがWiresharkでそのまま読める。-rオプションを付けてIPアドレスを逆引きさせないのがオススメ。コマンドを叩くと、Ctrl+Cを押すまでキャプチャしたパケットの数が表示される。

# snoop -r -d eri0 -o dump.cap

cshでファイル名の補完を行いたい

少し昔のSolarisを使っていると、素のcshしか入っていなかったりする。tcsh/bashは入っていないし、宗教上の理由からkshは使いたくない。zshなんて言ってるペンギン野郎は地獄の業火に投げ込んでおいた(tcshすら無いのにzshなんかあるわけ無いだろバカ野郎)。しかしbashの[Tab]でファイル名補完が羨ましすぎて死にそうだ。さてどうする。

こんな時は、"set filec"と打つとbashのようにファイル名が補完できる。ただし、補完キーはTabではなく、ESCもしくはCtrl+[である。

$ set filec
$ cd ho[ESC]
$ cd hoge

shutdownするには (Linux/FreeBSD/Solaris)

システムを終了して電源を切りたい……。この際に適切なコマンドは、UNIX系OSでもそれぞれ地味に作法が違うので、サーバ管理者を悩ませる一つのタネである。

Linuxの場合は、周知の通り-h nowするだけで良い。

# shutdown -h now

一方、FreeBSDの場合は、電源OFFまで行ってくれる-pを使う方が普通だろう。

# shutdown -p now

FreeBSDでは、shutdown -h nowした場合はシステムの停止を意味するので、電源OFFまでは行わない。一方、shutdown -p nowの場合は(ハードウェアがサポートしていれば)システム停止後に電源をOFFしてくれる。まぁ最近のPC/サーバなら、shutdown -p nowで電源落ちないなんてことは無いはず。

Solarisの場合は、ランレベルまで自前で指定する必要があるためこうなる。また、おまじないにsyncを入れる人も(未だに)多い。

# sync;sync;sync
# shutdown -i5 -g0 -y

最初にsyncを3回打つのは、まぁおまじないである(太古のSolarisでは本当にsyncを3回打つのにそれなりの理由があったらしいが)。その後、まずiオプションでランレベルを指定する。電源OFFの5を指定すれば良い。ちなみに-i0とランレベル0を指定すると、OS終了後PROMモニタに落ちるため電源OFFされない。この場合は、PROMモニタから続けて

ok power-off

と、power-offコマンドを打てば電源OFFできる。

gオプションはシャットダウンするまでの時間で、ここではnowにあたる0を指定している。yオプションは、シャットダウン時の確認に全て自動でyesを返す。普通はこのyオプションも付けてしまって良い。

ファイルを日付つきで簡単にコピーしておく

dateコマンドの結果を入れてしまおう。

$ cp hoge.conf hoge.conf.`date +%Y%m%d`

複数行のリダイレクトをすっきりさせたい

同一ファイルへのリダイレクトがずらずら並んでいるときは、{} でグループ化するとすっきり書ける。

# こう書かずに……
echo "foo" >> out.txt
echo "bar" >> out.txt
echo "piyo" >> out.txt

# -------------------------------

# こう書こう。
{
  echo "foo"
  echo "bar"
  echo "piyo"
} >> out.txt

変数の区切りを明示的に指定したい

変数の後ろに文字列を連結した場合、後ろの文字列までが変数名と見なされる場合がある。

FILENAME="aaa.txt"
echo "Start:" >> $FILENAME_log

上記の例は、"aaa.txt_log"というファイルに吐こうと意図しているのだが、"FILENAME_log"という変数名を探してエラーになってしまう。

こういう時は、{}でくくると「ここが変数」ということをはっきり指定できる。

FILENAME="aaa.txt"
echo "Start:" >> ${FILENAME}_log

また、パッと見でも一目で変数名が分かりやすいので、使わなくても大丈夫な場合でも文字列内ならば{}でくくることを(個人的に)推奨。

tarで一部ファイル・ディレクトリを除外してアーカイブしたい

--excludeオプションを利用する。

$ tar cvf archive.tar --exclude log /where/to/my/apps

こうすれば、/where/to/my/apps/log ディレクトリを除外してアーカイブできる。excludeにはパターンマッチでも指定できる。

定数を宣言したい

定数として使いたい場合は、変数代入の際にreadonlyと付けると、誤って代入しようとしても"readonly variable"とエラーになってくれる。

macbook:tmp ozuma$ cat a.sh 
#!/bin/sh

readonly MESSAGE="hello"
echo $MESSAGE
MESSAGE="Hi"
echo $MESSAGE
macbook:tmp ozuma$ ./a.sh 
hello
./a.sh: line 5: MESSAGE: readonly variable

バッククォート `` は$()でもいい

コマンドの出力結果を取りたい時にバッククォートでくくるけど、これがネストした時などは$()で全体をくくると良い。 \` とエスケープしてもいいけど見づらい。

たとえば、こういう感じでマッチ行数を数えられるとする。

$ grep -c hoge_pattern /var/log/httpd/access_log_`date -d '1 days ago' +%Y%m%d`

これをシェルスクリプト中で使ってカウント数を変数に格納するなら、エスケープするか、$()でくくれば良い。中身に手をいれなくていいし読みやすいので、$()でくくった方がいいね。

# エスケープする:全体をバッククォート `` でくくって、中のバッククォートをエスケープする。
count=`grep -c hoge_pattern /var/log/httpd/access_log_\`date -d '1 days ago' +%Y%m%d\``

# $()でくくる:
count=$(grep -c hoge_pattern /var/log/httpd/access_log_`date -d '1 days ago' +%Y%m%d`)

途中でコマンドが失敗したらそこで終了する

シェルスクリプトでは、途中でコマンドが失敗しても構わず次行に移って実行し続ける。これで問題になる典型的な例は、削除スクリプト内のcdでディレクトリに移動に失敗したのに、かまわずrmしてしまう例かな。

#!/bin/sh

cd /my/apps/tmpp   # "tmp"に入るつもりがタイプミスしていた!
rm *               # カレントディレクトリのファイルが全部消えちゃう!

こういう例では、set -e しておけば終了ステータスが非ゼロの場合にそこで止まる。

#!/bin/sh
set -e

cd /my/apps/tmpp
rm *

もっとも、exit statusが0以外ならなんでもかんでも止まってしまうので、いつでも付ければいいというわけでもない。これはケースバイケース。

未定義の変数はエラーにする

未定義の変数(大抵の場合はタイプミス)を参照したら、エラーを吐くようにするには set -u すれば良い。

#!/bin/sh
set -u

MES="hello"
echo $MESS

これを実行すると:

$ ./go.sh
./go.sh: line 5: MESS: unbound variable

lnコマンドに引数は1つで良い

リンク先と同じ名前のシンボリックリンクを作るなら、第二引数のtargetは必要無い。たとえばカレントディレクトリに、/var/tmp に向く "tmp" という名前のsymlinkを作りたい場合。

# こうして丁寧に書いてあげる必要は無い。
$ ln -s /var/tmp tmp

# これだけで良い。
$ ln -s /var/tmp

本の紹介

※2014/07/02追記:
このページを作って細々とメモしていたところ、幸運なことにシェルスクリプトの本を書いてみないかというお誘いがあり、ガシガシと書いて2014/06/25に発売されました。

色々なシェルスクリプトのTipsを、現場でLinuxサーバの構築・運用をしている方向けに「ありがちな事例」をイメージしつつ、たくさん紹介しています。なかなか他の本では書かれていないような、サーバ運用周りのノウハウも色々詰め込んでいますので、よろしければ読んでみてください。


プログラミングメモ

▲HOME

▲ABOUT ME