シェルスクリプトことはじめ

Windows に代表されるGUIに対してunix系OSを操作する場合のCUIの優位性の1 つとして、作業の自動化が挙げられます。つまり、普段行う作業を1 つのコマンドにまとめたり、複数のファイルに対して同じ処理を繰り返し行っ たりといったことが比較的簡単に実現できるわけです。その記述がすなわち シェルスクリプトです。WindowsにもWSH (Windows Script Host) が用意されていますが、unixのシェルスクリプトの方が簡単に扱えます。とは いえ、シェルスクリプトはプログラミング言語の基本的要件をそなえているの で、処理が重いことを除けばかなりいろんなことが実現できます。
ちなみにこの「処理が重い」ところを課題として、スクリプトにとどまらずプ ログラム言語として発展していったのが perl/php/... です。
ここでは入門として、シェルスクリプトとはどんなものかを概観します。 ちなみにシェルには大きくわけて (bourne) sh 系と csh 系の2系統があり、 引数(パラメータ)を含む変数の扱いかた等、sh 系と csh 系ではかなり構文 (文法)が違います。csh は(対話的に使うには便利なのですが)スクリプト に向いていないということもあり、この文書では主に基本的な sh 系の構文で 説明します。
liweb ではログインシェルが csh (厳密にはtcsh) なので注意が必要です。

シェルスクリプトとは

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

最も簡単な自動化

最も簡単なシェルスクリプトは、コマンドをそのまま並べたものです。例えば、 いまいるディレクトリのファイルのバックアップを取るとしましょう。バック アップ先はホームディレクトリの下に .backup というディレクトリをつくって、 そこにおくものとします。ここで、tar コマンドというのを使うと DOS/Windows の zip や lha のように1つのかたまりとして保存でき便利なの で、それを使うとすると、
$ mkdir $HOME/.backup
$ tar cfz $HOME/.backup/backup.tar.gz *
となります。
ちなみにオプションの意味は、 で、保存したファイルを取りだすのは c のかわりに x (eXtract) オプション を使うのですが、詳細は略します。また、ファイル名の最後 (suffix) は Windows の lzh や zip のように決まっているわけでは無いですが、慣習とし て tar.gz か tgz にしておくと後でみてすぐわかるので便利でしょう。

あと、実は .backup というディレクトリ名にしておくと、 * (wildcard) にマッチしない、ということなのですが詳細は略します。

これくらいならたかがしれていますが、もっと複雑になってくると、いちいち タイプするのは面倒です。そもそも、バックアップにはある程度の時間がかか る可能性もあるので、前のコマンドが終了するまで次のコマンドを入力できま せん。これでは作業効率が上がらない場合がありそうです。

そこで、シェルスクリプトを作ります。

vi でもいいのですが、初学者に厳しいです。 liweb には nano というエディタがありますので、それを使ってください。
[maechan@liweb ~]$ nano backup.sh
のようにコマンドとして打ち込みます。ここで backup.sh は編集したいファ イル名です。元々なければ新たにつくられますし、元からあるものであれば、 編集モードとして起動します。
ここでは、backup.shという名前のファイルを作ります。そのファイルの中味は、
mkdir $HOME/.backup
tar cfz $HOME/.backup/backup.tar.gz *
のように実行するコマンドを並べるだけです。WSHだと、普段メニュー とマウスで行っている作業をVB ScriptやJScriptに翻訳しながらプログラムを 作る必要があります。ところが、シェルスクリプトは普段使っているコマンド を書くだけでいいのです。

ちなみにファイル名の最後の .shは、無くてもスクリプトの実行には関係 ありません。これは DOS/Windows で実行ファイルは .exe でなければな らない、等といった制約があるのとは大違いです。.shを付けなくてもよいの ですが、そうなるとファイル名だけではC コンパイラなどを使って作ったバイ ナリの実行ファイルと区別できないので、最初のうちはつけておいたほうがい いでしょう。もちろん、ファイルサイズが全然違いますし、中を見れば簡単に 区別できますし、これを見分けるための "file" コマンドも存在します。

実行

$ sh backup.sh
とすれば、あとは勝手に順番にコマンドを実行します。その間にほかの仕 事ができるわけです。でも、折角つくったスクリプトはそれ自体を1つの コマンドのように扱いたいわけで、実は作成したシェルスクリプトを普通 のプログラムと同じ方法で実行することもできます。但しその場合にはい くつか準備が必要です。
  1. まず、シェルスクリプトの先頭に次の行を追加します。

    #!/bin/sh
    
    これはお約束で、「#!」以後に書かれたプログラムでこのスクリプトを実行す るという意味です。あと「#!/bin/perl」「#!/bin/ruby」などもあります。と はいえこの記述は環境によって異なります。というのは#! で指定するのはそ のスクリプトのインタープリタなのですが、そのパス(=それがどの場所にあ るか)はシステムによって微妙にことなるからです。例えば多くのLinux ディ ストリビューションでは、「#!/bin/perl」「#!/bin/ruby」ではなく 「#!/usr/bin/perl」「#!/usr/bin/ruby」になりますし、perl をソースから 作ってインストールした場合には /usr/local/bin/perl である可能性が高い です。ちなみにperlやrubyのパスは whichコマンド等で調べられます。

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

    $ chmod +x ./backup.sh
    
    とします。この2つの作業を行えば、

    $ ./backup.sh
    
    で実行できるようになります。もしパスがきれていれば($path に含まれ ているディレクトリにおいておけば、あるいはカレントディレクトリを含 んでおけば)"./" は不要で backup.sh だけで実行できます。

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

