この授業の第4回では、シェルで実行するコマンドをファイルに書いておき、そのファイルをシェルに読み込ませることでコマンドを実行させる方法について学びました。ただし、第4回では cat コマンドを使ってコマンドをファイルに書き込んでいましたが、普通これは第5回で学んだテキストエディタを使って作成します。
| テキストエディタでシェルスクリプトを作成する |
|---|
% emacs sample.csh &[Enter] |
ファイル名の拡張子は、実はここでは何でもいいんですが(付けなくてもいい)、csh のスクリプトなので一応 .csh という拡張子を付けることにします。テキストエディタが起動したら、以下の内容をタイプしてください。
#!/bin/csh -f[Enter] echo "今日は"[Enter] date[Enter] echo "です。"[Enter] cal[Enter] |
タイプできたら C-x C-s でファイルを保存してください。このとき、emacs を終了させないでおきましょう。次に、このファイルを実行可能に設定します。ターミナルのウィンドウで次のコマンドを実行してください。
| シェルスクリプトを実行可能にする |
|---|
% chmod +x sample.csh[Enter] |
それでは、このスクリプトを実行してみましょう。
| シェルスクリプトを実行する |
|---|
% ./sample.csh[Enter] |
このシェルスクリプトは csh によって実行されます。しかし csh のシェルスクリプトは、実はあまり一般的ではありません。Unix/Linux では多くのコマンドがシェルスクリプトで実現されていますが、そのほとんどが sh (Bourne shell) に対応したものになっています(Linux の場合、sh のは実際には sh と互換性のある bash (Bourne again shell) が実行されます)。
それでは sh 用のスクリプトを作成してみましょう。先ほど作成した csh 用のシェルスクリプトを、sh 用に変更してみます。emacs は sample.csh を編集している状態になっていると思いますから、これを一旦 sample.sh というファイル名で保存してください。emacs のバッファの内容を別の名前で保存するには、C-x C-w を使います。C-x C-w をタイプして、保存するファイル名に sample.sh を指定してください。
| -E:-- sample.csh |
| Write file: ~/sample.sh |
保存が完了したら、バッファの最初の1行を、csh ではなく sh を起動するように変更してください。
#!/bin/sh echo "今日は" date echo "です。" cal |
変更できたら C-x C-s でファイルを保存します。このときも emacs はバックグラウンドで動かしたままにしておいてください。そして、ターミナルのウィンドウで sample.sh を実行可能に設定したあと、実際に実行してみてください。
| シェルスクリプトを実行可能にしたあと実行する |
|---|
% chmod +x sample.sh[Enter] % ./sample.sh[Enter] |
このスクリプト sh で解釈されますが、単にコマンドを順番に実行するだけなので、結果は csh の時と同じはずです。それでは、この sh の動作を、sh を対話的なシェルとして使いながら確かめてみましょう。
sh コマンドを実行すると、sh をシェルとして使用できるようになります。sh のプロンプトは、ここでは $ 1文字であるとします。
| sh をシェルとして使う | |
|---|---|
| sh を起動してみる。 |
% sh[Enter] $ ←プロンプトが変わる |
| 今は csh の中で sh が起動している。sh もシェルなので、コマンドを実行することができる。 |
$ date[Enter] 2002年 5月29日(水曜日) 11時38分38秒 JST |
このように、sh もコマンドを実行するための標準的なシェルとして使用されます。特に Linux では、sh と互換性のある bash がログインシェル(ログイン時やターミナルのウィンドウを開いたときに実行されるシェル)としてよく使われます。sh はコマンドを実行する点では csh と同じですが、複雑なことをしようとしたときの書き方(文法)が異なります。sh (bash) の詳細は、man bash で調べてください。
sh はシェル変数や環境変数の取り扱い方が csh とは異なります。sh ではシェル変数に値を代入する際に set コマンドを使用しません。ただし、シェル変数を消去するには unset コマンドを使います。
| シェル変数 (sh) | |
|---|---|
| 変数 name に ichiro を代入してみる。変数 name は自動的に作られる。sh では変数への値の格納に set コマンドを用いない。また = の前後にはスペースを入れない。 |
$ my="My name is"[Enter] |
| echo コマンドを使って、name の内容を見てみる。変数の値を参照する(取り出す)には、変数名に $ を付ける。 |
$ echo $my[Enter] My name is |
| read コマンドを使えば、標準入力から入力したデータを変数に格納できる。 |
$ read name[Enter] Kentaro[Enter] ←データ入力待ちになるので、何か入れて改行 $ echo $my $name[Enter] My name is Kentaro ←さっき入れたものが入っている |
| 1回で複数の変数への代入ができる。expr は引数を数式とみなして計算するコマンド。 |
$ v1=10 v2=20[Enter] $ echo $v1 $v2[Enter] 10 20 $ expr $v1 + $v2[Enter] 30 |
| set コマンドでシェル変数を全部表示する。 |
$ set[Enter] |
| シェル変数を消去するには unset コマンドを使う。csh とは異なり、未定義の変数を参照してもエラーメッセージは出ない。 |
$ unset v1 v2 v3[Enter]
$ echo $v1[Enter]
←何もでない
|
sh の環境変数はシェル変数と密接に関連しています。環境変数を設定するには、一旦シェル変数を設定し、それを環境に組み込みます。これには export コマンドを使います。また環境変数の解除には、シェル変数同様 unset コマンド、環境変数の一覧を表示するには env コマンドを使います。
| 環境変数 (sh) | |
|---|---|
| HENSU という変数を表示するだけの csh スクリプト b を作る。もちろん emacs を使ってもよい。 |
$ cat > b[Enter] #!/bin/csh -f[Enter] echo $HENSU[Enter] Ctrl-D |
| b を実行可能にする。 |
$ chmod +x b[Enter] |
| b を実行してみる。多分エラーになる。 |
$ ./b[Enter] HENSU: 変数を定義していません. |
| シェル変数 HENSU に値を与えてもエラーになる。 |
$ HENSU=10[Enter] $ ./b[Enter] HENSU: 変数を定義していません. |
| export コマンドを使って、シェル変数 HENSU を環境に組み込む。今度はうまくいく。 |
$ export HENSU[Enter] $ ./b[Enter] 10 |
| コマンドの左にシェル変数への代入を置くと、そのコマンドに対してだけ一時的に環境変数となります。 |
$ HENSU=20 ./b[Enter] 20 ←b では環境変数 HENSU に 20 が入っている $ echo $HENSU[Enter] 10 ←もとのシェルでは HENSU は 10 のまま |
シェルや他のプログラミング言語において、データや画面表示に使う単語や文章のような「文字がならんだデータ」のことを文字列と呼びます。文字列は、それが空白などを含んでいてもひとかたまりのデータとして扱えるように、引用符でくくります。sh や csh では、引用符によって動作が違います。
- '~'(シングルクォーテーションマーク)
- '~'(シングルクォーテーションマーク、シングルクォート)ではさんだものは、スペースなどを含めて単一の文字列として取り扱われます。
- "~"(ダブルクォーテーションマーク)
- "~"(ダブルクォーテーションマーク、ダブルクォート)ではさんだものは、スペースなどを含めて単一の文字列として取り扱われます。ただし、変数(シェル変数、環境変数)はその内容に置き換わります。また次の `~`(バッククォーテーションマーク)による置き換えも行われます。
- `~`(バッククォーテーションマーク)
- `~`(バッククォーテーションマーク、バッククォート)は上の2つとは異なり、この間にはさんだものをコマンドとして実行します。そして、その標準出力への出力を、この場所に書かれた文字列として取り扱います。
| 引用符 | |
|---|---|
| echo コマンドで *(アスタリスク)を出力しようと思う。しかし、* はワイルドカードなので、ファイル名の一覧が出力されてしまう。 |
$ echo *[Enter] (ファイル名の一覧が出力される) |
| \ を使えば * の効果を打ち消すことができる。 |
$ echo \*[Enter] * |
| '~' や "~" ではさんでも * を出力できる。 |
$ echo '*'[Enter] * $ echo "*"[Enter] * |
| echo コマンドで $HOME を出力してみる。HOME はホームディレクトリの場所を格納している環境変数。 |
$ echo $HOME[Enter] /home/s3/s075000 |
| \ を使って $ の効果を打ち消すことができる。 |
$ echo \$HOME[Enter] $HOME |
| しかし、"~" ではさんでも $ の効果は生きている。 |
$ echo "$HOME"[Enter] /home/s3/s075000 |
| '~' では $ がそのまま出力される。 |
$ echo '$HOME'[Enter] $HOME |
| `~` にはさんだものは、コマンドとして実行される。右の例は、date コマンドの出力をシェル変数 d に格納する。 |
$ d=`date`[Enter] $ echo $d[Enter] 2002年 5月29日(水曜日) 11時45分22秒 JST |
それでは、sample.sh を次のように変更(太字の部分)してください。
#!/bin/sh echo "今日は" date echo "です。" echo "あなたは昭和何年生まれですか?" read year echo "あなたは何月生まれですか?" read month echo "あなたの誕生月のカレンダーです。" cal $month `expr $year + 1925` |
変更が完了したら C-x C-s で sample.sh を保存し、ターミナルのウィンドウでそれを実行してみてください。
| sample.sh を実行する |
|---|
$ ./sample.sh[Enter] |
引用符や変数は組み合わせて使用することができます。sample.sh を、次のように変更してみてください。最初に表示している3行を、1行にまとめます。
#!/bin/sh d=`date +'%m月%d日'` echo "今日は$dです。" echo "あなたは昭和何年生まれですか?" read year echo "あなたは何月生まれですか?" read month echo "あなたの誕生月のカレンダーです。" cal $month `expr $year + 1925` |
C-x C-s で sample.sh を保存し、ターミナルのウィンドウで sample.sh を実行してみてください。
| sample.sh を実行する |
|---|
% ./sample.sh[Enter] |
date コマンドは引数で出力する日付と時間の書式を制御できます。詳しくは man date を見てください。
この sample.sh を実行すると、「あなたは昭和何年生まれですか?」という質問が表示された後、次の行に改行してデータの入力待ちになります。「あなたは昭和何年生まれですか?」という表示は、データの入力を促すプロンプト(入力促進符号)なので、ここで改行されないほうが見やすいかもしれません。echo コマンドに -n というオプションを付ければ、この改行を抑止できます。
- echo コマンドの -n オプション
- echo コマンドは引数に指定した文字列を標準出力に出力するコマンドですが、出力される行の行末には改行が付加されます。echo コマンドに -n オプションを付けた場合は、改行を出力しません。
| echo コマンドの -n オプション | |
|---|---|
| コマンドを ;(セミコロン)で区切れば、1行に複数のコマンドを書くことができる。この例では、ひとつの echo コマンドごとに改行が出力される。 |
$ echo "This is "; echo "a pen."[Enter] This is a pen. ←改行が入る |
| echo コマンドに -n オプションを付ければ、この改行を抑止できる。 |
$ echo -n "This is "; echo "a pen."[Enter] This is a pen. ←改行が入らない |
| echo -n と read の組み合わせ | |
|---|---|
| read コマンドがデータ入力を待っているときは、プロンプトは表示されない。そこで、echo コマンドを使って自分でプロンプトを出してやることにする。しかし、これではプロンプト(ここでは "x=")の後に改行が出力されてしまい、read による入力データ待ちと分離してしまう。 |
$ echo "x="; read x[Enter] x= ←echo コマンドを使って出したプロンプト 10[Enter] ←データ入力待ちになるので、何か入れて改行 |
| echo コマンドに -n オプションを付ければ改行が出力されないため、プロンプトの直後で入力データを待つことができる。 |
$ echo -n "x="; read x[Enter] x=10[Enter] ←プロンプトのところで入力データ待ちになる |
それでは、sample.sh の中の2つの echo コマンドに、-n オプションを追加してください。
#!/bin/sh d=`date +'%m月%d日'` echo "今日は$dです。" echo -n "あなたは昭和何年生まれですか?" read year echo -n "あなたは何月生まれですか?" read month echo "あなたの誕生月のカレンダーです。" cal $month `expr $year + 1925` |
追加できたら C-x C-s で sample.sh を保存し、ターミナルのウィンドウで sample.sh を実行してみてください。
| sample.sh を実行する |
|---|
$ ./sample.sh[Enter] |
実は sh は csh より古く、Unix の初期のログインシェルとして使われていました。現在ではログインシェルとして使用するよりも、プログラミング言語としてよく使われます。ただし bash(皆さんが今使っている sh)は、ログインシェルとしても良く使われています。
| sh を終了する | |
|---|---|
| sh を終了する。sh の場合も Ctrl-D か exit コマンドで終了できる。 |
$ exit[Enter] % ←csh のプロンプトに戻る |
次に、sh スクリプトで数を数えるコマンド(カウンタ)を作ってみましょう。カウンターとは、下の例のように、実行するたびに出力する数値が1ずつ増えていくコマンドです(これはあくまでも例ですから、今はまだ実行できません)。
| カウンターの動作 |
|---|
% ./counter.sh[Enter] 1 % ./counter.sh[Enter] 2 % ./counter.sh[Enter] 3 ←実行するたびに数が増えていく |
counter.sh は数値(カウント)を出力するとすぐに終了してしまいますから、何らかの方法で前回出力した数値(カウント)が何だったか覚えておく必要があります。ここではこれをファイルに記録しておくことにします。この段取りは次のようになります。
カウントを記録しておくファイルのファイル名は count.dat とします。変数にデータを格納するには read コマンドを使用しますが、ここではそのデータを count.dat から取り出す必要があります。したがって、このファイルの中からデータを取り出してシェル変数に格納するには、read コマンドの標準入力をリダイレクトして、count.dat からデータを取り出すようにします。
| ファイルから変数にデータを格納する |
|---|
read count < count.dat |
ただし、最初に counter.sh を実行した時には前回のカウントは調べられませんから、その場合はカウントを 0 としておく必要があります。最初に counter.sh を実行したときには、おそらく count.dat は存在しないでしょうから、このファイルが見つからなければ counter.sh を初めて動かしたのだと判断できます。あるファイルが存在するかどうかを調べるには、test コマンドを使います。
| test コマンドと終了ステータス | |
|---|---|
| コマンドはその実行を終えると、終了ステータスをシェルに報告する。直前に実行したコマンドの終了ステータスは $? という特殊な変数で取り出すことができる。 |
$ ls[Enter] ←何かコマンドを実行してみる (ファイル名の一覧が出力される) $ echo $?[Enter] 0 ←終了ステータス 0(ls の実行成功) |
| 終了ステータスは整数値であり、0 が真(コマンドは正常に実行できた)、非 0 が偽(コマンドの実行に失敗した)を表す。 |
$ ls xyz[Enter] ←引数に存在しないファイル名を指定 ls: xyz: そのようなファイルやディレクトリはありません $ echo $?[Enter] 1 ←終了ステータス 1(ls の実行失敗) |
| test コマンドは引数に指定したテストを実行して、結果を終了ステータスでシェルに報告する。tesf -f file だと、file というファイルが存在すれば、終了ステータスは真 (0) になる。 |
$ test -f xyz[Enter] $ echo $?[Enter] 1 ←test コマンドは何も言わないが終了ステータスは 1 $ test -f sample.sh[Enter] $ echo $?[Enter] 0 ←sample.sh というファイルはさっき作ったのであるはず |
test コマンドはファイルの有無のほかにも、いろんなことを調べることができます。詳しくは man test で調べてください。これと if という制御文を組み合わせれば、終了ステータスをもとにコマンドの実行順序を制御(分岐)できます。
| if と終了ステータス |
|---|
if コマンド
then
(上のコマンドの実行が成功したときに
この部分のコマンドが実行される)
else
(上のコマンドの実行が失敗したときに
この部分のコマンドが実行される)
fi
|
すなわち、「もし(if)コマンドの実行が成功した、ならば(then) 前半部分のコマンドを実行する、さもなければ(else)後半部分のコマンドを実行する」という実行順序の制御を行います。ここでは count.dat というファイルが存在すれば、それからデータを取り出し、さもなければ(ファイルが存在しなければ)データを 0 とするという処理を行いますから、次のようなスクリプトになります。
| count.dat があるときだけデータを取り出す |
|---|
if test -f count.dat
then
read count < count.dat
else
count=0
fi
|
これでようやく「前回のカウント」をシェル変数 count に取り出すことができました。それでは、これに1を加えましょう。シェル変数 count の値は $count として参照できます。expr コマンドを使って、これに 1 を加えます。これは expr $count + 1 でできます。この出力を再び count に格納します。それには `~` を使います。
| シェル変数 count に 1 を加える |
|---|
count=`expr $count + 1` |
あとは、このシェル変数 count の値を標準出力に出力し、さらにファイル count.dat にも保存すれば完了です。コマンドの出力をファイルに保存するには、標準出力をリダイレクトします。
| シェル変数の値を標準出力とファイルに出力する |
|---|
echo $count echo $count > count.dat |
すべての処理が終わったら、sh スクリプトを終了します。sh スクリプトの終了にも exit コマンドを使いますが、その引数にこのスクリプトの終了ステータスを指定します。sh スクリプトが正しく仕事を完了して終了するなら、通常ここに 0 を指定します。終了ステータスを指定しなければ、sh スクリプト内で最後に実行したコマンドの終了ステータスが、スクリプトの終了ステータスとなります。
| sh スクリプトを終了する |
|---|
exit 0 |
実際の counter.sh の作成は課題として行ってください。
sh には if 以外に繰り返しを行う for や while、複数の条件に分岐できる case などの実行制御機能があります。
| for |
|---|
for x in this is a pen.
do
echo $x
done
|
for は in の右側に書かれたものを一つ一つシェル変数 x に格納し、そのたびに do ~ done の間を実行します。上の例では x に this を入れた後 echo $x を実行し、次に is を格納して echo $x を実行するという具合に、this、is、a、pen. の4つに対してそれぞれ echo $x が実行されます。ここにワイルドカードを指定すれば、カレントディレクトリにある一つ一つのファイルに対して処理を行うことができます。
| while |
|---|
while read x
do
echo $x
done
|
while は while の右側に書かれたコマンドの終了ステータスが真の間、do ~ done の間を繰り返し実行します。上の例では read が成功している間、すなわちデータの入力が行われている間、echo $x が実行されます。データの入力が終了した(ファイルの終わりに達した)場合などに終了ステータスは偽になりますから、繰り返しは終了します。
| case |
|---|
case $x in
a)
echo "$x は a というファイル"
;;
b*)
echo "$x は b で始まるファイル"
;;
*)
echo "$x はその他のファイル"
;;
esac
|
case は in との間に書かれたものと、")" のところに書かれた文字列パターン(case ラベル)との間でパターンマッチング(文字列パターンの照合)を行います。この文字列パターンにもワイルドカードが使用できます。
| 例 |
|---|
for file in *
do
case $file in
*.bak|*~)
echo "$file はバックアップファイルです。"
rm -i $file
;;
*.txt)
echo "$file はテキストファイルです。"
echo -n "中身を見ますか?"
read answer
case $answer in
y*)
less $file
esac
;;
*)
echo "$file はその他のファイルです。"
;;
esac
|
上の例は、カレントディレクトリにあるすべてのファイル (*) について、ファイル名の最後が ".bak" または (|) "~" のファイルは「バックアップファイル」であるとみなして削除します。また、ファイル名の最後が ".txt" のファイルは「テキストファイル」とみなして、ページャ (less) を使って中身を見るかどうかたずねます。ここで yes など y で始まる文字を入力すれば、中身を見ます。
sh スクリプトは他のコマンドを組み合わせて使用することで、非常に柔軟に目的の仕事をこなすスクリプトをスクリプトを作成することができます。その一方で、sh スクリプトはひとつの仕事をこなすのにも複数のコマンドを動員しなければならないという効率の悪さや、組み合わせたコマンドの間でデータをやり取りするのにシェル変数やパイプくらいしか使えないという使いにくさもあります。
Perl を使えば、sh スクリプトで実現されるような仕事を単一のコマンドで実現することができます。Perl にはログインシェルとして使えるような、対話的なコマンド操作の機能はありませんが、非常に多くのことを単一のコマンドで実現するために、Perl には「なんでもあり」と言えるほど多くの機能が組み込まれています。ここでは、その端っこすら紹介できませんが、まあこんな感じのスクリプトになるというあたりを見てください。
というか、Perl にまで手を伸ばしている時間はさすがにナイ・・・
counter.sh を Perl で作成してみます。ファイル名は counter.pl にしましょう。Perl スクリプトの拡張子には .pl が使われることが多いようです。演習室のコンピュータでは Perl コマンドは /usr/bin/perl にありますから、Perl スクリプトの最初の1行は次のようになります。
#!/usr/bin/perl |
次に、ファイル count.dat の有無を調べて、もしあればそこからデータを読むわけですが、Perl を含めて一般的なプログラミング言語では、データファイルを読み書きする前に、通常ファイルを開くという手続きが必要になります。Perl では open という関数を使います。ファイルの有無は、open が成功したか失敗したかで判断できます。
#!/usr/bin/perl
if (open COUNT, "<count.dat") {
$count = <COUNT>;
close(COUNT);
}
else {
$count = 0;
}
|
次に、読み出したカウントを1増します。
#!/usr/bin/perl
if (open COUNT, "<count.dat") {
$count = <COUNT>;
close(COUNT);
}
else {
$count = 0;
}
++$count;
|
そして、$count を出力します。
#!/usr/bin/perl
if (open COUNT, "<count.dat") {
$count = <COUNT>;
close(COUNT);
}
else {
$count = 0;
}
++$count;
print $count, "\n";
|
最後に $count をファイルにも出力します。
#!/usr/bin/perl
if (open COUNT, "<count.dat") {
$count = <COUNT>;
close COUNT;
}
else {
$count = 0;
}
++$count;
print $count, "\n";
open COUNT, ">count.dat" or die "count.dat: $!\n";
print COUNT $count, "\n";
close COUNT;
exit 0
|
(1)今日の授業の「1.5 引用符」のところで使った「cal $month `expr $year + 1925`」というコマンド行の動作を説明しなさい。
(2)実際に counter.sh を作成しなさい。
解答は一つのメールにまとめて tokoi@sys.wakayama-u.ac.jp まで送ってください。Subject: は「第7回」にしてください。