Practicalなシェルスクリプトの解説書 (α版)

はじめに

この文書ではシステム管理の観点からシェルスクリプトの書き方についてまとめています。 対象はbourne-shell系のksh(pdksh), bashです。 csh系はいくつかの理由からシステム管理に不向きと考えるため対象から外しています。

csh系をシステム管理に使わない理由については、 Cシェルを使わない理由 を参照してください。 csh系をシステム管理に不向きとする主な理由は、書式の自由度が高い反面、様々な書き方が可能でまぎらわしくなる事、機能的な利点がほとんどない事、挙動が予測できない場面があり制御が難しい事、などが挙げられます。

bourne-shell系は空白を切り分け文字として多用するため空白を含むファイル名などの取り扱いには一部注意が必要です。 しかし全般的に覚える事が少なく、条件分岐・ループなどの制御構文はシンプルでバッチ的なコマンド実行に向いています。

最近ではksh, bashの機能拡張が進み、慣れ以外にcshが好まれる理由はないと思います。 ぜひログインシェルとしてksh, bashを登録しましょう。

知識が増えればログインシェルとして便利に使えるcsh系、少ない知識を駆使して頑張るbourne-shell系といえるかもしれません。

この文書が対象とする範囲

この文書はシステム管理者の視点からまとめたシェルスクリプトの解説書を目指しています。 稼働確認は Ubuntu 8.04 LTS amd64版上でbashとpdkshを使って行なっています。

bashが標準で準備されていない環境を想定してpdkshでも稼働確認を行ない、bash固有の表記は避けました。 しかしbashが標準装備されていない(主に商用OSの)環境では、スクリプトの中で呼び出している外部コマンドの動きに違いがあるかもしれませんので注意が必要です。

またオフィスからサーバールームにあるサーバーにログインしている環境を想定しています。 WindowsであればTeraTermやPoderosa, MachintoshであればiTerm, Linuxならkterm, gnome-terminal, konsole等を使って作業をする環境です。

Remote Login from Office Room

この文書の構成

これまでの経験から例題と遭遇する問題点を挙げてシェルスクリプトの「べき・べからず」を紹介していこうと思います。

もちろん用途や状況によって必要とされる観点は異なるため、解決策も状況によって変化します。 ここで挙げたものを一つの例として捉えて、自身が遭遇した課題に対応する際の参考になれば幸いです。

シェルスクリプトの基礎

この文書で説明するために必要な用語、機能を説明します。

環境変数

環境変数は PATH=/usr/bin:/bin のように変数名と値がイコールで繋がったものです。 この環境変数はシェルに対して設定し、シェルから実行されるコマンドにも設定され、引き継がれていきます。

"export" 内部コマンドをいつ使うのか、よく判っていないと思われるコードを見ることがあります。

"pstree" コマンドを使う事で、プロセスの親子関係を

余談ですが、C言語でmain関数を記述する場合、通常は int main(int argc, char** argv) と書きます。 しかし環境変数を参照するための"envp"を追加した、 int main(int argc, char** argv, char** envp) という書き方もあります。 いまとなっては古い書き方ですが、これは元来プログラムは外部の状況を知るために環境変数を利用するという枠組みがUNIX系OSでは準備されていた事を表しています。

シェル変数

シェルスクリプト内部で扱う変数がシェル変数です。 実行するコマンドに伝える必要がなければ export に並べて書く必要はありません。 無闇に export を使う事は避けましょう。

外部コマンド

"/bin", "/usr/bin"などのディレクトリに存在する実体のあるコマンドを外部コマンドと呼びます。 外部コマンドが存在しても、シェルによっては後述する内部コマンドが代りに実行される場合があります。

内部コマンド

シェルの内部に実装されていて、外部コマンドと同様に扱える機能を差して内部コマンドと呼びます。 bashを使っていれば $ help で内部コマンドの一覧と使い方を確認する事ができます。

$ help
...
umask [-p] [-S] [mode]             unalias [-a] name [name ...]
unset [-f] [-v] [name ...]         until COMMANDS; do COMMANDS; done
variables - Some variable names an wait [n]
while COMMANDS; do COMMANDS; done  { COMMANDS ; }

