第 7 回 (2006.06.02) -- シェルスクリプト

シェルスクリプトとは スクリプトファイルのコマンド化 を利用して,シェルの複雑で連動するコマンドをあたかも一つのコマンドのように扱えるようにするものである.
シェルで何回も似たような作業をするときなどには,非常に威力を発揮するので, 面倒な作業だなと思ったら,シェルスクリプトを組めないか検討してみるのが良い.

シェルスクリプトの文法に関しては,sh 系シェルと csh 系シェルで結構な違いがある. csh 系シェルの方が人間にとって直感的な文法を採用しているので簡単にスクリプトを作成できる傾向がある.
しかし, 有害な csh プログラミング などでも指摘されているように csh では文法的な問題点がいくつかある. また,sh はどのような unix でもほぼ必ず存在するが csh はそうとは限らないため, シェルスクリプトの可搬性を考えると sh 系の方が良い.

こうした事情から,本講義では sh 系シェルスクリプトについて学習する(これは正統的である).
もし余裕があるようならば csh 系のシェルスクリプトについても学んでみると良いだろう.

シェルスクリプトの作成から実行まで

シェルスクリプトは便利なものなので, 一度作ったら後で何度も利用することになることが多い.
そこで,作ったシェルスクリプトは後からでも簡単に使えるようにしておくのが良い. それには,シェルスクリプトをまとめて置いておくディレクトリを用意して, そのディレクトリに path を通す,という手順を踏んでおくことになる.
また,新しくシェルスクリプトを作成したら,そのファイルを実行できるよ うにパーミッションを設定しておかないといけない.

以上を具体的に書いてみると次のようになるだろう.

  1. スクリプトファイルを貯蔵しておくディレクトリを用意する. 通常は bin という名前を用いるのが普通.
    cd ~
    mkdir bin

  2. 用意したディレクトリに path を通す..bashrc の中で設定するのが良いだろう. 具体的には,
        if [ -d $HOME/bin ]
        then
            PATH=$PATH:$HOME/bin; export PATH
        fi
    などと書き込んでおけば,次回ログイン時よりこれが有効になる.
    # この変更を有効にするためにいったんログアウトしてから再ログインしてもよいが, source ~/.bashrc とした方が簡単なのは… 覚えているか?

  3. シェルスクリプトを作り,1 で用意したディレクトリに置く.

  4. スクリプトファイルに実行パーミッションを与える. スクリプトファイルが test という名前だとすると, chmod u+x ./test などとすれば良い.

実習

上に示した 1, 2 を実行しておこう.

シェルスクリプトの基本構造(bash)

スクリプトファイルのコマンド化 を用いてシェルスクリプトは作成される. 問題は,スクリプトの先頭行に何と書くかである. シェルスクリプトはシェル設定ファイルを読まないようにした方が安定して動作すること, なるべく普遍的な環境で動作すること,非対話環境であること, を考慮すると,結論から言えば

#!/bin/bash --noprofile --posix

と書くか,

#!/bin/sh

と書くのが良いだろう(この二つは基本的に同じ動作になる). 間違いの少なさや普遍性から言えば,後者の表記が良さそうだ.

また, sh にオプションをつけて,次のようにすることもできる. 場合に応じて使うと良い.

#!/bin/sh -n と先頭行に書く.
実際にはスクリプトの内容を実行しないで,文法チェックだけ行う. ファイルの削除など,やや危険な作業を行うスクリプトはこれでチェックすると良いだろう.
#!/bin/sh -v と先頭行に書く.
実行時にスクリプトの内容も表示する(正確には標準エラーに出る). スクリプトの動作が望みのものと違う時はこれでチェックすると良い.

実習

  1. Hello というファイル名で,次のような中身のスクリプトを作る.

    #!/bin/sh
    echo "Hello, world!"


  2. シェルスクリプトの作成から実行まで の 3, 4 を行う.

  3. どのディレクトリからでもよいので, Hello と打ち込んで,作成したシェルスクリプトが動作することを確認する.

コマンドの連続実行

シェルではコマンドを連続実行したり,グループ化できたりするのは初期の授業の シェルの機能 の部分で学習したが, シェルには連続するコマンドに対する他の機能もある. ここでそれらをまとめておく.

