シェルスクリプトの最低限の知識

シェルのインタプリタとしての文法をまとめておく。 以下の説明は、 The Open Group Base Specifications Issue 6 IEEE Std 1003.1, 2004 Edition (http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html) に準拠するシェルを想定したものとなっている。 一部のシェルは仕様が異なる部分があることに注意する必要がある。

パラメータと変数

$ 記号にシンボルを続けるとパラメータ として別の値に展開される。パラメータのうち、 シンボルが英数字とアンダースコア(_)だけ(ただし先頭は数字以外)で構成されるものを 変数という。

特殊パラメータ

シェルには特別な意味を持つパラメータがある。 $* $@ $# $? $- $$ $! $0

1, 2, 3, ... 1以上の整数は位置パラメータ といいシェルスクリプト起動時に与えられたコマンドライン引数の1番目、 2番目、3番目、... の値が順に入る。
*位置パラメータすべてに展開される。 ダブルクォート内で用いた場合はそれらすべてを空白区切りで 結合した単一文字列に展開される。空白区切りは 変数 IFS の1文字目で変更できる。
@位置パラメータすべてに展開される。 ダブルクォート内で用いた場合は位置パラメータすべてを 別々の文字列として展開される。
#位置パラメータの個数に展開される。
?直前のパイプライン(後述)の終了コードに展開される。
-シェル自身に与えられたオプションフラグ文字に展開される。
$シェル自身のプロセスID(PID)に展開される。
!直前に起動されたバックグラウンドプロセスの PID に展開される。
0シェル自身、またはシェルスクリプトのパス名に展開される。

$@ と $* の違いについては次のシェルスクリプトを利用して確実に理解しておきたい。

args.sh

#!/bin/sh
arginfo() {	# 関数(やコマンド)に与えた引数の状態を調べるための関数
  n=1
  for i; do	# 第1引数から最後の引数までを1つずつ取り出してprintfする
    printf "%2d: %s\n" $n "$i"
    n=$((n+1))
  done
}
echo '$* expansion'
arginfo $*
echo '"$*" expansion'
arginfo "$*"
echo '$@ expansion'
arginfo $@
echo '"$@" expansion'
arginfo "$@"

3つの引数「yes」、「Let's try」、「This is a pen.」 を与えて起動してみた結果を示す。

./args.sh yes "Let's try" This\ is" a pen."
$* expansion
 1: yes
 2: Let's
 3: try
 4: This
 5: is
 6: a
 7: pen.
"$*" expansion
 1: yes Let's try This is a pen.
$@ expansion
 1: yes
 2: Let's
 3: try
 4: This
 5: is
 6: a
 7: pen.
"$@" expansion
 1: yes
 2: Let's try
 3: This is a pen.

$* と $@ いずれもダブルクォート外では空白ごとに別々の文字列に展開されるが、 ダブルクォート内では、位置パラメータごとに別々の引数に展開される。 引数の個数を保ちたい場合は "$@" を利用する。

次の変数はシェルスクリプトの実行に重要である。

IFS 単語(引数)区切りとみなす文字を列挙した値を格納する。 デフォルトは空白(ASCIIコード=0x20)、タブ(0x08)、改行(0x0a) の3文字の並びである。IFSが未定義のときはデフォルト値にしたがう。
PATH コマンド検索パス。入力されたコマンドにスラッシュが含まれない場合に どのディレクトリを探すかをコロン区切りで列挙しておく。 シェルスクリプト起動時にはそれを起動したプロセスの $PATH が継承されているため予定外のコマンドが起動される可能性に注意する。 必要ならスクリプトの先頭で PATH の値を定義しておく。
PWD 現在の作業ディレクトリをつねに保持している。

変数への代入

変数への代入は

変数=

の書式で行なう。イコールの前後に空白を入れない(変数の部分が 実行コマンドと解釈されてしまうため)。

変数=

とすると、変数の値を空文字列にするが変数自体は存在する。 変数定義そのものを抹消したい場合は unset 変数 とする。

位置パラメータの操作