例えば"/usr/bin/which"はスクリプトで、bashでは外部コマンドとして実行されますが、tcshでは内部コマンドが準備されているため"which"を実行しても"/usr/bin/which"は起動されません。 "echo"コマンドはbash, tcsh両方で内部コマンドとして動き、 $ echo -e test の挙動は両方で異なります。

サーバー運用に特有なことがら

インターネットに直結することもあるサーバーはセキュリティを比較的高い状態で維持する必要があります。 そのための基本的な対応として、導入するアプリケーションを限定したり、telnetdを停止するなどネットワークサービスを制限します。 当然gccなどのコンパイラは導入しません。

あると便利だけれどもパッケージされていないPerlのCPANライブラリを/usr/lib/perl5以下に導入できないという状況も当然考えられます。

システム管理者の仕事は、様々な制限との戦いといえるでしょう。 アプリケーション開発者は "PERL5LIB" の意義を理解できずに「必要なんだから、導入しろ」というかもしれません。 また、制約との戦いを時間の無駄と考えて、指示されるがままに(ディスク容量以外)限度なく自由にアプリケーション、ライブラリを導入できる環境もあるかもしれません。

しかし組織が成熟し大きくなってくると、負荷分散のためにクラスタリングなど冗長化を行なうようになります。 導入から時間が経つとクラスタリングされている複数のサーバー間での違いが障害の原因になってきます。 準備した変更手順書を配って数人で作業をする時にサーバーAでは実行できるが、 サーバーBではエラーになり、手順書実行に創造力が求められる事態に発展する可能性があります。

システム管理者に 必要なスキル は非常に大きな振り幅を持っています。 それは障害が発生するまで、日々の努力が必要だったと認識されることがないからです。 そのため障害が発生するまでは、スキルに乏しくても有能なシステム管理者になる事ができます。 いざ障害に至った時に、どのような対応をするかで、そのシステム管理者の真価が発揮されるといえるでしょう。

この文書ではシェルスクリプトの解説書ですが、念頭に置いている事は上記のようなことがらです。 シェルスクリプトを手掛りに自信をつけて、目立たないけれどいざという時に力を発揮する管理者を目指してください。

ケース・スタディ

ここからは例を使って、説明をしていこうと思います。

ケース0:シェルスクリプトの転送

シェルスクリプトに触る前に、一覧の作業について触れておきたいと思います。

UNIX系のOSでファイルを編集するには $ emacs$ vi といったコマンドを使います。 しかしサーバーの運用では導入されているエディタは限られているため、比較的使い易いemacsが導入されている環境はかなりラッキーといえるでしょう。viは使いづらいという評判があり、習得しようという気持ちにならないかもしれません。

そんな状況では、 手元にあるWindows上のエディタソフトを使いたいという気持ちになるのも自然といえるでしょう。 しかしサーバー上のシェルスクリプトを手元のエディタで編集した事が原因で問題を起してしまうことがあります。

端末間のコピー・ペーストは間違いのもと

TeraTermやPoderosaなどの端末ソフトでシェルスクリプトを $ cat コマンドなどで表示させ、エディタにコピーして編集をするかもしれません。 編集が終った後で、反対の作業をしてサーバー上のファイルを上書きしますが、この作業は下記のような問題点を含んでいます。

文字化けが起ってしまう

ここ数年間でUTF-8が普及していますが、以前からUNIX系サーバーを運用している環境では日本語の取り扱いに違いがあります。 大学などで使われてきたLinux、FreeBSD等の無料で入手できるOSではEUCが標準的に使われてきました。 企業で主に使われる商用OSではEUCに加えてShift_JISのサポートを独自に追加しており、Windows端末との相互運用性を高めるためにShift_JISを採用している組織も多くあります。

サーバー上のシェルスクリプトで日本語を扱わないという選択肢は後ろ向きですが、 日本語を扱うためには環境を管理する必要があり、慣れるまではとりあえずお勧めの選択肢です。