■ コマンドの連続実行 ■
文法 解説
コマンド1 ; コマンド2 コマンド1 を実行後,コマンド2 を実行する.
( コマンド1 ; コマンド2 ) コマンド1 を実行後,コマンド2 を実行する… のだが, これら二つのコマンドを「サブシェル」のもとで実行する(グループ化). サブシェルで行った環境変更はもとのシェルに影響しない. よって,状態を一時的に変更して作業するときに便利.

グループ化の利点例. 例えば, (cd /tmp; ls -a) とすると,/tmp で作業が行われるが,終わったあとにディレクトリは変更されていない.
コマンド1 && コマンド2 AND 実行. コマンド1 を実行してみて,コマンド1 が成功したならば(エラーにならない時), コマンド2 を実行する.
コマンド1 || コマンド2 OR 実行. コマンド1 を実行してみて,コマンド1 が失敗したならば(エラーになった時), コマンド2 を実行する.



シェルスクリプトの文法

コメントアウト

シェルスクリプト中では,# という文字があると, そこから行末までがコメントとして取り扱われる(ただし,文字列中は除く).
コメントを適切に書き込むことで,よりわかりやすいスクリプトを作ることができるだろう.

変数

シェルスクリプトも一種のプログラムであるから, 変数を使いたいというのは当然である. そこで,変数をどう使うかであるが,文法的には シェルでの変数の設定 の「シェル変数」の部分を参照すれば良い.

基本的には,

変数名=値

とすることで,変数の宣言と代入が同時に行える.

ただし… このとき,変数名と「=」の間にスペースを入れると, その名前のコマンドを実行するのだと解釈されてしまう(誤動作)ので注意せよ.

そして,その変数の内容を使いたい(参照)時は,

$変数名

とすれば良い.この時,

${変数名}

とした方がより安全に参照できる. 似た名前の変数がある時や,二桁番目の引数変数などはこうして参照するのが良い. 中カッコで囲むことによって問題が起きることはあまりないので, いつでもこうしておく,という方が良いかもしれない.

文字列

シェルスクリプトの中で文字列を文字列だ,と表すには quotation しないといけない. そうしないと変数とみなされてしまう.
この時, 「'」(シングルクォート) で囲むのと, 「"」(ダブルクォート) で囲むのでは性質がやや異なるので注意が必要だ.