位置パラメータは、シェルスクリプト起動時に自動的にセットされるが スクリプト起動途中にセットし直すこともできる。 位置パラメータへの代入

set -- 引数並び...

とすると、任意個並んだ 引数並び を順に位置パラメータにセットする。それまでの値は捨てられる。

shift コマンドは、先頭の位置パラメータの $1 を捨て、$2 を $1 に、$3 を $2 に、…、と順に前に詰める。shift N と自然数を指定するとその数だけ前に詰める。

パラメータ展開

$ 記号によりパラメータ展開がされる。パラメータ(変数)名の境界を 明確化するために中括弧 { } を用いることができる。 たとえば変数 foo の展開直後に文字列 "bar" を記したければ ${foo}bar と書けばよい。また、中括弧を用いた展開では展開結果を 操作するためのフラグを利用できる。

${param:-str} ${param-str} デフォルト値の供給。
paramが未定義か空文字列なら str に、それ以外は変数の値に展開する。 :- の部分をコロンなしの - にすると変数が未定義の場合のみ str になる。以後、コロンの有無の違いは 空文字列を未定義として扱うか、そのままで扱うかの違いである。
${param:=str} ${param=str} デフォルト値の代入。
paramが未定義か空文字列なら変数に str を代入しその値に、それ以外は変数の値に展開する。 位置パラメータなどの特殊パラメータには代入できない。 変数へのデフォルト値の代入のみを行ないたい場合は
: ${parameter:=initial}
のように書く。コロンコマンドは必ず真を返し引数を受け流すだけのものである。
${param:?str} ${param?str} 必須変数の検査。
paramが未定義か空文字列ならエラーメッセージとして str を出力してシェルをexitする。それ以外は変数の値に展開する。 str は省略でき、その場合はデフォルトのエラーメッセージとなる。
${param:+str} ${param+str} 条件つき文字列展開。
paramが未定義か空文字列なら空文字列に展開し、 それ以外は str に展開する。 典型的には区切り文字を利用して複数の値を 保持する変数の区切り文字の必要性を場合分けするときに使用する。たとえば PATH 変数はコロン区切りでコマンド検索パスを保持するが、 これに何かの値を足そうとして
PATH=${PATH}:/usr/local/bin
とすると元のPATHの値が空だったときに PATH=:/usr/local/bin となる。 これを防ぐには「PATHが空文字列でない場合のみ : に展開する」ように、
PATH=${PATH}${PATH:+:}/usr/local/bin
とする。

上記の str の部分は、必要とされたときに初めて評価される。 たとえば、

${foo:-`shutdown -h now`}

は、foo 変数に空でない文字列が入っていれば shutdown は実行されない。 バッククォートはコマンド実行した標準出力を文字列化した値に展開する(後述)。

パラメータ置換時の部分文字列取得

パラメータの値展開のときに、定義されている値の一部を切り取ることができる。 変数の前後切り取り

%%、% - 末尾切り取り(先頭残し)

${param%%pattern}
または
${param%pattern}

は、値の末尾から見てシェルパターン pattern にマッチする部分を除去した値に展開される。%% はパターンにマッチする最長部分を、% は最短部分を除去する。 利用例を示す。

##、# - 先頭切り取り(末尾残し)

