bashで始めるシェルスクリプト基礎の基礎

第8回 bashで始めるシェルスクリプト基礎の基礎

GUIに対するCUIの優位性の1つとして、作業の自動化が挙げられる。普段行う作業を1つのコマンドにまとめたり、複数のファイルに対して同じ処理を繰り返し行ったりといったことが比較的簡単なのだ。WindowsにもWSH(Windows Script Host)が用意されている。しかし、Linuxのbashスクリプトの方が簡単なのだ。

シェルスクリプトの基本はコマンドを並べること

 コマンドによる作業を自動化するには、その内容を記述したテキストファイルを用意すればいいのです。このテキストファイルを「シェルスクリプト」といいます。

最も簡単な自動化

 最も簡単なシェルスクリプトは、コマンドをそのまま並べることです。例えば、tarコマンドでファイルのバックアップを取るとしましょう。単純にルートディレクトリから下を全部バックアップするにしても、/devや/tmpは必要ないですね。すると、

$ tar cf /dev/nst0 /bin
$ tar cf /dev/nst0 /boot
$ tar cf /dev/nst0 /etc
$ tar cf /dev/nst0 /home

といった具合になります()。

注:1つのコマンドでルートディレクトリ以下をまとめてバックアップすることもできます。ただ、ある程度の範囲で分けておいた方が、メディア不良などのダメージが及ぶ範囲を小さくできますし、目的のファイルを探すのも楽です。

 これをいちいちタイプするのは面倒です。そもそも、バックアップにはある程度の時間がかかるのが普通ですから、前のコマンドが終了するまで次のコマンドはタイプできません。これでは作業効率が上がらないでしょう。

 そこで、シェルスクリプトを作ってみましょう。sysbackup.sh()という名前のファイルを作り、

tar cf /dev/nst0 /bin
tar cf /dev/nst0 /boot
tar cf /dev/nst0 /etc
tar cf /dev/nst0 /home

上記のように実行するコマンドを並べるだけです。WSHだと、普段メニューとマウスで行っている作業をVB ScriptやJScriptに翻訳しながらプログラムを作る必要があります。ところが、シェルスクリプトは普段使っているコマンドを書くだけでいいのです。

注:ファイル名の「.sh」は、なくてもスクリプトの実行には関係ありません。「.sh」を付けなくてもよいのですが、そうなるとファイル名だけではCコンパイラなどを使って作ったバイナリの実行ファイルと区別できません。もちろん、ファイルサイズが全然違いますし、lessなどで中を見れば簡単に区別できます。余談ですが、これを見分けるためのプログラムも存在します。

$ file sysbackup.sh

としてみてください。

 実行するのも簡単です。

$ sh sysbackup.sh

とすれば、自動的に順次コマンドを実行します。その間にほかの仕事ができるし、就寝前に実行すれば、翌朝には終了しているでしょう。

もう少しコマンドらしく

 作成したシェルスクリプトを普通のプログラムと同じ方法で実行することもできますが、それには2つばかり細工が必要です。まず、シェルスクリプトの先頭に次の行を追加します。

#!/bin/sh

 これはお約束で、「#!」以後に書かれたプログラムでこのスクリプトを実行するという意味です。応用として「#!/bin/perl」「#!/bin/ruby」(編注)などもあります。

編注:この記述は環境によって異なる。多くのLinuxディストリビューションでは、「#!/bin/perl」「#!/bin/ruby」ではなく「#!/usr/bin/perl」「#!/usr/bin/ruby」になるだろう。perlやrubyのパスは、whichコマンドなどで調べられる。

 もう1つは、ファイルに実行属性を付けることです。chmodコマンドを使って、

$ chmod u+x sysbackup.sh

とします。この2つの作業を行えば、

$ ./sysbackup.sh

で実行できるようになります。

シェルスクリプトによる出力の制御

結果の保存

 シェルスクリプトをバイナリの実行ファイルと区別しないのは、入出力のリダイレクトでも同じです。つまり、

$ ./sysbackup.sh > log.txt

とすれば、出力結果をlog.txtというテキストファイルに保存できます。また、定期的に実行するのであれば、スクリプトの中でリダイレクトを指定することもできます。この場合、柔軟性がやや低下する点に注意が必要です。

 なお、普通にリダイレクトを指定すると、標準エラー出力への出力はリダイレクト先のファイルではなく、端末に表示されます。これは、エラーが起きたことをユーザーに知らせるためです。この標準エラー出力もリダイレクトするなら、

$ ./sysbackup.sh > log.txt 2> err.txt

とします。「2>」というのがミソで、これが標準エラー出力のリダイレクトの指示です。さらに、

$ ./sysbackup.sh > log.txt 2>&1

とすると、すべての出力がlog.txtに記録されます。

長い文字列を出力する

 シェルスクリプトで文字列を出力するには、普通echoコマンドを使って、

echo 'Hello, world'