「'」(シングルクォート) で囲んだ文字列
何が書かれていても単なる文字列として扱う.
「"」(ダブルクォート) で囲んだ文字列
$ ` \ の3つの特殊文字を特殊な意味で扱う. よって,"(ダブルクォート)で囲まれた文字列の中で変数の参照は行われる. "(ダブルクォート) の囲み内部で上の3つの特殊文字を単なる文字として扱いたければ, \ を前につけて \$ \` \\ とすればよい.

実習

先の Hello スクリプトを次のように改造しよう.

#!/bin/sh
# Greeting Script

g='Hello, world!'
d="today is `date +%m/%d`"

echo $g, $d.


何がどうなっているのか理解せよ.

引数

引数とは,プログラムを起動するときに,一緒に与える指示子をいう… と書いても何が何だが, 例えば ls -a /tmp とプログラムを実行したら,広い意味で引数は -a, /tmp の二つである(オプションを引数に含めるかどうかは定義次第だ).

シェルスクリプトはその性質上,引数をともなって実行されることが多い. そこで,引数の取り扱いはきちんとできないといけないだろう. 次に引数がどう取り扱われるかを示す.

■ sh 系シェルスクリプトでの引数の扱い ■
変数 意味
$# 引数の個数
$n n 番目の引数.
ただし,0番目の引数 $0 は特別で, シェルスクリプト自身の名前になる.

また,n > 9 である場合は ${12} などと,中カッコで囲まないと使えない.
$* 引数全て.
${m}-${n} m 番目から n 番目までの引数.

また, shift というコマンドを実行すると, $1 に $2 の内容が入り, $2 に $3 の内容が入り… と 引数が一つずつずれる(このとき,$# は 1 減る).
古めのスクリプトではこの shift というコマンドはよく使われている.

実習

Welcome というファイル名でスクリプトを次のように作る.

#!/bin/sh
# Welcome Script

name=`whoami`
g="($1 says) Thank you, $name"

echo $g.


そして,このスクリプトを次のように実行してみよう.

Welcome smith

実行してみるとともに, 何がどうなっているのか理解せよ.

入出力

シェルスクリプトが実行中に入出力を行うには,主に次のコマンドを用いる.
# もちろん,ひねくれた方法はいくらでもありうるだろうが…

■ sh 系シェルスクリプトでの入出力 ■
コマンド 解説
read 変数 入力.
ユーザからの(キーボード)入力を待ち,その結果を変数にいれる.
#!/bin/sh
echo -n "Pls input something : "
read a
echo " You input is " $a
を実行するとどうなるだろうか.
echo 出力したいもの

echo -n 出力したいもの
出力.
-n オプションがついていると,出力後に改行しない.

実習

Hello スクリプトをさらに改造して次のようにしよう.

#!/bin/sh
# Greeting Script

g='Hello'
i='Please tell me your name: '
o="Oh, nice to meet you"

echo $g.
echo -n $i
read ans
echo $o, $ans.


そして,このスクリプトを実行してみるとともに, 何がどうなっているのか理解せよ.

計算

オリジナルの sh は原則として計算機能を持っていない(bash は実は計算機能を持っているのだが). そこで,シェルスクリプト中で計算を行いたいときは,互換性も鑑みて, バッククォーテーションを用いた 先読み評価 `` を利用して外部コマンドを呼び出す手法を使うことになる. 具体的には, exprbc を使って,次のように行うことになる.
簡単に言って,簡単な整数計算だけでよければ expr を,やや複雑な実数計算を行いたければ bc を使う,ということになる.

expr を使った例

expr については,マニュアルを参照してもらうとして, 使い方は以下のように簡単なので問題ないだろう.

#!/bin/sh
a=5
b=`expr $a + 10` ; echo $b
c=`expr $b \* 2` ; echo $c
← ` ` の中では * は展開されてファイル名などになってしまいかねないので,\ で単なる文字としておく.

実習

上のスクリプトを試してみよ.

bc を使った例

bc は,標準入力から計算式をもらうことで計算を行う機能があるので, echo でリダイレクトすることでそれを利用する. 具体的には次の例をみれば良いだろう.

#!/bin/sh
a=`echo "4*a(1.0)" | bc -l` ; echo $a
← πを計算している.
b=`echo "c($a)" | bc -l` ; echo $b
← cos(π)を計算している.

実習

上のスクリプトを試してみよ.

シェルの関数

sh 系シェルには,そのシェル内で関数を作成することができる. 関数という名前がついているが,コンピュータ言語的には手続き(procedure)とか,サブルーチンとかいう方がわかりやすいだろう. 要するに,コマンドをひとまとめにして名前をつけたものである.
# 全てのルーチンを関数として扱う C 言語流になっている.unix だから当たり前とも言える(^-^).

■ sh系シェルでの関数 ■
構文 説明
関数名 () {
    コマンド…
    }


ちなみに… 関数に対する n 番目の引数を $n として参照できる.

また,この関数内部だけで使いたい変数があるときは, 変数宣言の時に local を頭部につける.
複雑な手続きをひとまとめにすることができる.
引数変数が使えるので, 引数を持てない alias の代わりに使える.

#!/bin/sh

mawaru () {
  local i=1
  while [ $i -le 10 ]
  do
    echo $i
    i=`expr $i + 1`
  done
  }

mawaru
      
というシェルスクリプトはどういう動作をするだろうか?
(文法的に分からない部分は,この web を最後まで読めばわかるはずだ)

実習

Hello スクリプトをさらに改造して次のようにしよう.

#!/bin/sh
# Greeting Script

g='Hello'
d=`date +%d`
dm=`expr $d - 1`
t=`date +%H`
m=`date +%M`

d2min () {
min=`echo "(($1 * 24) + $2) * 60 + $3" | bc -l`
}

d2min $dm $t $m
echo $g, "about $min minutes have passed this month."


そして,このスクリプトを実行してみるとともに, 何がどうなっているのか理解せよ.

シェルの制御構造

(algol系)プログラムには,「もし…ならば」とか,「…を繰り返して…」 という動作が必要になる. これらの動作を実現するためのコマンドや構造を制御構造という. sh 系シェルスクリプトでは,次のような制御構造がある.

if … (単純)条件分岐
条件に応じて(二つに)分岐していく.具体的な構文と動作の流れは以下の通り.
構文 動作の流れ
if 条件1
then コマンド1 …

elif 条件2
then コマンド2 …
elif 条件3
then コマンド3 …

(以下, elif … then … を好きなだけ繰り返し)

else コマンドN …
fi

ただし… if の後の条件については, 条件式および条件式のチェック を参照せよ.
ちなみに… elif 以下と else 以下は存在しなくても良い.
最初に 条件1 が実行 & チェックされ,
条件1 が成り立つ(= 実行が成功する) → コマンド1… が実行されて if 文は終了する.
条件1 が成り立たない → 次の elif へ.

→ 次の elif に来た場合.条件2 がチェックされ,
条件2 が成り立つ → コマンド2… が実行されて if 文は終了する.
条件2 が成り立たない → 次の elif へ.

(… 以下,繰り返し …)

→ else に来た場合.無条件で コマンドN が実行されて if 文は終了する.


実習

次のような内容のスクリプトを作り,実行し,理解せよ.
ただし, if のすぐ後の [ -e $fn ] という部分については後述するので理解は後回しでよい.

#!/bin/sh

fn=~/.bashrc

if [ -e $fn ]
   then
   cat $fn
   else
   echo "You has no $fn."
   fi


case … (パターンマッチ)条件分岐
ある「文字列」が用意したパターンとマッチするかどうかで分岐する. 単純だが分岐が多い,というような場合に有効である. パターンは "|" で区切って並列表記(or を意味する)することもできる. 具体的な構文と動作の流れは以下の通り.
構文 動作の流れ
case 文字列 in
    パターン1 ) コマンド1… ;;
    パターン2 ) コマンド2… ;;

(以下, パターンk ) コマンドk…;; を好きなだけ繰り返し)

    * ) コマンドN…
esac

*) は存在しなくても良い.
最初に 文字列 を評価して,
文字列がパターン1 とマッチする → コマンド1… が実行されて case 文は終了する.
マッチしない → 次のパターンとマッチするか(以下繰り返し)

→ * ) に来た場合.無条件で コマンドN が実行されて case 文は終了する.


実習

次のような内容のスクリプトを作り,実行し,理解せよ.

#!/bin/sh

echo -n "Please input file name: "
read fn

case $fn in
    *.txt )
        echo "$fn is text file.";;
    *.c )
        echo "$fn is C program file.";;
    * )
        echo "Hey, what is $fn ? You know it?";;
esac


for … あてはまる変数の繰り返し
具体的な構文と動作の流れは以下の通り.
構文 動作の流れ
for 変数 in パターンorリスト
do
    コマンド…
done
まず,パターンorリストを展開して,リストを生成する.
そのリストの要素を順番に 変数 に代入して, そのたびにコマンド… を実行する.


実習

次のような内容のスクリプトを作り,実行し,理解せよ.

#!/bin/sh

for x in 0 1 2 3 4 5 6 7 8 9 10
do
    v=`echo "$x * 0.31416" | bc -l`
    echo "sin($v) = " `echo "s($v)" | bc -l`.
done


while, until … 条件チェックつきでの繰り返し.
具体的な構文と動作の流れは以下の通り.
構文 動作の流れ
while 条件
do
    コマンド…
done

until の構文も同じ.
まず,条件が成り立つかチェック.
[ while の場合] 成り立つならば,コマンド… を実行.
[ until の場合] 成り立たないならば,コマンド… を実行.

再び,条件が成り立つかチェック. 以下,繰り返し.


実習

次のような内容のスクリプトを作り,実行し,理解せよ.
ただし, while のすぐ後の [ $x -le 10 ] という部分については後述するので理解は後回しでよい.

#!/bin/sh

x=0

while [ $x -le 10 ]
do
    v=`echo "$x * 0.31416" | bc -l`
    echo "sin($v) = " `echo "s($v)" | bc -l`.
    x=`expr $x + 1`
done


break … ループ脱出.
このコマンドが実行されると,for,while, until のループから脱出する.

exit … 終了.
シェルスクリプトの終了.

条件式および条件式のチェック

sh 系シェルでは,条件は基本的に [ 条件式 ] という形式で書かれる.

ただし… " [ " と条件式の間にはスペースが必要なので注意すること.
# 詳しく言うと,[ は条件式をチェックする,という機能を持つコマンドである. 本来の名前は test である. どうなっているのかは, ls -lg `which [` などとしてみると分かるだろう.

条件式の書き方は,基本的に「単項演算子」か「二項演算子」 と項目を組み合わせるというものである. 以下に列挙しよう.

ファイルに関する条件式

-e 名前
その名前のファイルが存在するならば真.
-d 名前
その名前のディレクトリが存在するならば真.
-f 名前
その名前の通常ファイルが存在するならば真.
-r 名前
その名前の読み込み可能なファイルが存在するならば真.
-w 名前
その名前の書き込み可能なファイルが存在するならば真.
-x 名前
その名前の実行可能なファイルが存在するならば真.
-s 名前
その名前のサイズが 0 より大きなファイルが存在するならば真.

実習

制御構造 if のところの実習例などを参考にして,上の条件を全部試すスクリプトを作成してみよ.

文字列に関する条件式

-z 文字列
文字列の長さがゼロならば真.
-n 文字列
文字列の長さがゼロでなければ真.
文字列1 == 文字列2
文字列が等しければ真(見えにくいが,「=」が二つつながっている).
文字列1 != 文字列2
文字列が異なれば真.

実習

制御構造 if のところの実習例などを参考にして,上の条件を全部試すスクリプトを作成してみよ.

数値に関する条件式

シェルは条件式の判定には整数しか用いることができないので,注意すること.

数値1 -eq 数値2
数値が等しければ真.
数値1 -ne 数値2
数値が異なれば真.
数値1 -gt 数値2
数値1 > 数値2 ならば真.
数値1 -ge 数値2
数値1 ≧ 数値2 ならば真.
数値1 -lt 数値2
数値1 < 数値2 ならば真.
数値1 -le 数値2
数値1 ≦ 数値2 ならば真.

実習

制御構造 if のところの実習例などを参考にして,上の条件を全部試すスクリプトを作成してみよ.

その他 条件式

!条件式
NOTを表す. 条件式が成り立たなければ真.
条件式1 -a 条件式2
ANDを表す. 両方の条件式が成り立てば真.
条件式1 -o 条件式2
ORを表す. どちらかの条件式が成り立てば真.
( 条件式 )
条件式をグループ化する. 複雑な条件式を書くときには, 意図と異なる結果にならないためにも積極的に使うべし.

実習

制御構造 if のところの実習例などを参考にして,上の条件を全部試すスクリプトを作成してみよ.

レポート課題(20点)

以下に示された課題について

AppliedMath-Report-07

という題名をつけて e-mail にて教官宛にレポートとして提出せよ(教官のメールアドレスは授業中に口頭で伝える).
なお,レポートを e-mail の代わりに TeX で作成した書面にて提出してもよい.

課題内容

  1. 引数1 に「金額」を,引数2 に「日数」を入れると, その金額でその日数だけお金を借りたら最終的にいくらになるかを計算するシェルスクリプトを作成せよ. ただし,金利は 10日で 1割 (複利)とする.
    ただし, read コマンドなどを使わず,シェルスクリプトの引数をきちんと処理すること.

  2. ファイルを消去するシェルスクリプトを作成せよ. ただし,次の機能を持っているようにせよ.
    1. ファイルのバックアップファイルがあるかどうか調べて,
      • 無い … 作成する.
      • その数が3つ以下 … 新たにバックアップファイルを作成する.
      • その数が3つより多い … 一番古いバックアップファイルを消去してから, 新たにバックアップファイルを作成する.
    2. 以上の作業を行ってから,ファイルを消去する.


  3. これまでの授業課題で,シェルスクリプトを利用して解決できる課題があればそれを行え.




最終更新日 … $Date: 2006-05-29 21:32:57+09 $
Valid CSS! Valid XHTML 1.1!