${param##pattern}
または
${param#pattern}

は、値の先頭から見てシェルパターン pattern にマッチする部分を除去した値に展開される。## は最長部分を、# は最短部分を除去する。

シェルコマンド

コマンド起動は次のいずれかの形式を持つ。

バックグラウンド実行

コマンド起動の最後に & をつけると、コマンド終了を待たず バックグラウンドで起動する。

コマンド1 & [ コマンド2 & ... ]

連続実行

コマンド起動の後ろに ; (セミコロン)をつけて、 同じ行に次のコマンド起動を記述できる。

グルーピング

複数のコマンドをまとめてグループ化できる。

( コマンドリスト ) コマンドリストをサブシェル環境で実行する。 サブシェルはfork()した先の別プロセスで実行されるため、 サブシェル内で設定した変数はサブシェル実行終了と同時に消え、 元のシェルには伝播しない。
{ コマンドリスト; } コマンドリストを現行シェル環境で実行する。 グループ内で設定した変数はグループ終了時も残る。 ただし、グループをパイプの1要素としたときはサブシェル化されるので 変数値やカレントディレクトリを変更しても現行シェルに影響を与えない。

パイプラインの有無による挙動を例示する。

cd /
: パイプなし  ( ) のグルーピング
(cd /tmp; echo yeah)
yeah
pwd
/
: パイプなし  { } のグルーピング 要空白と末尾セミコロン
{ cd /tmp; echo yeah;}
yeah
: 現行シェル環境での実行なのでPWDが変わる
pwd
/tmp
: パイプあり  { } のグルーピング
{ cd /; echo yeah;} | cat
yeah
: サブシェル環境での実行なので現行シェルでは /tmp のまま
pwd
/tmp

複数のコマンド出力をまとめてファイルにリダイレクト(後述)するときに { } が利用できる。

{ echo "===== `date` ====="
  command1
  command2
} > output.txt

AND/OR

2つのコマンドを && や || でつなぐと条件つき連続実行となる。

コマンド1 && コマンド2

&& では、コマンド1 の実行が真(終了コードが 0)の場合に限って コマンド2 を実行する。

コマンド1 || コマンド2

|| では、コマンド1 の実行が偽(終了コードが 0 以外)の場合に限って コマンド2 を実行する。2つのオペレータは同じ優先順位を持ち、 左にある結合から順に評価されるため、

A || B && C

とした場合は、下線部が先に評価され &&C に進む。

true || echo yes && echo YES
YES
false || echo yes && echo YES
yes
YES

制御構造

if, while, case, for 文は制御構造でもあり、 グループ化された文(複文)としても振る舞う。

if

if は条件分岐を記述できる。

if コマンド1
then  ブロック1 # コマンド1が真のときに実行されるブロック
elif コマンド2
then  ブロック2 # コマンド2が真のときに実行されるブロック
  :
else
  ブロックELSE  # 上記コマンドすべて偽のときに実行されるブロック
fi

コマンド1 などの部分は単コマンド、 グループ化された文いずれも該当する。

if false || false || false || true; then
  echo TRUE
else
  echo FALSE
fi
TRUE

if 文全体の終了コードは最後に実行されたコマンドの終了コードである。

while, until

while はループを記述できる。

while コマンド
do
  ...
done

コマンド が真を返したら内部ブロックを実行しまた while に戻る。偽を返したら while 文を終了する。while 文全体の終了コードは内部ブロックが実行された場合はその最後の終了コード、 一度も実行されなかった場合は 0 である。

untilコマンド が偽を返す間繰り返すという点以外は while と同じである。

case

case はパターンマッチの利用できる値選択分岐を記述できる。

case 文字列 in
  パターン1) ブロック1 ;;
  パターン2) ブロック2 ;;
     :
esac

esac 直前の ;; は省略可能である。 パターンにはシェルのメタキャラクタを利用したワイルドカード規則が使える。

? 任意の1字にマッチ
* 任意の文字列にマッチ
[ ... ] 文字クラス

パターンは、f*|x* のように縦棒で区切って複数指定できる。また、 パターンマッチはファイル名マッチと違い、スラッシュを特別扱いしない。 たとえば、ファイル名マッチで s* としても subdir/file にはマッチしないが、 case のパターンマッチの 文字列 が foo/bar だった場合 f* というパターンでもマッチする。

文字クラス指定では、括弧内に列挙したどれか1字にマッチを意味する。 [0123456789] はASCII数字のいずれか1字とのマッチを意味する。 文字コードが連続するものはハイフンでまとめて [0-9] のように書ける。 意味の反転、つまり「どれか1字にマッチしない」 場合の指示は括弧内の先頭に ! を指定する。たとえば、[!a-z] は小文字英字以外の1字にマッチ、を意味する。

case 全体の終了コードは、どのパターンもマッチしなかった場合は 0、 それ以外は最後に実行されたコマンドのものとなる。