などとします。しかし、ちょっと長い文字列を出力したいこともあるでしょう。また、HTMLファイルへの加工を行うなら、ヘッダなどを見やすい形で記述したいと思うでしょう。こんなときに便利なのが、「ヒアドキュメント」です。

 例えば、HTMLのスケルトンをechoコマンドで出力するなら、

#!/bin/sh
echo "<HTML>"
echo "<HEAD>"
echo "<TITLE></TITLE>"
echo "</HEAD>"
echo "<BODY>"
echo "<H1></H1>"
echo "<P>"
echo "</P>"
echo "</BODY>"
echo "</HTML>"

ですが、ヒアドキュメントを使うと、

#!/bin/sh
cat << EOS
<HTML>
<HEAD>
<TITLE></TITLE>
</HEAD>
<BODY>
<H1></H1>
<P>
</P>
</BODY>
</HTML>
EOS

となります。「<<」の後に指定した文字列が出現する直前まで、コマンドに対する標準入力として扱われます。上の例では、「EOS」を目印にしています。

 ヒアドキュメントを使うと、出力したい文字列をそのまま書けばいいので、スクリプトをすっきりと記述できます。後から文字を追加するのも簡単です。

引数と変数で柔軟性を実現

引数と変数

 いつも同じことを繰り返すシェルスクリプトだけでもかなりの省力化になりますが、処理対象を実行時に決めたいこともあります。シェルスクリプトはテキストファイルなので、そのたびに書き換えるのも1つの手です。しかし、処理対象の数が増えてくると面倒ですし、あまりスマートな方法ではありません。

 これを解決するため、シェルスクリプトに引数を渡すことができます。引数は、シェルスクリプトからは順番に$1、$2、$3、……として参照できます。引数の数は「$#」で分かります。また、「$*」とすることで、すべての引数を一度に参照できます。なお、「$0」はスクリプトが呼び出されたときの名前が入ります。

 例として、簡単なあいさつを行うシェルスクリプトで引数を試してみましょう。greeting.shという名前で、

#!/bin/sh
echo "Hi, $1. I am $0."
echo "Hi, $*. I am $0."

という内容のファイルを作ります。chmodコマンドで、直接実行できるようにしておいてください。以下はこのシェルスクリプトの実行例です。

$ ./greeting.sh tom
Hi, tom. I am ./greeting.sh.
Hi, tom. I am ./greeting.sh.
$ ./greeting.sh tom joe
Hi, tom. I am ./greeting.sh.
Hi, tom joe. I am ./greeting.sh.
$ sh greeting.sh tom joe
Hi, tom. I am greeting.sh.
Hi, tom joe. I am greeting.sh.

 実のところ、シェルスクリプトでは「$」で始まる文字列を変数として扱います。より正確にいうと、文字列が$で始まっている場合はその文字列に格納されたデータを取り出して置き換えます。ですから、変数にデータを代入するときは$が不要です。例えば、

test=one
echo $test

というシェルスクリプトを実行すると、

$ sh var.sh
one

となります。

 また、シェルスクリプトでは基本的に変数を文字列として扱います。つまり、

test=1
test=$test+1
echo $test

の実行結果は、

$ sh add.sh
1+1

となります。

 変数を数値として扱いたいときは、declareコマンドで指定します。

declare -i test
test=1
test=$test+1
echo $test

というシェルスクリプトを実行すると、

$ bash add2.sh
2

と、今度は整数演算を行った結果が返ってきます。「-i」で、整数値として処理することを指定するのです。

 なお、変数に何も代入されていない場合は、空の文字列が返ってきます。

変数に対するパターンマッチ

 前回、ファイルの拡張子を書き換える例を示したと思います。そこに使ったパターンマッチは4種類あり、非常に汎用性の高いものです。

 変数testpathに/home/sekino/Linux/how.to.linuxという値を設定しておくと、