結果の保存

シェルスクリプトでも、当然入出力のリダイレクトやパイプをつかえます。つまり、

$ ./backup.sh > log.txt
とすれば、出力結果を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 "Hello, $1. This is $0."
echo "Hello, $*. This is $0."
という内容のファイルを作って chmodコマンドで、直接実行できるようにしま す。このシェルスクリプトを実行すると以下のようになります。
$ ./greeting.sh maechan
Hi, maechan. I am ./greeting.sh.
Hi, maechan. I am ./greeting.sh.
$ ./greeting.sh maeda toshi
Hi, maeda. I am ./greeting.sh.
Hi, maeda toshi. I am ./greeting.sh.
$ sh greeting.sh maechan toshi
Hi, maeda. I am greeting.sh.
Hi, maeda toshi. I am greeting.sh.

変数

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

test=one
echo $test

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

$ sh test1.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」で、test という変数を整数値として処理することを指定できるのです。ちな みに変数に何も代入されていない場合は、空の文字列が返ってきます。

ちなみに、sh ではできませんが bash の場合は

test=1
test=$[test+1]
echo $test
と [] で囲むことで、その式を数値として処理することを指定できます。

変数に対するパターンマッチは主に4種類あり、非常に汎用性の高いものです。

たとえば変数 aaa に/home/maechan/script.sh という値を設定しておくと、

aaa=/home/maechan/my.script.sh
echo ${aaa##/*/}
echo ${aaa#/*/}
echo ${aaa%%.*}
echo ${aaa%.*}

というスクリプトを実行すると

my.script.sh
maechan/my.script.sh
/home/maechan/my
/home/maechan/my.script

と表示されます。

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

条件判断

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

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

ここで注意が必要なのは、条件が式ではなく つまり、コマン ドの実行であるということです。すなわち、一般的な真偽によって実行するか しないかを決定するのではなく、実行した文の「終了ステータス」で決定する というメカニズムです。

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

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

という表現が成り立ちます。

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

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

というシェルスクリプトなら、引数の数を教えてくれます(1、2、3、たくさん)。

 testコマンドで使える条件式は、manコマンドで調べられます。代表的なものを 以下に表にまとめておきます。

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

繰り返し

シェルスクリプトでも繰り返し構文である「forループ」が使えます。Cや Basicといったプログラミング言語にもforループは用意されていて、特定回数 のループを実行するのに使われますがシェルスクリプトのforループはちょっ と違って、複数のファイルに対して同じ処理を行うことが多いです。

 構文は、

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

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

では、簡単な例を示します。以下の内容のスクリプトをgreeting2.shとして作成します。

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

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

$ ./greeting2.sh maeda toshi yuki
hi maeda toshi yuki
hi maeda
hi toshi
hi yuki

となります。つまり$*を1つずつ処理しているわけです

特定条件での繰り返し

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

条件文についてはforループと同じ扱いになります。終了ステータスが0な ら真、0以外なら偽ということです。until の場合は逆になります。

以下に例として PATHに設定されている文字列を分解するシェルスクリプトを示します

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

実行例は以下の通りです。

$ echo $PATH
/home/maechan/bin:/usr/local/netscape:/usr/java/j2sdk1.4.2_02/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/netfront/bin:/usr/local/netpbm:/usr/local/bin/mh:/usr/local/canna/bin:/usr/ucb:/usr/bin:/bin:/etc:/usr/etc:/usr/hosts:/usr/bin/X11:/usr/games:
$ ./sp-path.sh
/home/maechan/bin
/usr/local/netscape
/usr/java/j2sdk1.4.2_02/bin
/usr/local/bin
/sbin
/usr/sbin
/usr/local/netfront/bin
/usr/local/netpbm
/usr/local/bin/mh
/usr/local/canna/bin
/usr/ucb
/usr/bin
/bin
/etc
/usr/etc
/usr/hosts
/usr/bin/X11
/usr/games

つい最近書いたスクリプト

以下の 自分のパスワードが漏れてるかどうかをチェックできます(その手のサイトに アクセスするのを簡単に、と)
#!/bin/bash
# by maechan on 30/Jun/2019
#
if [ "x$1" == 'x' ] ; then
        echo '# pwnedpasswords.com leaked-password checker'
        echo "Usage: $0 <password>"
        exit 1
fi
P=$(echo -n $1 | sha1sum)
if  [[ ${P} =~ ^([0-9a-f]{5})([0-9a-f]{9}).* ]] ; then
        head=${BASH_REMATCH[1]}
        tail=${BASH_REMATCH[2]}
        R=$(w3m -dump https://api.pwnedpasswords.com/range/$head | grep -i $tail)
fi
if [ "x$R" == "x" ]; then
        echo "Not leaked"
else
        echo "Leaked: $R"
fi

おわりに

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


目次に戻る
講義用スタイル
印刷用スタイル