シェルスクリプト

シェルスクリプトとは

スクリプトファイルのコマンド化 を利用して,シェルの操作を一つのコマンドに変えるものである. Unix で何回も似たような作業をするときなどには,非常に威力を発揮する.

繰り返す操作はシェルスクリプトにしておこう

コマンドにしておけば再利用も簡単.

シェルの文法に関しては,Bsh 系シェルと Csh 系シェル(, さらに他のシェル, fish など)でかなり異なる. おおよそ、

  • Csh 系の方が人間にとって直感的.
  • Bsh 系の方が文法が硬い(= きちんとしている) (“有害な csh プログラミング” という言葉で web を検索してみよう)

という傾向がある.今回は Bsh 系で学習しておこう.

シェルスクリプトを分析してくれるサイト, ソフト

先に紹介しておこう. シェススクリプトについて学んだり,自分でスクリプトを書いたりしているといろいろ疑問が生ずると思う. その際,スクリプトを入力すると分析して解説をしてくれるサイト ShellCheck や、ソフト(Emacs で動作する flycheck など)は大変役に立つだろう. シェルスクリプトを書いたけれども動作しない、というような時はぜひ有効活用してもらいたい.


準備, 一連の流れ

準備

  1. まず、スクリプトファイルがどこにあるのかが統一されていないと環境変数 PATH の設定で困るので、用意していない人はそれらを貯蔵しておくディレクトリを一つに決めて用意しておこう. 多くの場合は、ユーザディレクトリの直下に bin というディレクトリを作ってそれとすることが多いので、ここでもそうしよう.実際には

    1
    
    cd ~ ; mkdir bin

    とすれば良いだろう. - 用意したディレクトリを環境変数 PATH に追加しよう. bash を使っているならば通常は ~/.profile ファイル(ログインシェル起動時に読み込まれる) か ~/.bashrc ファイル(対話用に起動するときに読み込まれる)の中でそう設定するのが良いだろう. この二つのファイルの使い分けが効いてくるようなヤバイ設定はしないし、使い分けが面倒だという人は、~/.profile ファイルの中に

    1
    
    source ~/.bashrc

    と書き込んでおいて、設定自体は ~/.bashrc に書き込めば良いだろう. さて、PATH 追加のために書き込む内容だが、具体的には,これらのファイルのどちらかの中の適当なところに

    1
    2
    3
    4
    5
    
    if [ -d $HOME/bin ]
        then
        PATH=$PATH:$HOME/bin
        export PATH
    fi

    などと書き込んでおけば,次回ログイン時よりこれが有効になる.
    注: [] の前後に入っているスペースを除去しないこと! [ は一文字だが,コマンドなのだ. - この変更を有効にしよう.いったんログアウトしてから再ログインしても有効になるが、前にも記したように失敗していると危険なので、シェルからそのまま

    1
    
    source ~/.profile

    などと source コマンドで読みこめば設定がその場で有効になるのでそうしよう.

  実習

上の準備 1, 2, 3 を行っておこう.


シェルスクリプト利用の一連の流れ

awk スクリプトの時に解説したことから分かるだろうが、 シェルスクリプトを作って使うには、いつも、次の二段階の作業をしておけば良い. スクリプトを作ったら忘れずにこうしよう.

  1. シェルスクリプトを作り,上で用意したディレクトリ ~/bin にいれておく.
  2. スクリプトファイルに実行許可を与える.
    例えば,スクリプトファイルが dummy という名前ならば

    1
    
    chmod u+x ~/bin/dummy

    とすれば良い.


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

    スクリプトファイルのコマンド化 を用いてシェルスクリプトは作成される.
    スクリプトの先頭行(シバン shbang)にどう書くかであるが,通常は

    1
    
    #!/bin/sh

と書けば良いだろう.
スクリプトの内容を実行しないで,文法チェックだけ行いたい場合は,

1
#!/bin/sh -n

と先頭行に書けばよい. ファイルの削除など,やや危険な作業を行うスクリプトはこれでチェックすると良いだろう.
実行時にスクリプトの内容も表示したい場合は,

1
#!/bin/sh -v

と先頭行に書けばよい. スクリプトの動作が望みのものと違う時はこれでチェックするのも良いだろう.

  実習

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

    1
    2
    
    #!/bin/sh
    echo "Hello, world!"
    • そして、上の シェルスクリプト利用の一連の流れ の 1,2 相当を行おう.
    • そして、どのディレクトリからでもよいので, sh Hello と打ち込んで,作成したシェルスクリプトが動作することを確認しよう.


コマンドの連続実行

コマンドの連続実行等について 連続実行,grouping の部分で学習したが,ここで情報を再度書いておこう.

文法 解説
コマンド1 ; コマンド2; コマンド3 コマンド1 を実行後,コマンド2 を, さらにコマンド3 を… 実行する.
( コマンド1 ; コマンド2 ) 上とほぼ同様なのだが,
( ) の中のコマンドを「サブシェル」のもとで実行する(グループ化: 通常のシェルの場合).
サブシェルで行った環境変更はもとのシェルに影響しない.
よって,状態を一時的に変更して作業するときに便利.
コマンド1 && コマンド2 AND 実行.
コマンド1 を実行してみて,コマンド1 が成功したならばコマンド2 を実行する.
コマンド1 ¦¦ コマンド2 OR 実行.
コマンド1 を実行してみて,コマンド1 が失敗したならばコマンド2 を実行する.

グループ化の例:
例えば,

1
 (cd /tmp; ls -a) 

とすると,サブシェルは /tmp ディレクトリに移動して作業を行うが,作業が終わってサブシェルが終わり、戻ってきた元のシェルでのカレントディレクトリはもとのまま、である. こうしておけば、一時的な作業に伴う副作用が少なく、勘違いが減って間違いも経るだろう.

グループ化の例 2:
グループ化したコマンドの標準出力等は 合わさって出てくる ので、これが便利なことも多い. 例えば,

1
 (\ls /tmp; \ls /var) | less 

とすると,/tmp/var の2つのディレクトリにあるファイルのリストを一度に閲覧できる.


シェルスクリプトの文法

コメントアウト

シェルスクリプト中で “#” という文字があると, そこから行末までがコメントとして取り扱われる(ただし,文字列中は除く).

変数

変数の設定: シェル変数 で学んだように、シェルは変数を持つことが出来る. そしてもちろん、シェルスクリプトの中でもこうした変数が使える.

注1: 変数設定のとき,変数名と「=」の間にスペースを入れると失敗するぞ.
注2: 変数を参照するときは ${変数名} ({} で囲む)とした方が,より安全だぞ.

文字列

文字列は quotation で表す. この時, ' (シングルクォート) で囲むのと, " (ダブルクォート) で囲むのでは以下のように性質が異なるので注意が必要だ.

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

  実習

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

1
2
3
4
5
#!/bin/sh
# Greeting Script
g='Hello, world!'
d="today is `date +%m/%d`"
echo $g, $d.

そして,何がどうなっているのか理解しよう.
注: こうしたときに気を使わないといけないということもあり、(以前書いたように)コマンド置換は ` で囲む代わりに $( ) で囲んだ方が気が楽だ.


引数

引数とは,プログラムを起動するときに一緒に与える情報である. 例えば ls -a /tmp とプログラムを実行したら,通常は引数は /tmp の一つである( -a はオプション). ただし、広い意味で引数は -a , /tmp の二つととらえることもある.

この引数は以下のように扱われる.

■ sh 系シェルスクリプトでの引数の扱い ■
変数 意味
$# 引数の個数
$n n 番目の引数.
ただし,0番目の引数 $0 は特別で,シェルスクリプト自身の名前になる.
n > 9 の場合は ${12} などと,中カッコで囲んで使う.
$* 引数全て.
(参考) $argv{m-n} m 番目から n 番目までの引数の指定.
ただし Bsh系では有効ではないようだ.
bash などでは,引数の複雑な処理は getopts で行うのが良いだろう.

引数を処理する shift コマンド

shift というコマンドを実行すると,一つ目の引数の内容が消去され,二つ目以降が前にずれる. 言い換えると, \$1$2 の内容が入り, $2$3 の内容が入り… と いった動作になる. また,この結果としてオプションの数 $# は 1 減る.

  実習

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

1
2
3
4
5
#!/bin/sh
# Welcome Script
name=`whoami`
g="($1 says) Thank you, $name"
echo $g.

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

1
Welcome Smith

そして、なにがおきるか把握して、スクリプトの内容を読んで理解せよ.


入出力

シェルスクリプトが実行中に入出力を行うには,主に次のコマンドを用いる.

コマンド 解説
read 変数 入力.
ユーザからの(キーボード)入力を待ち,その結果を変数にいれる.
echo 出力したいもの 出力
echo -n 出力したいもの 出力.ただし,出力後に改行しない.

実行例:
入力と出力を使っているサンプルとして、下記のようなスクリプトを考えてみよう.

1
2
3
4
#!/bin/sh 
echo -n 'Please input something: '
read a
echo 'You input is ' $a

  実習

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

1
2
3
4
5
6
7
8
9
#!/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.

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


計算

bash はともかく, オリジナルの sh は計算機能をほとんど持っていない. そこで,互換性を鑑みたシェルスクリプト中で計算を行いたいときは、コマンド置換機能を利用して計算能力をもつ外部コマンドを呼び出す方法などが採られる.

具体的には,簡単な整数計算だけでよければ expr コマンドか bash 独自の算術式展開 $(()), fish 独自のコマンド math を,やや複雑な実数計算を行いたければ bc コマンドなどを使って次の例のようにする.

expr を使った例:

1
2
3
4
#!/bin/sh  
a=5
b=`expr $a + 10` ; echo $b
c=`expr $b \* 2` ; echo $c 

なお、最後の行のコマンド置換の中で * は展開されてファイル名などになってしまわないようにバックスラッシュをつけ * として、エスケープしている.

bash の機能である算術式展開 $(()) を使った例:

bash には算術式展開と呼ばれる、整数計算機能が付いている. 使い方は簡単で、整数の計算式を $(()) で囲むだけでその式が bash によって計算され、結果に置き換えられる.

1
2
3
4
#!/bin/bash  
a=5
b=$((a+10)) ; echo $b
c=$((b*2))  ; echo $c 

簡単だし、外部コマンドを呼ばないので計算は速い. bash しか使わないのであれば積極的に使っても良いだろう.

(おまけ) fish の機能である算術式展開 math を使った例:

独特かつ便利な使い勝手で知られるシェル fish には math という整数計算コマンドが付いている. まあこれはなんのことはない、オプション無しで呼びだされた bc コマンドに式を渡しているだけだ. 使い方は簡単で、bc が理解できる整数の計算式を math のあとに書くだけで良い. fish はコマンド置換を単なる ( ) で行うので、例は次のようになるだろう.

1
2
3
4
#!/bin/fish  
set a 5
set b (math $a+10) ; echo $b
set c (math $b\*2) ; echo $c 

外部コマンドを呼んでいるだけなので、やはり \* として * をエスケープする必要がある. なお、fish の文法はシェルの中ではだいぶ「筋が良い」.まあ、fish 自体がまだ流行っていないが…

  実習

上のスクリプトを試してみよう.ただし、自分で fish がインストールされている環境を用意する必要があるだろう.


bc -l (bc に ハイフン エル オプションをつけたもの)を使った例

bc は標準入力から計算式をもらって結果を標準出力に出せるので,例えば echo コマンドで式そのものを渡せば済む. なお、bc で小数計算を行うには -l (ハイフン エル) というオプションが必要なので忘れないようにしよう.

1
2
3
#!/bin/sh  
a=`echo "4*a(1.0)" | bc -l` ; echo $a
b=`echo "c($a)" | bc -l` ; echo $b

ちなみに,2行目では円周率 π を,3行目では cos(π) を計算している.

  実習

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


シェルの関数

Bsh 系シェルでは,「複数のコマンドをまとめて名前をつけたもの」を関数として自分で作ることができ、そしてもちろん、使える. 関数を作るには,

1
2
3
function 関数名(){
  コマンド…
}

という構文を利用する(実は function という文字列は省略できるが…). ちなみに,{ から } までの間で, 関数に対する n 番目の引数(スペース区切りで渡す)を $n として参照できる. また,この関数内部だけで「閉じた」変数を使いたければ、関数の内部で local を頭部につけて変数宣言をすればよい. 以下の例を見てみよう.

例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/sh

function countdown(){
  local i=$1
  while [ $i -ge 1 ]
    do
    echo $i
    i=`expr $i - 1`
    done
}

countdown 100

というシェルスクリプトはどういう動作をするか,考え,そして動かしてみよう. なお,while コマンドの意味と文法はこのすぐ後で学ぶ.気になる人はそこを読んでから戻ってこよう.

  実習

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/sh
# Greeting Script
g='Hello'
d=`date +%d`
dm=`expr $d - 1`
t=`date +%H`
m=`date +%M`

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

d2min $dm $t $m

echo $g, "about $min minutes have passed this month."

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


シェルの制御構造

ある程度複雑な動作をさせるには、「もし…ならば」とか,「…を繰り返して…」という制御が必要になるだろう. それには次のようにすれば良い.


“if” … (単純)条件分岐

条件に応じて(二つに)分岐していく. 具体的な構文は以下の通り.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if 条件1
    then コマンド1 …

    elif 条件2
        then コマンド2 …
    elif 条件3
        then コマンド3 …
    (以下, elifthen … を好きなだけ繰り返し)

    else コマンドN …
fi

条件 の書き方については,このすぐ後学ぶ. なお、elif 以下と else 以下は存在しなくても良い.

そして、上の構文での流れは以下の通りになる.

  1. 最初に 条件1 が実行 & チェックされ,
  2. 条件1 が成り立つ(= 実行が成功する) → コマンド1… が実行されて if 文は終了する.
  3. 条件1 が成り立たない → 次の elif へ.
    そこで条件2 がチェックされ,
    i. 条件2 が成り立つ → コマンド2… が実行されて if 文は終了する.
    ii. 条件2 が成り立たない → 次の elif へ.
    (… 以下,繰り返し …)
  4. else に来た場合.無条件で コマンドN が実行されて if 文は終了する.

  実習

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/bin/sh

fn=~/.bashrc

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


“case” … (パターンマッチ)条件分岐

ある「文字列」が用意したパターンとマッチするかどうかで分岐する. 単純な列挙の場合分けに便利. パターンは ¦ で区切って並列表記(or を意味する)することもできる.

具体的な構は文以下の通り.

1
2
3
4
5
6
7
8
case 文字列 in
  パターン1) コマンド1… ;;
  パターン2) コマンド2… ;;

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

  *) コマンドN…
esac

注: *) は「なににでもマッチする」.なお、この指定はしなくても良い.

流れは以下の通り.

  1. 最初に 文字列 を評価して,
  2. 文字列がパターン1 とマッチする → コマンド1… が実行されて case 文は終了する.
  3. マッチしない → 次のパターンとマッチするか (以下繰り返し)
  4. *) に来た場合.無条件で コマンドN が実行されて case 文は終了する.

  実習

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/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 を使うと良い. 具体的な構文は

for 変数 in パターンorリスト
do
  コマンド…
done

という感じで、動作としては以下のようになる.

  1. まず,in に続くパターンorリストを展開して,リストを生成する.
  2. そのリストの要素を順番に in の前の 変数 に代入して,そのたびに dodone の間のコマンド… を実行する.

  実習

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

1
2
3
4
5
6
7
#!/bin/sh

for x in {0..10}
do
  v=`echo "$x * 0.31415" | bc -l`
  echo "sin($v) = " `echo "s($v)" | bc -l`.
done


“while, until” … 条件チェックつきでの繰り返し.

繰り返すたびに、ループをぬけ出す条件をチェックするならばこれだ. 具体的な構文と動作の流れは以下の通り.

1
2
3
4
while 条件
do
  コマンド…
done

なお、until の構文も同じ. 動作は以下の様な感じだ.

  1. 条件が成り立つかチェックする.
  2. [ while の場合] 成り立つならば,
    [ until の場合] 成り立たないならば,
    コマンド… を実行してから 1. へ戻る.以下,繰り返し.

  実習

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
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” …ループ脱出.

時々必要なコマンド.このコマンドが実行されると,forwhile, until のループの中から脱出する.


“exit” … 終了.

シェルスクリプトを終了するコマンド.


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

これまで何回か出てきた条件式について述べよう. Bsh 系シェルでは,条件は基本的に

1
[ 条件式 ]

という形式で書かれる.

条件式の [ の周囲にはスペースが必要

この [ は条件式をチェックする機能を持つ、たった1文字の「コマンド」なので、スペース無しで前後になにかくっつけるとおかしなことになる.

そして、中身の条件式であるが,これは基本的に「単項演算子」か「二項演算子」と項目の組み合わせである.以下、みてみよう.

ファイルに関する条件式

条件式 意味
-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 のところの実習例などを参考にして,上の条件をなるべく全部試すスクリプトを作成してみよ.


レポート

以下の課題について能う限り賢明な調査と考察を行い,
AppliedMath7-Report-06
という題名をつけて e-mail にて教官宛にレポートとして提出せよ. なお,レポートを e-mail の代わりに TeX で作成した書面にて提出してもよい.

課題

  1. 引数1 に「金額」を,引数2 に「日数」を入れると, その金額でその日数だけお金を借りたら最終的にいくらになるかを計算するシェルスクリプトを作成せよ. ただし,金利は 10日で 1割の複利としておこう.
    なお、read コマンドなどを使わず,シェルスクリプトの引数をきちんと処理すること.
  2. ファイルを消去するシェルスクリプトを作成しよう. ただし,次のように「バックアップを用意する」ような動作をするようにせよ.
    1. 実行時にはまず、ファイルのバックアップファイルがあるかどうか調べ,
      • 無い場合は作成する.
      • 有るが、その数が3つ以下の場合は新たにバックアップファイルを作成する.
      • 有り、かつ、その数が3つより多い場合は、一番古いバックアップファイルを消去し、新たにバックアップファイルを作成する.
    2. 以上のバックアップ作業を行った後,ファイルを消去する.
  3. 自前の環境などにシェル fish をインストールして、その Ctrl-f 機能を試し、それについて感想を述べよう.
  4. これまでの授業課題で,シェルスクリプトを利用して解決できる課題があればそれを行え.