for

for は単語群に対する繰り返しを記述できる。

for i [ in 単語... ]
do
  ...
done

指定した 単語 が展開された結果すべてを1つ1つシェル変数 i に代入して内部ブロックを繰り返し実行する。単語 を省略した場合はその時点で定義されている位置パラメータに対して繰り返す。

break, continue

while, for のループは、break で中断、 continue で次回繰り返しに飛ぶことができる。いずれも自然数 n を後置すると 内側から数えて n 段階のループへの適用となる。たとえば「break 3」とするとその位置から数えて外側3つめにあるループを抜ける。

制御構造も文であること

制御構造の判定部分に別の制御構造を入れたり、 制御構造をパイプラインの一要素としても正しく実行される。 いくつか例を示す。

while read x &&	 # 標準入力から1行読み x に入れる
      case $x in	 # read x が成功したら case に進む
	  [Yy]*) true ;; # Yかyで始まる文字列なら true
	  *) false    ;; # そうでなければ false
      esac
do			 # Yかyで始まる文字を入れ続ける限り
  echo はい!		 #「はい!」と出し続ける
done

while の条件指定部分に && により連結されたコマンド実行を指定している。 次の例は無意味だがパイプの途中で起動するコマンドを切り替えている。

cal | { echo "行番号をつける=1、つけない=2" >&2 # 標準エラー出力へ
	read n < /dev/tty	# 標準入力はパイプなので端末は /dev/tty
	case $n in
	  1) cat -n ;;
	  *) cat ;;
	esac } | sed 's/^/]/'	# パイプの途中の { } はサブシェル

echo "n=[$n]"		#  変数nは残らず "n=[]" とだけ出力される

testコマンド

if や while の条件を指定するために test コマンドが多用される。

test 
[  ]

いずれかの書式で用いて の部分を評価した結果を真(終了コード 0)、偽(終了コード 1)で返す。詳細はシステム付属のマニュアル test(1) に譲り、ここでは本書で多用するもの、それに関連するものを抜粋して示す。

-d file file が存在するディレクトリなら真
-e file file が存在するなら真(ファイルの種別問わず)
-f file file が存在する通常ファイルなら真
-r file file が読み取り可能なら真
-w file file の書き込み属性があれば真(readonly マウントでも真を返す)
-x file file の実行属性があれば真
-s file file が1バイト以上のファイルなら真
-n str str が空文字列でなければ真
-z str str が空文字列なら真
file1 -nt file2 (Newer Than)file1file2 より新しければ真
file1 -ot file2 (Older Than)file1file2 より古ければ真
str1 = str2 str1str2 が文字列として等しければ真
str1 != str2 str1str2 が文字列として異なっていれば真
n1 -eq n2 n1n2 が数値として等しければ真(整数限定、以下同様)
n1 -ne n2 n1n2 が数値として異なれば真
n1 -gt n2 n1n2 より数値的に大きければ真
n1 -ge n2 n1n2 より数値的に大きいか等しければ真
n1 -lt n2 n1n2 より数値的に小さければ真
n1 -le n2 n1n2 より数値的に小さいか等しければ真
1 -a 2 12 がともに真なら真
1 -o 2 12 のいずれかが真なら真
( ) が真なら真

オプションさえ覚えれば文法的には難しくない test コマンドだが使用時に注意しなければならない点がある。

testコマンドの癖

test コマンドはハイフンで始まるオプションで評価処理を切り替える。 それゆえ評価対象の文字列がハイフンで始まるとオプションと誤判定される。 たとえば、シェル変数 v の値が yes か否かを判定したい場合。 自然に考えると

if [ $v = yes ]; then
  ...
fi

と書きたくなるが、v="no = no -o foo" と代入されていると上記の if は

if [ no = no -o foo = yes ]; then

となり、test コマンドは真を返す。$v が複数の引数に展開されないよう クォートして

if [ "$v" = yes ]; then

と書き変えたとする。これも不十分で v='!' でこれを実行すると test コマンドには

! = yes

という式が渡るので

test: argument expected

