シェルスクリプトの基礎

目次

シェルスクリプトは運用・管理で利活用すると非常に便利ですが、その反面root権限で誤ったシェルスクリプトを走らせてしまうと、システムを破壊してしまう恐れもあります。ここで紹介している一切の構文・サンプル文は自己責任でご参考下さい。

はじめに
クオーテーション
標準入出力

シェルスクリプト中の変数定義

制御文の構造

制御文の条件

シェルスクリプトでループ
コマンドの連結
エイリアス と関数
Perlとの連動
シェルスクリプト構文集
戻る

はじめに

UNIXはMS-Windowsと違い拡張子でファイル形式を判別しません。ファイルの中身の先頭部分で判断します。つまりシェルスクリプトであると認識させるためにはファイルの先頭に、/bin/shと記述します。perlと認識されるには、/usr/bin/perlと記述します。これらはWindowsファイルの.txt(ドットテキスト)や.csv(ドットCSV)の様にそのファイルをどの様に動かすのか、どの様に読み込むのかを意味しています。/bin/sh と記述してあればそれは/bin/sh を呼び出して利用する、という事なのです。

では、シェルスクリプトの中でもなぜzshやcsh を使うスクリプトが少ないのでしょうか。その原因は、動作の共通点が少ないところにあります。文法上エラーになる可能性があるためです。また/bin/shのスクリプトが一番書きやすいのです。ちなみにLinuxの場合、/bin/shはbashのことです。SystemV系UNIXの場合はBorneシェルのことです。

クオーテーション

perl等の文字列を処理するプログラムでは頻繁に利用されている 「 " 」 などは、文字を宣言するものです。たとえば

$100

これを100ドルとして使いたい場合、コンピューターは正しく理解してくれません。なぜならメタキャラクタの「$」が付いているからです。$100は「100ドル」ではなく「変数100」として認識してしまうのです。それを「 $ 」も単なる文字と認識させる為に

'$100'

の様にクオーテーションでくくります。あるいはバックスラッシュ\を使って

\$100

の様な方法もあります。「 \ 」はその直後を「文字」と宣言するものです。シングルクオート「 ' 」は全てのワイルドカード「 $ ? 」バックスラッシュ「 \ 」の働きを抑えます。絶対的に文字列と宣言します。これに対して 注意しなくてはいけないのがダブルクオート「 " 」です。ダブルクオートは「 $ ? \ 」の働きは抑える事ができません。つまり

"$100"

は、「 $100 」と変わりありません。使い慣れるまで注意が必要です。しかし、スペースを含んだ文字列の場合などはこのダブルクオートが活躍するのです。

標準入出力

Bashのファイルディスクリプタと言うものを説明します。これらは0〜9までの番号でファイルの入出力を操作する事です。もっともよく使う0〜2を例にあげると次の様になります。

$ ls -l > hoge

これは もうご存知のとうりlsの出力をhogeというファイルに書き込む事です。しかしこれは ファイルディスクリプタを省略した書き方なのです。本当は

$ ls -l 1> hoge

が正しいのです。ファイルディスクリプタは0と1は よく使う事から特別に省略が可能になっています。では もう一つの例をあげてみます。

$ ls -l < hoge

今度はhogeに書き込まれたテキストをls が受け取り表示をするものです。これもまた省略された記述です。本当は

$ ls -l <0 hoge

なのです。

ただこれが シェルスクリプトにおいては無視できません。ファイルディスクリプタをまとめてみました。

意味
0 標準入力 ( キーボード入力 )
1 標準出力 (ディスプレイ出力 )
2 エラー出力 ( エラーのディスプレイ出力 )

エラー出力(値2)の説明がまだでした。それでは説明します。例えばhogegeというファイルが存在しない場合にcatを使うともちろんエラーメッセージが出力されます、次のように。

$ cat hogege > hoge
cat: hogege をオープンできません。

hogegeというファイルが存在した場合はhogegeファイルの内容をhogeに上書きできます。次の様に

$ cat hogege 1> hoge

で エラーメッセージは出力されません。あとでhogeの内容をみてみると hogegeになっているはずなのです。では、hogegeが存在しない初期条件に話を戻します。ここで値1を値を2に変えてみます。

$ cat hogege 2> hoge

これだと hogegeは存在しないにも関わらずエラーメッセージは出ないのです。ではエラーメッセージはどこに行ったかというと、それはhogeの中に書き込まれています。値2はこの様にエラーメッセージを出力する能力もっています。厳密にいうと標準出力をエラー出力に変えた事になります。

