RedHat系Linuxの起動ファイル |
YAMAMORI Takenori ●yamamori |
ここではシェルスクリプトの基礎について解説します. ただし,シェルスクリプトそのものの話題となると範囲が広過ぎるため, おもにrcスクリプトでの使用を前提に,焦点を絞ります.
なお,シェルスクリプトといっても実はあまり難しく考える必要はなく, 原始的には,単に実行したいコマンドをそのまま並べればよいのです. たとえば,rc.localに独自の設定を追加したいような場合, 単に起動したいコマンドをスクリプトの最下行に追加するだけで十分な場合も 多いでしょう.ただし,OSによってすでにインストールされているrcスクリプトには, さまざまなシェル文法が使われ,少々複雑な書き方もされています. したがって,これらを読みこなすためにはそれなりのシェルスクリプトの知識が 必要になるでしょう.
rcスクリプトはBシェル(Bourne Shell)のスクリプトであり, 決してcshなどのほかのシェルは使われません. シェル自体はどのOSでも/bin/shに存在します.(※注)
また,rcスクリプトの1行目には,通常のシェルスクリプトのように 「#!/bin/sh」と書かれている場合と,そうでない場合があります. さらに,rcスクリプトのファイルのパーミッションの,「x」(実行属性)が 必要な場合と必要でない(別にあっても構わない)場合があります. これらの違いは,initやほかのrcスクリプトから呼び出される際に, 直接の実行ファイルとして呼び出されているか, または「sh /etc/rc.local」のように明示的にshを指定して, 単にshの引数として実行されているかの違いであり, OSやrcスクリプトの呼び出される場面によって異なります.
rcスクリプトも通常のシェル環境と同じく,実行されるコマンドはPATHに設定された ディレクトリからサーチされます. このため,rcスクリプト中では最初に明示的にPATHを設定します. たとえばFreeBSDの/etc/rcでは,その先頭付近に次のように書かれています.
HOME=/ PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin export HOME PATH |
ここでは,シェル変数としてPATHおよびHOMEを設定し, そのあとexportコマンドによってシェル変数を環境変数に反映しています. ここで,Bシェル系の一部のシェルでは,「export PATH=/sbin:...」のように 代入とexportとを同時に行なうこができる場合があります. ただし,この書き方は少なくともrcスクリプト中では避けるべきでしょう. (コラム「Bシェルの拡張文法について」参照)
なお,変数への代入時には,C言語とは違って「=」の両側にスペースを入れることは できませんので,注意して下さい.
シェルスクリプト中では,C言語などのほかのプログラミング言語と同様に if文などの制御文が使えます.これらについて順に説明します.
Bシェルでは,以下のようにしてif文を使うことができます. if文の終了は,ifの綴を逆順に並べたfiです. なお,elseやelifも使えます.
rcスクリプトでは,あるファイルが存在すればそのファイルを実行する (または読み込む)といった構文がよく使われます. たとえば,RedHat系Linuxには次のリスト*のような記述があります. これは,/etc/sysconfig/networkというファイルが存在すれば, そのファイルの内容を「.」コマンド(後述)で読み込むというものです.
if [ -f /etc/sysconfig/network ]; then . /etc/sysconfig/network else NETWORKING=no HOSTNAME=localhost fi |
ここで,ifの右にある[ ]は,一見Bシェルの文法上の構文のように見えますが, 実はそうではありません.奇妙ですが,これは「[」という名前のコマンドで, 「/bin/[」(または「/usr/bin/[」)として外部コマンド版が存在します. 「[」コマンドはtestコマンドとハードリンク(またはシンボリックリンク) されており,testという名前が本当のコマンド名です. したがってこのスクリプトはリスト*のように書くこともでき, むしろこちらが本来の書き方であるといえます.
if test -f /etc/sysconfig/network then . /etc/sysconfig/network else NETWORKING=no HOSTNAME=localhost fi |
このように,シェルは本来,自分自身ではファイルの存在判定や数値の比較などの 条件判断を行なうことができません.シェルは基本的には外部コマンドを呼び出し, コマンドの戻り値をif文やwhile文で参照することによって条件判断を行なうだけです. そこで,これらの条件判断を専用に行なうコマンドとして, testというコマンドが用意されているのです.
上記リストでの[ ]の中の-fオプションは, シェル本体ではなくtestコマンドに対するオプションです. この場合は/etc/sysconfig/networkというファイルが(通常ファイルとして) 存在すれば真になります. なお,コマンドの戻り値の真偽はC言語とは逆で,値が0ならば真となります.
testコマンドは,もともとは外部コマンドでしたが, シェルスクリプトのif文やwhile文から頻繁に呼び出されるため, 現在のBシェル系では組み込みコマンドになっています.
シェルスクリプトでも,C言語と同様に「&&」や「||」で条件演算が行なえます. たとえば,「command1 && command2」と書いた場合,まずcommand1が実行され, その戻り値が0であった場合に限りcommand2が実行されます. このように,「&&」の動作はif文に似ており, 実際にrcスクリプト中でif文の代わりに「&&」を使っている例もあります.
たとえば,前述のリストを「&&」を使って書くと下のリストのようになります. なお,ここではelseを使えないため,先に変数にデフォルト値を代入しています.
NETWORKING=no HOSTNAME=localhost [ -f /etc/sysconfig/network ] && . /etc/sysconfig/network |
case文はC言語のswitch文に相当するものです. caseの右側には,変数を記述したり,「` `」(バッククオート)で囲まれたコマンドを 記述したりします.case文はその文字列の内容によって条件分岐します. なお,case文の終了はif文と同様,caseの綴を逆順に並べたesacです.
case文は,たとえばSysV系のrcスクリプトで,引数のstart/stopを判断するために 使われています.
case "$1" in start) lpd ;; stop) …省略… ;; esac |
case文をC言語のswitch文と比較すると次のような違いがあります. C言語で「case 'a':」と書くところはシェルスクリプトでは「a)」となります. また,C言語での「break;」はシェルスクリプトでは「;;」となり, C言語とは違って場合分けされた箇所それぞれに必ず「;;」が必要です. C言語では「case 'a': case 'b':」と,caseを並べてOR条件を表現できますが, これはシェルスクリプトでは「a|b)」と書きます. 「)」の左側の文字列にはワイルドカードが使え, 「*)」と書くとC言語での「default:」の意味になります.
for文はcsh系でのforeach文に相当するもので, 「for 変数名 in 引数1 引数2 ...」のように記述すると, 引数1から順に変数に代入されて,ループを実行します. ループはdoとdoneで囲みます. なお,C言語と同様,ループ中からのbreakやcontinueも可能です.
for文はSysV系の分割されたrcスクリプトを起動する部分に使用されています. ここで,たとえば変数runlevelに3が代入されているとすると, /etc/rc.d/rc3.d以下のSで始まるファイル名のスクリプトが, ワイルドカードの展開順(つまりS00xxx,S10xxxなどの数字の順)に, シェル変数iに代入され,それがdo〜doneのループ中で startの引数をつけて順に実行されることになります.
for i in /etc/rc.d/rc$runlevel.d/S*; do $i start done |
while文はC言語のwhile文とほぼ同じで,ループ中からのbreakやcontinueも可能です. while文もif文と同様,whileの右には多くの場合testコマンド(つまり[ ])を 記述し,その戻り値が真(つまり0)である限りdo〜doneのループを実行します.
なお,whileで無限ループを作るには,教科書的には「while true」でよいのですが, trueが外部コマンドであるため,後述の組み込みコマンドの「:」を使い, 内部コマンドのみで「while :」とした方が賢いでしょう.
また,あまり使われていないようですが,whileとは条件判断が逆の untilという制御文もあります.
シェルの組み込みコマンドとして,すでに紹介したtestコマンド以外の主なものを いくつか紹介します.
Bシェル系をログインシェルにしている人には説明はいらないと思いますが, typeコマンドはcsh系のwhichコマンドに相当するものです. 「type コマンド名」と入力すると,現在のシェルにおけるそのコマンドのタイプ (組み込みコマンドか,外部コマンドかまたはシェル関数か)を表示します. 外部コマンドの場合はそのフルパスも表示されます. なお,DOSのtypeコマンドとは何の関係もありませんので, 混同しないようにして下さい.
これも説明の必要はないと思いますが,echoコマンドは引数に書かれた文字列を 標準出力に出します.echoコマンドは古いBシェルでは外部コマンドでしたが, 現在では組み込みコマンドになっています. ちなみに,標準エラー出力に出したい場合は,「echo message 1>&2」とします.
奇妙ですが,「.」(ピリオドひとつ)がシェルの組み込みコマンドです. これはcsh系のsourceコマンドに相当し,実行中のシェル上に ほかのシェルスクリプトを直接読み込むために使われます.
たとえば,シェル変数・環境変数やシェル関数などを設定したい場合, 別のシェルスクリプトを起動してしまったのでは,設定が呼び出し元に反映 されません.そこで「.」コマンドを使って, 動作中のシェル自体に結果を反映させるのです.
「.」コマンドは,前述の例のようにRedHat系Linuxで/etc/sysconfig/networkなどの ファイルを読み込んだり,FreeBSDで/etc/defaults/rc.confファイルを読み込んだり する場合に使われています.
さらに,「:」(コロンひとつ)というコマンドもあります. これは何もしないヌルコマンドで,「:」コマンドが書かれた行は 「#」でコメントアウトしたものと似ています.
しかし,「:」コマンドでは,その右側に記述された引数が 通常のコマンドと同様に解釈され,変数の展開や標準入出力のリダイレクトなどが 通常通り行なわれます.
たとえば,「: > file」と書くと,ファイルへのリダイレクトのみが行なわれ, その結果,ファイルを作成することができます. これはtouchコマンドに似ていますが,すでにファイルが存在していた場合, その内容は削除され,ファイルサイズがゼロになってしまうという点が違います.
ただしBシェルの場合は,この場合については「:」コマンド自体を省略でき, 単に「> file」とすることができます. この手法はrcスクリプト中で,「> /etc/mtab」として, mtabの内容をクリアするために使用されています.
exitコマンドはシェルを終了します. 「exit 0」のように引数に数字をつければ,その値をシェルスクリプトの戻り値として 親プロセスに返すことができます. exitコマンドの引数を省略した場合,またはexitコマンドなしでシェルスクリプトの 最終行に達した場合は,その直前に実行されたコマンドの戻り値が, そのままシェルスクリプトの戻り値として返されます.
シェルスクリプトの中からは,もちろんどのような外部コマンドでも呼び出せます. ただし,rcスクリプトが実行されるのはシステムの起動の途中であるため, 呼び出されるコマンドがその起動途中の段階において動作可能である必要があります. rcスクリプト中でよく使用される外部コマンドを以下にまとめます.
シェルスクリプト中で数値を扱う場合,たとえば変数aに数値が入っていて, その値に1を足したい場合は,exprを用いて次のようにします.
a=`expr $a + 1`
exprは計算結果を文字列として標準出力に出します. そこで,その文字列を「` `」(バッククオート)で受けて, 再び変数aに代入するという書き方が一般的です.
そのほか,シェルスクリプト中で文字列の処理を行ないたい場合, sedやawkを呼び出して処理させることがあります. この場合,「|」(パイプ)や「` `」(バッククオート)などを駆使した書き方が 行なわれているはずですので,注意深くスクリプトを読んでみてください.
なお,sedやawkではなくperlの方が慣れているという人も多いかも知れませんが, rcスクリプトのようなシステムの起動に関する部分にはperlは使いません. また,rcスクリプトに限らず,awkで書けるものはawkで,sedで書けるものはsedでと, 使い分けた方が,システムリソース的には有利です.
また,簡単な正規表現による文字列の切り出しを行なう場合には, 前述のexprも使えます.
さらに,詳しい説明は省略しますが, Bシェルのset(内部コマンド)やIFS変数(予約済みの変数)を使って, シェル自身にある程度の文字列処理をさせることもできます.
Bシェルではシェル関数を定義することができます. 定義されたシェル関数は,そのシェル内で通常のコマンドと同様に使うことができます. 引数も,通常のコマンドと同様に,$1, $2,... などで渡されます.
シェル関数を使って,csh系でのaliasと同様のこともできます.(※注) たとえば,llというシェル関数で「ls -l」を実行させたい場合は 次のようにしてシェル関数を定義します.
ll() { ls -l "$@" } |
シェル関数について,興味深い例をもうひとつ紹介します. Solarisの/etc/rcSの中では,shcat()というシェル関数が定義されており, これによってcatコマンドとほぼ同様の動作をさせています. このようなシェル関数を定義しているのは,Solarisではcatは/usr/bin以下にあり, rcSスクリプトの実行時点ではまだ/usrがマウントされていないためです.
そのほか,rcスクリプトを読んだり,調べたりする際に便利な基本コマンドを いくつか紹介します.
lessは,UNIXで古くから使われているmoreに似たページャプログラムで, 逆方向スクロールなどの拡張が施されています. lessは,テキストファイルの表示用に広く使われ, ファイルの表示のほか,パイプを通した標準入力の表示もできます. manコマンドでマニュアルを読む際にも,環境変数PAGERの設定により, lessが使用されるのが普通です. ただし,日本語を表示するためには,日本語化パッチの適用されたlessが必要で, FreeBSDでは日本語対応lessのファイル名がjlessと変更されているため, 環境変数PAGER=jlessと設定しておく必要があります.
ファイルの中から特定の文字列を含んだ行を検索するためにはgrepコマンドが よく使われます.grepは,基本的には「grep 文字列 ファイル名」とコマンド入力して 使います.grepと同様のことはsedを使ってもできますが,文字列検索に特化した コマンドとしてgrepが用意されているのす.
なお,検索する文字列や正規表現の扱いなどが異なる,egrepやfgrepという コマンドもあります.詳しくはmanコマンドにより確認して下さい.
また,GNUのgrepの場合は,-rオプションをつけて 「grep -r 文字列 ディレクトリ名」とコマンド入力することにより, サブディレクトリを含めすべてのファイルを検索することができるので便利です.
findは,引数で指定したディレクトリ以下から,特定の条件に合致したファイルを 捜し出すためのコマンドです.findの典型的な使用方法は,
find ディレクトリ名 -name ファイル名 -print
となります.この場合,-nameオプションで指定したファイル名が, 引数で指定したディレクトリ以下から捜し出されて表示されます.
ここで,ファイル名には「*」などのワイルドカードも使えます. ただしその場合は,ワイルドカードがfindコマンドに渡される前に 先にシェルによって展開されないように, 「' '」(シングルクオート)で囲む必要があります.
なお,GNU findなどの場合は-printオプションを省略できます. この場合,たとえば単に「find .」と入力すると, カレントディレクトリ以下のすべてのファイルのリストを表示できます.
findは,条件に合致したファイルに対して-execオプションでコマンドを実行したり, あるいは-printオプションでいったん標準出力に出してからそれをパイプを通して xargsコマンドで受けたりといった使い方ができます. ただし,findのコマンドライン文法については少々複雑ですので, 詳しくは「man find」を参照して下さい.
findコマンドは,実行のたびに指定されたディレクトリ以下を実際に探し回ります. これには,ハードディスクのアクセスなど,システム上の負荷がかなりかかり, その結果findコマンドの実行にも時間がかかります. そこで,なるべくシステムに負担をかけずにファイルを捜し出せるように locateというコマンドが用意されています.
locateは,ディレクトリの中を実際に検索するのではなく, システムによってあらかじめ作成されているデータベースを参照して 所定のファイルの位置を表示します. このため,locateはfindよりも実行速度が速く,システムの負担もかかりません. ただし,locateコマンドのコマンドライン文法は,findとは異なり, またlocateが正常に機能するためにはあらかじめデータベースが作成されている必要が あります.locate用のデータベース作成コマンドはupdatedbで, これはcronによって深夜に実行されるのが普通です.
manはもちろん,各種コマンドのマニュアルを表示するためのコマンドです. たとえば,「man man」とコマンド入力すれば, manコマンド自体のマニュアルが読めます. UNIX系OSであれば,インストールされているコマンドの使用方法は manコマンドで確認するのが基本です. すでによく知っているコマンドであっても,普段はあまり使用しない忘れがちな オプションもあるはずです. その意味でも,時々manコマンドを実行してマニュアルを読んでみるとよいでしょう.