更新:$Date:: 2014-07-02 20:32:16 +0900#$, $Rev: 284 $
sh, bash, csh, tcsh でのテクニックや小ネタなど。
すぐ忘れてしまうのでサンプルコードを集めたメモを作りました。基本的に環境はLinuxという前提で書いていますが、SolarisやFreeBSDの話も時々混じります。
なお、このページの一番最後↓に本の紹介もしていますので、良かったら見てください。
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したい場合、どうするか。
Linuxならば、grepコマンド自体に -r オプションがあるのでこれを使うだけで良い。Solarisのgrepには -r オプションが無いので、findを組み合わせる。
# Linuxの場合 $ grep -r -i "PATTERN" * # Solarisの場合 $ find . -print | xargs grep -l -i "PATTERN"
あまり知られていない(?)が、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
サーバの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
少し昔のSolarisを使っていると、素のcshしか入っていなかったりする。tcsh/bashは入っていないし、宗教上の理由からkshは使いたくない。zshなんて言ってるペンギン野郎は地獄の業火に投げ込んでおいた(tcshすら無いのにzshなんかあるわけ無いだろバカ野郎)。しかしbashの[Tab]でファイル名補完が羨ましすぎて死にそうだ。さてどうする。
こんな時は、"set filec"と打つとbashのようにファイル名が補完できる。ただし、補完キーはTabではなく、ESCもしくはCtrl+[である。
$ set filec $ cd ho[ESC] $ cd hoge
システムを終了して電源を切りたい……。この際に適切なコマンドは、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
また、パッと見でも一目で変数名が分かりやすいので、使わなくても大丈夫な場合でも文字列内ならば{}でくくることを(個人的に)推奨。
--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
リンク先と同じ名前のシンボリックリンクを作るなら、第二引数のtargetは必要無い。たとえばカレントディレクトリに、/var/tmp に向く "tmp" という名前のsymlinkを作りたい場合。
# こうして丁寧に書いてあげる必要は無い。 $ ln -s /var/tmp tmp # これだけで良い。 $ ln -s /var/tmp
※2014/07/02追記:
このページを作って細々とメモしていたところ、幸運なことにシェルスクリプトの本を書いてみないかというお誘いがあり、ガシガシと書いて2014/06/25に発売されました。
色々なシェルスクリプトのTipsを、現場でLinuxサーバの構築・運用をしている方向けに「ありがちな事例」をイメージしつつ、たくさん紹介しています。なかなか他の本では書かれていないような、サーバ運用周りのノウハウも色々詰め込んでいますので、よろしければ読んでみてください。