しかし 一般のUNIXコマンドでつかわれるcsh系ではエラー出力だけを先ほどの様に取り扱うことはできません。ですから次の様にしてエラーと標準を別 々の場所に出力させます。

$ (cat hogege > hoge1) >& err

">&"が"2>" の変わりと思っても良いです。しかし標準出力も書き込みます。 ()については「コマンドの連結」で詳しく解説したいとおもいます。

$ cat hogege 2>&1 hoge

この記述はログ採取などでよく見かけるでしょう。 >&は出力を切り替えます。よってエラー出力を標準出力にかえることになります。
なるべく分かりやすいように解説したつもりですが わからなければ試してみてください。

シェルスクリプト中の変数定義

変数のお決まりとして幾つか紹介しておきます。基本的に変数は大文字を使います。スペースは使えませんのでアンダーバー( _ )を使います。代入する場合イコール( = )を使いますが スペースを入れてはいけません。変数の定義として変数の前に「$」を付けます。変数で使える文字は英数字とアンダーバー( _ )です。例えば

# VARUE=hogehoge
# echo $VARULE
hogehoge

この様に echoで呼び出しました。ここでプロンプトの$と変数の$は別 である事に注意して下さい。また変数の後に別の文字列を続ける場合は区別 する為に{}でくくります。例えば

# VARUE=hogehoge01
# echo ${VARULE}hogehoge02
hogehoge01hogehoge02

この様に変数VARULEと文字列hogehoge02は区別されています。この時スラッシュ( / )やドット( . )の様に変数として使えない文字で仕切られていると{}は必要ありません。

# VARUE=hogehoge01
# echo $VARULE/hogehoge02
hogehoge01hogehoge02

# VARUE=hogehoge01
# echo ${VARULE}.c
hogehoge01.c

つまり 変数で使えない文字が入った時点でそこから後ろは変数でなくなります。ただ「 @ # * ? $ ! - 」や一桁の数字を区切り代わりに使ってはいけません。別 の意味が出てきますので注意しておいてください。

制御文の構造

プログラムでもお馴染みなので ここではシェルスクリプトらしい部分を紹介します。

<if>
if 条件文
then
   条件文と一致した場合の処理
else
   条件文と一致しなかった場合の処理
fi ←if文の終了

<for>
for 変数 in リスト
do
リストの項目を1つずつ変数に代入して処理
done ←for文の終了

<while>
while 条件文
do
   条件文と一致した時の処理
done

<case>
case 文字列 in
   パターン1) パターン1に一致した時の処理 ;;
   パターン2) パターン2に一致した時の処理 ;;
esac ←case文の終了

一般のプログラム制御文と違い fi やesac など制御文を逆に綴った終了命令があります。

制御文の条件

制御文の条件の記述も他のプログラムとは違います。その中でも間違いやすい条件文をいくつか紹介します。

if文は、条件が
真の場合thenに、偽の場合はelseに動作が分岐します。下のスクリプトでは$boxが空ならifは偽を返してelseに飛ばします。変数$boxの中に何かが入っていれば真を返します。
if [ $box ]
then
echo "$box"
変数boxの中に何かが入っている場合
else
echo "box is empty"
変数boxの中が空の場合
fi

代入の場合は number=1としました。等しい意味を持つ=(イコール)を使う場合は両サイドにスペースを入れて下さい。 もしくは-eqを使って下さい。
if [ $number = 1 ]
then
echo "eq"
変数numberが1の場合 (等しい場合)
else
echo "not"
変数numberが1ではない場合(不等の場合)

fi

その他の数値に関する条件をまとめました。

A = B 値Aと値Bが等しい場合、真をかえす
A -eq B 値Aと値Bが等しい場合、真をかえす 
A -ne B 値Aと値Bが異なる場合、真をかえす
A -gt B 値Aが値Bより大きい場合、真をかえす
A -ge B 値Aが値B以上である場合、真をかえす
A -lt B 値Aが値Bより小さい場合、真をかえす
A -le B 値Aが値B以下である場合、真をかえす

次の条件は /tmp/hogeファイルが存在する場合としない場合で動作を分ける条件判定です。
if [ -f /tmp/hoge ]
then
:
else
echo "hoge file is nothing"
exit
fi
これはif [ `ls /tmp/hoge` ]と同じことです。

その他のtestコマンドオプションをまとめました。

-b ブロックデバイスファイルであれば真
-c キャラクタデバイスファイルであれば真 (rawデバイスファイルであれば真)
-d ディレクトリであれば真
-e ファイル存在すれば真
-f ファイルが普通のファイルなら真
-L シンボリックリンクファイルなら真
-r 読み取り可能ファイルであれば真
-s ファイルサイズが0でなければ真
-w 書き込み可能ファイルであれば真
-x ファイルが実行可能であれば真