それができない場合には、スクリプトに日本語を書いても影響がないようにサーバーがサポートする言語環境を理解する必要があります。 とりあえずの理解は $ locale -a の出力の中の一つを "LANG" 環境変数に指定すると覚えておけば良いかと思います。

$ locale -a | grep -i ja
ja_JP.utf8
$ export LANG=ja_JP.utf8

またTeraTermやPoderosaといった端末ソフトウェアにもサーバーから受け取った文字コードをUTF-8、EUC、Shift-JISのいずれと判断するか設定する項目があります。

OS間での行末コードの違い

サーバー上でファイルを $ cat で開いても問題はないのに、 $ more$ vi コマンドで開くと各行の終りに ^M が表示される場合があります。 最近のLinuxを使っていれば気がつかないと思いますが、AIXやNetBSDなどでこの現象に遭遇します。

Windows上では行末記号が2文字分使うのに対して、UNIX系OSでは1文字分であるために、余った1文字が ^M として画面に表示されます。 UNIXサーバー上で修正する必要はいくつかありますが、 $ tr コマンドを使う方法を覚えておけば大抵の環境で通用すると思います。

$ tr -d '\015' < script.sh > new.script.sh 

小さくテストし、石橋を叩く

これまで挙げてきたような問題は、実は、事前にテストしていれば防げていました。 そもそも何が障害に発展するか理解していないので「何をテストしたら良いかわからない」というのが現状でしょう。 本番環境と同じ構成のテスト環境を持つ事を真剣に検討してください。

負荷に耐える必要はないですが、2つのサーバーを1つにまとめるといった事はせずに、VMWareやVirtualBoxなどの仮想化環境の利用して構築する事をお勧めします。

その他はシェルスクリプトの中では変数を echo コマンドで表示させたり、 exit を使って実際に cprm を実行する前に処理を止めるといった事ができます。

ケース1:リロケータブルなスクリプト

スクリプトを別のディレクトリにコピーしたら動かなくなったり、 ログアウト→ログインしたらうまく動かなくなるという場合があります。

~(チルダ)で終ったファイルを削除する

"emacs"を使っていると増えるのが"名前が~(チルダ)で終るファイル"です。 ホームディレクトリの下から~で終るファイルを探し出して削除するスクリプトを作成します。

$ cd ~/
$ mkdir bin
$ vi bin/remove_~file.sh

"~/bin/remove_~file.sh"の内容は次のようにしました。

#!/bin/bash
find . -name "*~" -exec rm {} \; -printf "remove %f\n"

遭遇する問題

このスクリプトはホームディレクトリから実行する事で正常に稼働します。 次のようなコマンドを実行したらどうなるでしょうか。

$ cd /
$ ~/bin/remove_~file.sh

トップディレクトリ"/"から"find"コマンドを実行すると、時間と負荷がかかるだけになってしまいます。 こういう間違いはシェルのヒストリー機能で起こるかもしれません。

この他にも".logで終わるファイルをgzipで圧縮する"といったシェルスクリプトをcrontabに登録して、定時実行すると意図しないディレクトリのログファイルを圧縮することも考えられます。

考えられる対応

このスクリプトの修正方法を2つ考えてみます。 1つはホームディレクトリに"cd"する事で毎回決まった場所に移動するケース。 もう1つは"find"の引数にホームディレクトリを指定するケースです。

#!/bin/bash
cd ${HOME}
find . -name "*~" -exec rm {} \; -printf "remove %f\n"
#!/bin/bash
find ${HOME} -name "*~" -exec rm {} \; -printf "remove %f\n"

どちらも正解ですが、状況に応じて最適な方法は異なってきます。 通常は"cd"コマンドを使わずに解決する道をまずは探すべきでしょう。

カレントディレクトリ(==作業用ディレクトリ)を意識する

シェルに限らずプログラムを作成する場合には、カレントディレクトリを強く意識しなければなりません。 「ファイルを絶対パスで指定すればOKじゃない?」と考えるかもしれません。それも1つの解決策です。