のようなエラーとなる。これらの問題を解決するには、 文字列比較には、「オプション解釈されないダミー文字列を前置」し、 「変数展開はダブルクォート」する。つまり以下のようにするのが定石である。

if [ x"$v" = x"yes" ]; then

yes はクォートの必要はないが、敢えて括ることでダミー文字列 x との違いを明確化できる。

クォート規則

特殊文字の持つ特別な意味を消したいときは、バックスラッシュ、シングルクォート、 ダブルクォートのいずれかでクォートする。

バックスラッシュ

バックスラッシュの次の文字の特別な働きを消す。

echo a          b         c
a b c
echo a\ \ \ \ \ \ \ \ \ \ b         c
a          b c

スペースは通常引数を区切る働きを持ち、何個連ねても区切りの働きは1つである。

シングルクォート

2つのシングルクォートの間の文字の持つすべての特別な働きを消す。

echo The value "of" HOME is   $HOME
The value of HOME is /home/yuuji
echo 'The value "of" HOME is   $HOME'
The value "of" HOME is   $HOME

シングルクォート文字そのものだけはシングルクォートでクォートできない。

ダブルクォート

2つのダブルクォートの間の文字のうち、 $、`(バッククォート)、\(バックスラッシュ) 以外の文字の持つ特殊な意味を消す。つまりダブルクォート内でも $ によるパラメータ展開、バッククォートによるコマンド出力置換は機能する。

ダブルクォート内のバックスラッシュは、$、バッククォート、 ダブルクォート、改行文字 だけに作用する。

echo  1\2 \ 1\\2
12  1\2
echo "1\2 \ 1\\2"
1\2 \ 1\2
echo "ab
cd\
ef"
ab
cdef

コマンド置換

既に例に登場しているが、 バッククォートは括られた文字列をコマンドとしてサブシェル環境で起動し、 得られた出力(標準出力に出されたもの)に置き換えられる。

: $SHELL が /bin/sh の場合
dirname $SHELL
/bin
cd `dirname $SHELL`
: → /bin に移動する

バッククォートによるコマンド置換は \` でネストできる。

: シェルスクリプトの存在する実ディレクトリを得る定石
: $0 にある起動スクリプト名のディレクトリ部分を利用する
mydir=`cd \`dirname $0\`; pwd -P`
: → cd `dirname $0` してから pwd -P した結果を mydir 変数に代入

コマンド置換は $(コマンド) でも記述でき、 この場合は任意にネストできる。

コマンド置換では出力文字列の末尾が改行文字だった場合は1つだけ削除される。

コマンド置換のフィールド分割の挙動を正確に把握することは重要である。 コマンド置換を代入で用いたときは単一文字列のように振る舞うが、 コマンド置換をダブルクォートせずに、コマンド引数位置に書くと、 IFS 変数の値に応じて複数の引数に分割される。

: 代入の右辺は空白分割できない
x=a b c
b: not found
echo a b c
a b c
: コマンド置換では1つの文字列として代入できる
x=`echo a b c`
echo $x
a b c
: しかし引数として渡すと展開後が空白($IFS)で引数分割される
test `echo a b c` = "a b c" && echo OK
test: b: unexpected operator
: これは test a b c = "a b c" と展開されるからである。
: 1引数にするにはダブルクォートで括る必要がある
test "`echo a b c`" = "a b c" && echo OK
OK

この性質を利用すると出力から改行文字を除去できる

zshの変数展開の単語分割 zsh では setopt sh_word_split するか、 $変数 の部分を ${=変数} に置き換えると同じ結果が得られる。

printf 'a\nb c'
a
b c
x=$(printf 'a\nb c')
: x自体には改行文字が含まれる。
echo 1:$x 2:"$x"
1:a b c 2:a
b c
: echo $x は改行文字を引数区切りとして処理するので3引数になるが、
: "$x" は改行と空白をまとめて1引数のままにする。
x=$(echo $(printf 'a\nb c'))
: echoの引数になった時点で改行は引数分割として使われて消える。
echo 1:$x 2:"$x"
1:a b c 2:a b c