$ echo ${testpath##/*/}
how.to.linux
$ echo ${testpath#/*/}
sekino/Linux/how.to.linux
$ echo ${testpath%%.*}
/home/sekino/Linux/how
$ echo ${testpath%.*}
/home/sekino/Linux/how.to

といった結果になります。また、ファイル拡張子を置き換えるという観点からは、

#!/bin/sh
file=scan01.jpg
echo ${file}
echo ${file%jpg}
echo ${file%jpg}png

というスクリプトを実行すると分かりやすいと思います。

条件式と繰り返しによるスクリプトの制御

条件判断

 引数によって実行時にパラメータを与えることができるようになりました。次に欲しくなるのは、条件判断でしょう。つまり、特定の条件が満たされたときだけコマンドを実行するということです。このための構文が「if文」で、

if 条件文
then
 実行文
elif 条件文
 実行文
else
 実行文
fi

という構造で使います。このうち「elif」はなくてもよいですし、好きなだけ繰り返すこともできます。また、「else」は使わない、あるいは1回だけ使えます。

 ここで注意が必要なのは、条件が式ではなく文であることです。すなわち、一般的な真偽によって実行するかしないかを決定するのではないのです。では一体何をもって判断するのかというと、実行した文の「終了ステータス」です。

 Linuxでは、あらゆる実行ファイルが終了時に自分自身を呼び出したプロセスに対して整数のコードを返します。これが終了ステータスで、普通は正常に終了したときに「0」を、エラーが発生した場合などはそれに応じた数値を返します。そして、この終了ステータスが0であることが、すなわち真ということになります。言い換えれば、条件文が正常に実行を終了すれば、真であるということです。従って、

if コマンドが正常に終了した
then
 通常の処理
else
 エラー処理
fi

という言い回しが成立します。

 これでは一般的な条件判断を行うのが難しくなります。そこで、testコマンドが用意されました。このコマンドは、続く条件式を評価して、真ならば0を、偽ならば1を終了ステータスとして返します。このままではちょっと分かりにくいので、「[]」として使えるようになっています。

if [ $# -eq 1 ]
then
        echo one.
elif [ $# -eq 2 ]
then
        echo two.
elif [ $# -eq 3 ]
then
        echo three.
else
        echo many.
fi

というシェルスクリプトなら、引数の数に応じた答えを返します。

 testコマンドで使える条件式は、manコマンドで調べられます。代表的なところでは、

 
条件式
意味
  s1 = s2 文字列s1とs2が等しい
  s1 != s2 文字列s1とs2が等しくない
  n1 -eq n2 数値n1とn2が等しい
  n1 -ne n2 数値n1とn2が等しくない
  -e file fileが存在する
  -z s1 s1の長さが0である

といったものがあります。

すべてのファイルに同じ処理を

 シェルスクリプトの中で私が一番重宝しているのは、「forループ」です。CやPascalといったプログラミング言語にもforループは用意されていますが、シェルスクリプトのforループはちょっと違います。CやPascalでは特定回数のループを実行するのに使われますが、シェルスクリプトでは複数のファイルに対して同じ処理を行うためにあるといっていいでしょう。どちらかというと、オブジェクト指向言語でいうイテレータに近い存在です。

 構文は、

for 識別子 in リスト
do
 $識別子を使う文
done

です。これで、リストの内容を1つずつ識別子に代入して、それぞれについてdoとdoneで囲まれた部分を実行します。

 簡単な例で試してみましょう。以下の内容のスクリプトをgreeting2.shとして作成します。

#!/bin/sh
echo hi $*
for name in $*
do
        echo hi $name
done

 これに3つの引数を与えて実行すると、

$ ./greeting2.sh tom joe mike
hi tom joe mike
hi tom
hi joe
hi mike

となります。単純な$*とforループの違いが分かるでしょうか?

 実際に私が使っているスクリプトに、

#!/bin/sh
echo "<HTML>"
echo "<HEAD>"
echo "<TITLE></TITLE>"
echo "</HEAD>"
echo "<BODY>"
echo "<H1></H1>"
echo "<P>"
echo "</P>"
for file in *.jpg
do
 convert -size 20%x20% $file ${file%jpg}png
 echo \<A HREF="$file"\>\<IMG src=\"${file%jpg}png\" alt=\"$file\" hspace=2\>\</A\>
done
echo "</BODY>"
echo "</HTML>"

というものがあります。カレントディレクトリにあるJPGファイルすべてのサムネイルを作り、サムネイル一覧と元のファイルへのリンクを持ったHTMLファイルを出力します。肝はforループで、ループ内の1行目で縦横を20%に縮小したPNGファイルを作り、これをサムネイルとします。ループ内2行目では「<A HREF="scan01.jpg"><IMG SRC="scan01.png" alt="scan01.jpg" hspace="2"></A>」といった行を作って出力します。これを、JPGファイルの数だけ繰り返すわけです。

特定条件での繰り返し

 特定の条件が成立している間、繰り返し処理を行うという場合は、whileかuntilを使います。構文としては、

while 条件文
do
 実行文
done

となり、条件文についてはforループと同じ扱いになります。終了ステータスが0なら真、0以外なら偽ということです。この点を除けば、CやPascalのwhileループに似ています。

 簡単な例として、PATHに設定されている文字列を分解してみましょう。シェルスクリプトは、

#!/bin/sh
path=$PATH:
while [ $path ]
do
        echo ${path%%:*}
        path=${path#*:}
done

で、実行例は、

$ echo $PATH
/usr/local/bin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/sbin:
/usr/X11R6/bin:/home/tom-a/bin:/usr/local/pgsql/bin:/usr/interbase/bin
$ ./split.sh
/usr/local/bin
/bin
/sbin
/usr/bin
/usr/sbin
/usr/local/sbin
/usr/X11R6/bin
/home/tom-a/bin
/usr/local/pgsql/bin
/usr/interbase/bin

です。

手抜きのススメ

 シェルスクリプトは制御構造を持っており、かなり高度な作業が行えます。その分、理解して使うにはちょっと苦労するかもしれません。しかし、最初に苦労してしまえば後でずっと楽ができます。また、分からない機能を無理して使わなくても、繰り返し利用するコマンドをまとめるだけでも、ずいぶん省力化できます。ぜひシェルスクリプトを活用して、楽をしましょう。