しかし異常終了した時のcoredumpファイルや外部コマンドの出力先が、カレントディレクトリになる場合があります。 もしスクリプトをrootユーザーが"/"で実行している間に異常が発生すると、実行時にいたファイルシステムの使用率が100%に逹っする可能性があります。

スクリプトの中で "cd" コマンドを使っても良いのですが、その場合はスクリプトの先頭で実行し、相対パスを使う場合は "cd" 先のディレクトリを基点にするべきです。 coredumpファイルを作成する可能性のあるアプリケーションを起動する場合には、溢れても問題のないファイルシステムに "cd" し、スクリプト内部でのファイル指定には絶対パスを使うというハイブリッドな方法も検討するべきでしょう。

相対パスの使い方

設定ファイルの読み込みや、アプリケーションを特定のディレクトリの中に閉じ込めたい場合もあります。 例えばこのドキュメントに使用しているスクリプトは次のようなディレクトリ構造を持っています。

anakia-1.0/            .. コンテンツ作成用anakiaパッケージ
apache-ant-1.7.1/      .. 自動処理用antパッケージ
build.sh               .. antコマンドを実行するための環境変数を設定するwrapperスクリプト
xml/                   .. コンテンツ作成のファイル入力用ディレクトリ
html/                  .. 公開用htmlファイル出力先
prop/build.xml         .. ant用の設定ファイル
prop/shell_script.vsl  .. コンテンツ変換用のルールを設定したファイル

この構造を保つ限り、マシン上のどのディレクトリ以下に配置しても動く事が可能になります。 文書の生成には"build.sh"から"prop/build.xml"ファイルを読み込む必要があるため、 このスクリプトは次のような構造を持っています。

#!/bin/bash
...
BASEDIR="$(dirname $0)"
cd "$BASEDIR"
ant -f prop/build.xml"

ここでは "BASEDIR" 変数を cd "$BASEDIR" にしか使っていませんが、 他の処理でもこの情報を利用すると問題が起る場合があります。 例えば、次のような処理を考えてみます。

#!/bin/bash
BASEDIR="$(dirname $0)"
export PATH="$BASEDIR/apache-ant-1.7.1/bin:$PATH
...
cd "$BASEDIR"
...

$ ../build.sh のように起動したとすると、"$BASEDIR"は".."となり、PATH環境変数が差し示す場所は cd "$BASEDIR" の後では相対的に変化し、正しい場所ではなくなります。 使い方が難しいので通常は"cd"を使うべきではなく、"ant"コマンドの "-Dbasedir=$BASEDIR" オプションを使って解決する方法を探すべきなのですが、呼び出しているライブラリがどうしてもカレントディレクトリを参照するため止むを得ず"cd"コマンドを使っています。

ケース2:ファイルパーミッションの管理

一般的なセキュリティを確保するためのルールの中には「権限を持つユーザーにのみ必要な情報を提供する」という考え方があります。 サーバー自体にログインさせないという選択肢もありますが、セキュリティ上の問題の多くは内部の関係者から発生しているのが実情です。 システム管理者自身が責任を取らされる状況を回避するためにも、システムに妥当なルールを設ける事は必要です。

その他に複数のアプリケーションが連携する場合に、CSVファイルなどの形式で相手側アプリケーションのディレクトリにコピーするスクリプトを準備する場合があるかもしれません。 アプリケーション開発者からファイルを置いたのに参照することができないという問い合せを受けたこともあります。 開発者とはいってもWindows上での経験が主で、UNIX系OSでの経験が不足している状況にもよく遭遇します。

ログファイルを圧縮する

システムのログファイルであれば "logrotate" を使えば解決しそうですが、 自作のアプリケーションが出力したログファイルを圧縮するスクリプトを作成してみました。

#!/bin/bash
BASEDIR="$(dirname $0)"
find "${BASEDIR}/../logs/" -type f -name '*.log' | while read file
do
  newlog="${file}.$(date +%Y%m%d.%H%M)"
  mv "${file}" "${newlog}"
  touch "${file}"
  gzip "${newlog}"
done

このスクリプトはメンテナンスのために、サーバープロセスが日に1回定期的に実行するようになりました。

遭遇する問題