注意点をまとめておく。

また、$(...) 内にサブシェルコマンドを書く場合は、

x=$( (command) )

と括弧の内側に空白をおいて、以下で述べる算術展開の記法と同じにならないように注意する必要がある。

算術展開

整数や、整数を値に持つ変数で整数範囲で計算した結果を得ることができる。

i=10
echo $(($i*2)) $((i*2)) $((i/3)) $((i%3))
20 20 3 1

$((...)) の内側の変数は $ なしでも値に展開される。

リダイレクト

入出力切り替え

ファイル記述子 n 番の入出力先を切り替えることができる。 表中の括弧内の整数は n を省略したときの番号である。

[n]> file ファイル記述子 n の出力先を file にする(1)。
[n]>| file ファイル記述子 n の出力先を file にする(1)。シェルの -C オプションが有効でもファイルを上書きする。
[n]>> file ファイル記述子 n の出力先を file にする(1)。内容はファイルの末尾に追記する。
[n]< file ファイル記述子 n の入力を file にする(0)。
[n1]<&n2 入力用ファイル記述子 n1n2 を複写する(0)。
[n1]>&n2 出力用ファイル記述子 n1n2 を複写する(1)。
[n]<&- 入力用ファイル記述子 n を閉じる(0)。
[n]>&- 出力用ファイル記述子 n を閉じる(1)。

これらの切り替えはコマンド行のどこにでも置ける。

> file echo foo bar
echo > file foo bar
echo foo > file bar
echo foo bar > file

上記いずれも "foo bar" が file に出力される。

execによる入出力切り替え 入出力切り替えは同時に起動するコマンド環境のみの適用で、 次のコマンドラインには影響が残らないものだが、 シェル組込みコマンド exec を使うと恒久的な切り替えができる。 exec は直後にコマンドを付けて呼ぶとシェルの現行プロセスをそのコマンドに 置き換える(execする)が、コマンドを付けずに入出力切り替えだけを行なうと、 シェルにとっての恒久的なファイル記述子の切り替えを行なう。

exec > logfile

とすると以後そのシェルのすべての標準出力が logfile に書かれる。 インタラクティブシェルでこれを行なうと以後の出力が画面に得られないので注意する。 また、サブシェルでこの切り替えを行なうと、サブシェルが終了するまでの適用になる。 たとえば、以下のようにするとサブシェルで起動するコマンドのログを取ることができる。

( exec > logfile
  date
  ls)

先頭の exec によりサブシェル環境の標準出力が logfile になり、date と ls いずれの出力もそのファイルに行く。

ヒアドキュメント

ヒアドキュメントはシェルスクリプトでは多用するので活用したい。

基本書式

[n]<<WORD

とすると、次に WORD が現れる行までの内容を ファイル記述子 n の入力とする。n を省略した場合は標準入力への内容となる。

wc -l<<EOF
The quick brown fox jumps over the lazy dog.
That's all.
EOF

は、下線部の内容が wc -l に送られる。区切り単語に挟まれたデータ部分では、 $ によるパラメータ展開と、コマンド置換が利用できるが、 << の直後に書く単語をクォートすると展開されずそのまま送られる。


cat<<EOF
User=$USER, PWD=`pwd`
EOF
User=yuuji, PWD=/home/yuuji
: (1)バックスラッシュでクォート
cat<<\EOF
User=$USER, PWD=`pwd`
EOF
User=$USER, PWD=`pwd`
: (2)ダブルクォートでクォート(シングルクォートでも同様)
cat<<"EOF"
User=$USER, PWD=`pwd`
EOF
User=$USER, PWD=`pwd`

ヒアドキュメントの連続

1行にヒアドキュメント指定を2つ以上書くことも出来る。 1つめのデータ記述が終わったらすぐ次のものを書けばよい。

cat<<EOM; cat<<EOM;
Hello,
EOM
World!
EOM

ヒアドキュメントからの連続パイプ