シェルスクリプトでループ

1から100までの数値を順に変数iに入れて順に出力する場合Cシェルでは、このように書くことができます。

#!/bin/csh -f
@ i = 1
while ($i <= 100)
echo $i
@ i ++
end

このスクリプトはどこにでもある例文なのですが 問題はBourneシェルで書くにはどうすれば良いかなのです。Bourneシェルで書くとこうなります。

#!/bin/sh
i=1
while [ $i -le 100 ]; do
echo $i
i=`expr $i + 1`
done

Bourne シェルは、算術演算の機能を自分では持っていません。ここでは、それを実現するために、testとexprという外部コマンドを呼び出しています。testの方は見つけにくいかもしれませんね。これは3行目の `[' です。`[' は testコマンドの省略形です。ループ変数が100以下かどうかの判定はtestコマンドに、ループ変数のインクリメントは exprコマンドに肩代わりしてもらっているというわけです。

しかし細かな部分で注意が必要です。以下にまとめました。

2行目のi=1は、スペースを入れて書いてはいけない。
3行目のwhile, [, $i, -le, 100 の後のスペースは入れて書かなければならない。
5行目のi=`は、スペースを入れて書いてはならない。 exprの後の3つの要素間のスペースは入れて書かなければならない。
2行目のiに、 `$' を付けてはならない。 5行目の1番目のiに `$' を付けてはならない。 5行目の$iの `$' を忘れてはならない。
このような問題は、何もBourneシェルだけのものではありません。上に挙げたCシェルスクリプトは、見やすさの上では若干有利ですが、 @ i = 1などのスペースはやはり省略できません。

ではforを使うとどんな記述になるのでしょうか。一般 に使われるシェルのforは以下の様なものですよね。

#!/bin/sh
for i in 1 2 3 4 5; do
echo 今 $i 回目
done

しかし in 以降の数値はひとつひとつの動作に対して記述しなければなりません。

ではforでのループを考えていきましょう。その前に次のPerlスクリプトを見てください。

#!/usr/bin/perl
for($i=1; $i<=5; $i++) {
print "今 $i 回目\n";
}

Perlのことを全く知らなくても、このコードは簡単に理解できるでしょう。 C との違いは、main()がないこと、変数の宣言がないこと、変数 の頭に$が付いていることくらいでしょうか。え、printfじゃないのかって? 大丈夫、ちゃんと printfも用意されています。

このスクリプトをloop5という名前で保存してからコマンドラインでperl loop5と入力すると、結果が見られます。実行属性を与えてから単にloop5としても同じです。

ここで、もう一つ覚えてください。それは、Perlはスクリプトを直接引数に書くことができる、ということです。このためには、-eというオプションを使います。上述したPerlスクリプトを実行するのと同じことを、コマンドラインから次のように指示できます。

perl -e 'for($i=1;$i<=5;$i++){print"今 $i 回目\n"}'
Perlは自由なフォーマットでプログラムが書けますから、このように1行で書いたりスペースや改行を省いたりしても問題ありません。

さて、本題に戻りましょう。Bourneシェルのfor文では、制御変数の値を inの後ろに並べて書くのでしたね。1から100までの数字を手で入力するのはとても大変です。そこで、この部分はPerlにやってもらうことにしましょう。次が最終形です。


#!/bin/sh
for i in `perl -e 'for($j=1;$j<=100;$j++){print "$j "}'`; do
echo $i
done


このシェルスクリプトの最大のミソは、バッククオート(`)です。一対のバッ ククオートで囲まれたコマンド列はシェルによっていったん実行され、その結果 得られる出力がこの部分と置き換わります。つまり、このスクリプトの意味 するところは次のものと全く同じになります。

(補足: 最終形に出てきたPerlの1行スクリプトでは、制御変数に $iではなく$jを使っています。これは、シェル変数iと見間違えないように変えておいただけで、 $iにしても問題なく動作します。)

コマンドの連結

よりスクリプトを簡潔に書くためにコマンドの連結というものがあります。ここで紹介する連結は普段のシェルコマンドでも利用できます。

;
A ; B ; C . . . . コマンドA B C . . . を順番に前から実行します。スペースはあってもなくてもかまいません。
(例) find / -name hoge.tar.gz -print ; echo '^G'
"hoge.tar.gz"を"/"から探した後で echo '^G'を実行します。'^G'はビブー音を鳴らす特殊文字ですから cshなら「C-g」(Controlキーを押しながらg) tcshなら「C-v C-g」と入力してください。検索が終わればビブー音が鳴ります。if文を使うと見つかった時だけビブー音を鳴らすような新しいfindコマンドも作れますよね。

&
A & B & C . . . . コマンドA B C . . .を並列に実行します。スペースはあってもなくてもかまいません。
(例)cat README & make
READMEファイルを読んでる間にmakeします。

>&
A >& File コマンドAの結果をFileに出力します。エラーも出力(書き込み)されます。
(例)make >& make.log & tail -f make.log
make中の出力をmake.logに書き込みながら tailでそれを画面に出力します。

&&
A && B && C . . . . コマンドAの実行が成功したらコマンドBに移ります。コマンドBが成功したらコマンドCに移ります。
(例)./configure && make && make install
./configureが成功すればmakeします。makeが成功すればmake installを実行します。これはセットで良く使いますよね。

||
A || B || C . . . . コマンドAの実行が失敗すればコマンドBが実行されます。コマンドBの実行が失敗したらコマンドCに移ります。
(例)make >& make.log || cat make.log
makeの実行が失敗した時に 出力しておいたmake.logを開きます。

()
(A B). . . . コマンドAとコマンドBをグループ化します。
(例)(find / -name hoge.tar.gz -print ; echo '^G') &
始めの例文を全てバックグラウンド処理(並列処理)します。
(例)(make && echo 'make success!') || echo 'make failed!' | mail root
makeの結果成功したら'make success!' 失敗したら'make failed!'と出力し それをパイプでrootユーザーにmailを送ります。
(例)( sleep 1 ; echo username; sleep 1 ; echo password; sleep 1 ; echo QUIT ) | telnet warp
username/passwordをtelnetコマンドに渡してログインし、QUITで切断します。

``
`A`....コマンドの実行結果を変数にする事ができます。
(例)time=`date`
dateコマンドをtime変数に格納しました。取り出す時は echo $timeで出力します。

エイリアス と関数

aliasとは別名の事ですから"ls -a"を"la"のような自分の分かりやすい「別 名」を付ける 時に利用します。 物理や数学で言うと a=加速度 m=質量 として 計算式に使いやすい文字に置き変えることも言わばaliasでしょう。シェルの場合での定義は

$ alias la='ls -a'

と記述します。こうするとlaコマンドが出来上がりです。新しい"la"コマンドは"ls -a"を意味するようになります。またこの"la"を使ってさらに新しいaliasを作る事もできます。つまり

$ alias laf='la -F'

とすると "laf"コマンドは [la -F] = [(ls -a) -F] = ls -a -F という事になります。「ちょっとまて!自分の環境じゃ上手くいかないぞ!?」と言われる方もいるとおもいます。それは csh(tcsh)だからではないですか? 言い忘れていましが、csh(tcsh)では基本的に =(イコール)は要らないのです。 これは覚えておいて下さい。あらゆる代入する場合の=(イコール)は 要りません。ですからcsh(tcsh)の場合は 次の様になります。

$ alias la 'ls -a'
$ alias laf 'la -F

これで上手くいくはずです。始めにcshの方を紹介すると=(イコール)が無いので見にくいですよね。ですからcshの場合の解説を後にしました。
では cshに話題を戻して例題です。csh環境で次のようなaliasを張りました。

$ alias ls 'ls -a'
$ alias ls 'ls -F'

この後で "ls"を実行するとどうなるのでしょうか?

この例題のポイントは再起が使えるのかどうかと言うところです。 2行目の'ls -F'は1行目の'ls -a'なのか、それとも原形の'ls'なのか・・・。
答えは 再起が使われます。つまり ls -a -F になります。
では 再起させたくない場合はいったいどうするのでしょうか。一度aliasをはったモノをキャンセルすか、または1行目の"ls -a"が 有効になっていますので2行目の"ls -F"は原形のコマンドを使うと宣言しなければなりません。

$ alias ls '/ls -F'


こうすることにより "/"の後ろにあるコマンドは原形のコマンドを使う事を宣言できます。
これは 重要なところで使います。例えば

$ alias cd 'ls; cd'

と、すると"ls;"の後ろに加えられた "cd"は "ls; cd"になっているわけです。その中の"cd"も'ls; cd'になっているわけですから無限ループになってしまうわけです。(やったこと無いので どうなるか分かりません)
こういう場合は必ず先程の"/"を使い ls; cd のcdは原形のcdを使うことを宣言しなければなりません。

$ alias cd 'ls; /cd'

と、する必要があります。これはcshだけではなくスクリプトのshでも同じです。

Perlとの連動

最後に、Perlとシェルの連動について紹介したいと思います。Perlの解説はその専門のサイトを参照して下さい。

<シェルを動作する関数>
cgiなどでシェルスクリプトを呼び出すにはsystem関数とexec関数を使います。

system '[シェルコマンド]', "[第1引数]", "[第2引数]", "[第3引数]";
exec '[シェルコマンド]', "[第1引数]", "[第2引数]", "[第3引数]";

<並列処理>
シェルスクリプトの動作時間が長い場合はそれをバックグラウンドで処理する
と便利です。
$| = 1;
[動作させる関数]
unless (fork) {
close (STDOUT) ;
[バックグラウンドで動作させる関数]
}

例えば下のスクリプトを実行するとpermanent.shが$kankaku変数を引数にして常駐している間、「読み込み中」ページがブラウザに表示されます。「強制終了」ボタンでkill.cgiに飛べばpermanent.shをkillできます。kill.cgiは pkill ./permanent.shなどが書かれているものとします。
(kill.cgiの例:
kill `/usr/bin/ps -e | /usr/bin/grep permanent.sh | /usr/bin/sed -e 's/^ *//' -e 's/ .*//'` )

$| = 1;
print <<"hoge";
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
<title>読み込み中</title>
</head>
<body>
<form name="form1" method="post" action="./kill.cgi">
<input type="submit" name="Submit" value="強制終了">
</form>
</div>
</body>
</html>
hoge

unless (fork) {
close (STDOUT) ;
system './permanent.sh', "$kankaku";
}

<乱数>
パスワード自動生成は、シェルにはできない文字列処理が必要です。そういった場合は一度Perlに作業を渡して結果だけを受け取るようにします。次の例はASCIIコードから数字と大文字小文字からなる6桁のランダムな文字列を生成します。
#! /usr/bin/perl
my ($result,$intval)=('','');
srand(time);
while ( length($result) <= 5 )
{
$intval = int( rand(75) ) + 48;
next if ($intval >= 91 and $intval <= 96 )
or ($intval >= 58 and $intval <= 64);
$result .= sprintf("%c", $intval);
}
print $result, "¥n";

<正規表現>
Perlは正規表現を使う場合にとても役立ちます。下記は引数にとったファイルを目的の正規表現にかけて、指定したファイルに出力するサンプルです。
#!/usr/bin/perl
$file = $ARGV[0];
print "file -> $file\n";
open(FILE2,">[出力ファイル]");
open(FILE,"$file");
@all = <FILE>;
foreach(@all){
$_ =~ [目的の正規表現];
print FILE2"$_";
print "a -> $_¥n";
}
close(FILE2);
close(FILE);

次の例では各OSの改行コードをUNIX改行コード\nに置換して、hogeファイルに出力します。FTPクライアントがASCIIモードでUploadやDownloadすると、クライアントOSの改行コードが追加されます。その様な場合はこのスクリプトでサーバー側での不具合を修正できます。
#!/usr/bin/perl
$file = $ARGV[0];
print "file -> $file\n";
open(FILE2,">hoge");
open(FILE,"$file");
@all = <FILE>;
foreach(@all){
$_ =~ s/\r\n/\n/g;
$_ =~ s/\r/\n/g;
print FILE2"$_";
print "a -> $_¥n";
}
close(FILE2);
close(FILE);

サンプル集

シェルスクリプトに使う構文を紹介します。単純な構文があればなにかと役に立つと思いますので、これから更新を重ねていきます。

<オプション>
オプションabcをつける事ができ、オプションabcそれぞれによって場合分けします。
getopts 'abc' opt
case $opt in
a) echo "option a" ;;
b) echo "option b" ;;
c) echo "option c" ;;
*) echo "usage: ls [-abc]" ;;
esac


<オプション>
オプションabcxをつける事ができ、オプションxに限り引数を持つ事ができます。引数はシェル変数OPTARGに格納されます。
getopts 'abcx:' opt
case $opt in
a) echo "option a" ;;
b) echo "option b" ;;
c) echo "option c" ;;
x) echo "option x $OPTARG" ;;
*) echo "usage: ls [-abc]" ;;
esac

<対話式>
printfは改行が含まれませんので、対話式プロンプトに適しています。readによって入力が変数verify1に格納されます。
printf "Please answer [y/n]: "
read verify1
case $verify1 in
y)
echo "Selection y";;
n)
echo "Selection n";;
*)
echo "Error! Please answer with y or n."
exit;;
esac

 


<関連書籍の購入>
入門csh & tcsh
sed&awkプログラミング

入門bash

<戻る>