障害が発生したのでログファイルを解析する必要があり、サーバーにログインし昨日のログファイルを確認しようとしました。

$ cd logs
$ gzip -cd system.log.20091212.1234.gz
gzip: system.log.20091212.1234.gz: Permission denied

確認したところ "system.log.20091212.1234.gz" の権限が"root"ユーザーのみが確認できる状態でした。

-rw------- 1 root www-admin 22 2009-12-12 12:35 system.log.20091212.1234.gz

考えられる対応

この状況の対策を2つ考えてみます。1つはrootユーザーになりファイルを確認します。もう一つは生成されるログファイル全体に読み取り権限を付与します。

解決案の1つ目、rootユーザーになる場合、 "su" を使うか、最近では "sudo" が好まれます。 これは "sudo" がrootユーザーのパスワードを教えることなく、root権限でコマンドを実行できるためです。 また実行できるコマンドの制限やログの取得もできるため、不注意によるコマンドの打ち間違いを防ぐといった事はできます。

もう一方のログファイルに読み取り権限を与える方法は、ファイルが作成される前後のどちらかで行なう事ができます。 今回は迅速なログファイルの確認を優先する考えから、ファイルが作成されるタイミングで -rw-r--r-- となる権限を与える方法を考える事にします。

umask内部コマンドの使い方

環境変数に対する説明のところで、実行するプログラムに引き継がれるという説明をしました。 シェルスクリプトもシェルやcrontabから起動されるプログラムですから、実行元の環境を引き継いだ上に実行されます。 新規に作成されるファイルの権限を -rw-r--r---rw------- に変更する仕組みが "umask" です。

シェル上で "umask" を実行すると、現在の設定値が表示されます。

$ umask
0022

設定値が"0022"の場合には、新規作成ファイルの権限は"0666"から引き算された権限、 -rw-r--r-- になります。 よりセキュリティを高めたい場合には"0066"を設定し、必要に応じて明示的に許可を出さない限りは、第三者がファイルを読めないようにしてしまう事ができます。

今回問題になったスクリプトでは、設定値が"0066"だったためにこのような問題が起りました。 修正は先頭部分で1行追加し、以下のようになりました。

#!/bin/bash
umask 022
BASEDIR="$(dirname $0)"
...

セキュリティの観点だけをみれば、 umask 066 をデフォルトで適用し、必要に応じて許可を出す形が良いと思います。 いずれにしてもシェルスクリプトの中では目的に応じて "umask" を適切に設定する事をお勧めします。

一般的なファイル権限

UNIX系OSで使われるファイルのセキュリティには次の2種類があります。

1番目は古典的なUNIXから存在し、ファイル権限の管理の仕組みとして一般的なものです。 シンプルですが、その分 "find" コマンドの "-perm" オプションで確認する事も容易です。

2番目のACLはWindowsのNTFSでも見られますが、一般的なUNIXでも拡張属性を利用した機能として最近では一般的になりつつあります。 ACLは適切な権限がファイルに付与されている事を確認する仕組みが一般的ではなく、「ユーザーAがファイルFを参照できない事」を後から証明しようとすると、ACLに登録されているグループを総当たりで調べるなど、使い方によっては自動化するためにちょっとした規模のスクリプトを作成する必要がでてきます。

ACLは便利ですが、セキュリティのルールとして「定期的に適切な権限が付与されているか検証する」といった項目があった場合には、少し混乱を招くでしょう。 ルールを設定する事も管理者の仕事ですが、それが検証可能かどうかも考えてみてください。 あまり真に受けずになぜそういったルールが存在するのか、その理由を考えてみると妥当な落とし所に落ち着くと思います。


Created: 2009-12-12, Last modified: 2010-03-19

2009,2010 © Yasuhiro ABE <yasu@yasundial.org>

Valid XHTML + RDFa RDFa it (RDF/XML)!

正当なCSSです!

Creative Commons License www.yasundial.org by Yasuhiro ABE is licensed under a Creative Commons Attribution 2.1 Japan License. Permissions beyond the scope of this license may be available at http://www.yasundial.org/info/license.html.