SQLをシェルスクリプトから利用する場合は、 問い合わせ文をヒアドキュメントで sqlite3 コマンドに供給し、得られた問い合わせ結果を さらに別のフィルタコマンドに与える使い方を多用する。 フィルタ処理がそれなりに長くなる場合は次のようにヒアドキュメント指定行を パイプ記号だけで終わらせておき、データ記述の次の行に続きを書くとよい。

sqlite3 database.sq3<<endSQL |
SELECT uid,uname,login,logout
FROM users u LEFT JOIN lastlog l USING uid;
endSQL
awk -F'|' ... | 次のフィルタ | さらに次のフィルタ

上記の例では、awk コマンドからのパイプラインは sqlite3 からの出力を受け取って動く。

ヒアドキュメントとソースのインデント

ヒアドキュメントの区切りを示す単語をハイフン(-)で始めると、 データ終端までの各行の先頭から連続するタブ文字を除去したデータになる。

# TAB除去ありの場合
while true; do
  cat<<-eof
	おはよう!
	eof
done
# TAB除去なしの場合
while true; do
  cat<<eof
おはよう!
eof
done

終端にはハイフンを含めない。 インデントしている行から始まる場合には便利である。 先頭以外のタブや、スペースは削除されない。

シェル関数

一連の処理を関数として名前つき手続きにできる。 関数定義は以下の書式で行なう。

関数名() 

は単文、またはグルーピングした複文が書ける。 下記いずれも関数定義である(※)。

sql()	sqlite3 -cmd ".timeout 3000" database.sq3 "$@"

args()	for i
	do echo "$i"
	done

japan()	{ unset LC_ALL; export LANG=ja_JP.UTF-8; }

sumcsv() (
  IFS=',' # フィールド区切り文字をいきなり変更。CSVを読むのでカンマにする。
  [ -n "$1" ] && exec < $1	# 引数あれば標準入力を置き換える
  while read id name point; do	# ID,氏名,得点   の並びを想定
    sum=$((${sum:-0} + point))	# ${sum:-0} は sum 未定義なら0
    n=$((${n:-0} + 1))
  done
  printf "%d人受験、合計=%d\t平均=%4.2f\n" $n $sum `echo "$sum/$n"|bc -l`
)

※ bash では関数本体定義に単文を書くことはできない。 1文だけでも { ...; } で括る必要がある。

1つめの sql() は、単コマンドを呼ぶ関数である。 関数への引数は(その関数ローカルの)位置パラメータとして渡されるので、 "$@" が、関数に渡されたすべての引数に展開される。

2つめの args() は、構成物が for 文である。for はそれ全体で一かたまりの複文である。

3つめの japan() は { } によるグルーピングであるから、 呼び出し元がパイプやバッククォートなどのサブシェル起動でなければ、 関数本体も現行シェルで実行されるので、 設定したシェル変数は呼び出し元でも継続して利用できる。

最後の sumcsv() は、( ) によってサブシェル化された状態ですべての 文が実行されるため、呼び出し方法に依らずつねにサブシェル環境で実行される。 よって、関数内で代入されている IFS、id、name、point、sum、n 変数は呼び出し元には残らない。また、第1引数($1)が与えられている場合に 関数内の exec で標準入力をそのファイルに切り替えているが、 これも大元のシェルには影響しない。

CPUの速度とメモリが十分な現在では、 サブシェルを多用してもシステムに過重な負担を掛けることもなく、 マルチコアを利用できる側面もあるので、サブシェルはうまく活用したい。

シグナル処理

シェルスクリプトは通常終了することもあれば、突然止められたりすることもある。 たとえば処理中に作成した一時ファイルなどは、 スクリプト終了時にはきれいに消すなど後片付けの必要があるが、C-c などで止められた場合にもしっかり消す必要がある。 スクリプト実行時のシグナルハンドラを設定することでこれらのことが可能となる。 シグナル捕捉処理の登録は trap で行なう。

trap アクション シグナル...

一時ファイル処理

一時ファイルを作り、終了時に後始末する流れを示す。 一時ファイルを複数作る可能性も考えて、 ファイルではなくディレクトリを作成して利用する。

tmpd=`TMPDIR=${TMPDIR:-/tmp} mktemp -d -t myname.XXXXXX` || exit 1
cleanup() {
  rm -fr $tmpd
  # その他もろもろの終了処理
}
trap cleanup EXIT INT HUP QUIT TERM

この例は cleanup 関数を呼び出す設定である。 各シグナルの意味はそれぞれ以下のとおりである。

シグナル数値による指定意味
EXIT0シェル(スクリプト)の終了時
HUP1ハングアップ(端末が失われたとき)
INT2割り込み(C-cで止めたとき)
QUIT3終了(C-\で止めたとき)
TERM15中断(killコマンドで送信するデフォルト)

実行環境

その他、シェルの実行環境に影響を与えるコマンドについて列挙する。

サブシェルとの戦い

パイプラインを利用するとパイプ先はサブシェルで実行される(※)。 入力を順次読み取り処理を進めるプログラムでは、パイプの先でデータを受け取りたい。 そのような場合に、パイプの先で設定した変数を引続き利用したいことがある。 たとえば次のような形式である。

kshやzshのようにパイプライン最後のコマンド列が 現行シェル環境で実行されるものもあるが、シェルスクリプト化の際は そうでないシェルでも動くようにしたいため、つねに「パイプの先はサブシェル」 と考えることにする。

count=0
cmd1 | cmd2 ... | while read x y z;
do if 条件; then
     count=$((count+1))
   fi
done
echo ${count}件処理しました

この結果は必ず「0件処理しました」となる。while はサブシェルで動くので、 内部で変更されている count 変数は現行シェル環境には影響しない。

多くの人がこの問題に苦しんでいるが、解決法はとても簡単で、 「パイプの先を『文』でなく『複文』にする」だけでよい。

cmd1 | cmd2 ... | {
  count=0
  while read x y z;
  do if 条件; then
       count=$((count+1))
     fi
  done
  echo ${count}件処理しました
}

グルーピングから抜けたら count 変数は消滅するが、 むしろグローバル変数が増えないことの方がメリットとして大きい。

関連して、ローカル変数を作るために一部のシェルでは local コマンドで関数ローカルの変数宣言できる。しかし、 これを多用するシェルスクリプトが可搬性を損ねて苦労することがある。 可搬性の高いものを作りたければ、関数定義をサブシェル化すればよい。

func1() (
  # ここで使用した変数は関数を抜けるとすべて消える
)

「関数から抜けて現行環境に渡したい変数」を使いたいのだとしたら 大抵悪い設計である。 グローバル変数はそもそも極力避けるべきだし、どうしても必要だとしても 関数を越えて持ち歩く行った先の関数で再定義するのはバグの温床である。 もし、現行シェル環境に変数値を持ち帰りたいなら、 変数の代入操作自体が呼び出し元になるようにする。 そのための方法はいくつかある。

  1. 値が1つの場合

    var=`func1 ..` のようにバッククォートで設定すべき値を受ける。

    func1() {
      if some_condition blah blah blah...; then
        echo value-1
      else
        echo value-2
      fi
    }
    ...
    var=`func1`			# func1 で echo した値が入る
    

    関数側で echo で返したものを代入できる。

  2. 値が複数の場合

    グローバル変数ではなくテンポラリファイル等の利用を検討する。

    tmpdir=`mktemp -d -t mytmp.XXXXXX` || exit 1
    trap "rm -r $tmpdir" EXIT INT HUP QUIT TERM
    func2() {
      echo value1 > $tmpdir/var1
      echo value2 > $tmpdir/var2
      echo value3 > $tmpdir/var3
    }
    
    func2
    echo "var1 = `cat $tmpdir/var1`"
    

2のテンポラリファイルもメモリディスク化された /tmp を利用することにより、速度上極端に不利にはならないだろう。 もっともそこまで速度を重視する用途では最初から別の言語で書くだろう。 程々の速度でよいシェルスクリプトであれば、SQLite データベースはテンポラリファイルの代替用途としても使える。 3章以降では一時的な値保存に SQLite を存分に使用して説明を進める。