シェルのインタプリタとしての文法をまとめておく。 以下の説明は、 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 | シェル自身、またはシェルスクリプトのパス名に展開される。 |
$@ と $* の違いについては次のシェルスクリプトを利用して確実に理解しておきたい。
#!/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}${PATH:+:}/usr/local/bin
|
上記の str の部分は、必要とされたときに初めて評価される。 たとえば、
${foo:-`shutdown -h now`}
は、foo 変数に空でない文字列が入っていれば shutdown は実行されない。 バッククォートはコマンド実行した標準出力を文字列化した値に展開する(後述)。
パラメータの値展開のときに、定義されている値の一部を切り取ることができる。 変数の前後切り取り
${param%%pattern}
または
${param%pattern}
は、値の末尾から見てシェルパターン pattern にマッチする部分を除去した値に展開される。%% はパターンにマッチする最長部分を、% は最短部分を除去する。 利用例を示す。
x=/usr/local/bin/zsh
echo ${x%/*}
/usr/local/bin
y=a=b=c=d
echo $y
a=b=c=d
echo ${y%%=*}
a
= が複数あった場合に、最初の = を代入とみなす場合の例である。
${param##pattern}
または
${param#pattern}
は、値の先頭から見てシェルパターン pattern にマッチする部分を除去した値に展開される。## は最長部分を、# は最短部分を除去する。
x=/usr/local/bin/zsh
echo ${x##*/}
zsh
y=a=b=c=d
echo $y
a=b=c=d
echo ${y#*=}
b=c=d
コマンド起動は次のいずれかの形式を持つ。
コマンド名と0個以上の引数指定を含む。
例: somecommand -x a foo bar
すべてのコマンド起動の基本要素となる。 コマンド起動の前に変数への代入を0個以上付加することもできる。
変数=値 コマンド [ 引数並び ]
1個以上の単コマンド起動をパイプ記号(|)で結んだものである。
[!]単コマンド1 [ | 単コマンド2 ... ]
各パイプの前に書いたコマンドの標準出力と、 後ろに書いたコマンドの標準入力がつなががれる。 最後のコマンドの終了コードがパイプライン全体のものとなる。 パイプラインの前に ! 記号を前置すると、 終了コードの意味が反転される。
コマンド起動の最後に & をつけると、コマンド終了を待たず バックグラウンドで起動する。
コマンド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
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 コマンド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 はループを記述できる。
while コマンド
do
...
done
コマンド が真を返したら内部ブロックを実行しまた while に戻る。偽を返したら while 文を終了する。while 文全体の終了コードは内部ブロックが実行された場合はその最後の終了コード、 一度も実行されなかった場合は 0 である。
until はコマンド が偽を返す間繰り返すという点以外は while と同じである。
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 i [ in 単語... ]
do
...
done
指定した 単語 が展開された結果すべてを1つ1つシェル変数 i に代入して内部ブロックを繰り返し実行する。単語 を省略した場合はその時点で定義されている位置パラメータに対して繰り返す。
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=[]" とだけ出力される
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)file1 が file2 より新しければ真 |
file1 -ot file2 | (Older Than)file1 が file2 より古ければ真 |
str1 = str2 | str1 と str2 が文字列として等しければ真 |
str1 != str2 | str1 と str2 が文字列として異なっていれば真 |
n1 -eq n2 | n1 と n2 が数値として等しければ真(整数限定、以下同様) |
n1 -ne n2 | n1 と n2 が数値として異なれば真 |
n1 -gt n2 | n1 が n2 より数値的に大きければ真 |
n1 -ge n2 | n1 が n2 より数値的に大きいか等しければ真 |
n1 -lt n2 | n1 が n2 より数値的に小さければ真 |
n1 -le n2 | n1 が n2 より数値的に小さいか等しければ真 |
式1 -a 式2 | 式1 と 式2 がともに真なら真 |
式1 -o 式2 | 式1 と 式2 のいずれかが真なら真 |
( 式 ) | 式 が真なら真 |
オプションさえ覚えれば文法的には難しくない 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 | 入力用ファイル記述子 n1 に n2 を複写する(0)。 |
[n1]>&n2 | 出力用ファイル記述子 n1 に n2 を複写する(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 関数を呼び出す設定である。 各シグナルの意味はそれぞれ以下のとおりである。
シグナル | 数値による指定 | 意味 |
---|---|---|
EXIT | 0 | シェル(スクリプト)の終了時 |
HUP | 1 | ハングアップ(端末が失われたとき) |
INT | 2 | 割り込み(C-cで止めたとき) |
QUIT | 3 | 終了(C-\で止めたとき) |
TERM | 15 | 中断(killコマンドで送信するデフォルト) |
その他、シェルの実行環境に影響を与えるコマンドについて列挙する。
exec [ コマンド ]
exec(3) によってコマンドに現行プロセスを譲る。 対話的利用ではシェルを切り替えるときに利用する。
exec zsh
zsh%
シェルスクリプトからの利用では、 システムのログイン設定用スクリプトなどで、 必要な設定をすべて済ませたあと最後にデスクトップ環境や ウィンドウマネージャプログラムを呼ぶときに利用される。
現行シェルプロセスの umask(2) を設定する。 親プロセスから子へと継承されるものであるため、 シェルスクリプトを起動したプロセスの umask を引き継いで始まり、 シェルスクリプトから起動するすべてのコマンドやサブシェルに伝播する。 注意が必要なのはメイルシステムから起動した場合で、このとき umask は 0077 になっていて、このまま新規にファイル(ディレクトリ)作成すると 他者に読めないファイル属性になる。
シェル変数の操作をする。 引数なしで起動すると定義されているシェル変数一覧を出力する。 シェルスクリプトからの利用では、位置パラメータの設定が主なところで、
set -- 引数...
とすると、引数... を位置パラメータに設定する。 場合によっては配列代わりに使用できる。
変数に export 属性を付ける。シェル変数はシェル内だけのもので シェルから起動する他プロセスには伝わらないが、export 属性のある変数はいわゆる環境変数となり、起動する子プロセスに受け継がれる。
パイプラインを利用するとパイプ先はサブシェルで実行される(※)。 入力を順次読み取り処理を進めるプログラムでは、パイプの先でデータを受け取りたい。 そのような場合に、パイプの先で設定した変数を引続き利用したいことがある。 たとえば次のような形式である。
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() (
# ここで使用した変数は関数を抜けるとすべて消える
)
「関数から抜けて現行環境に渡したい変数」を使いたいのだとしたら 大抵悪い設計である。 グローバル変数はそもそも極力避けるべきだし、どうしても必要だとしても 関数を越えて持ち歩く行った先の関数で再定義するのはバグの温床である。 もし、現行シェル環境に変数値を持ち帰りたいなら、 変数の代入操作自体が呼び出し元になるようにする。 そのための方法はいくつかある。
var=`func1 ..` のようにバッククォートで設定すべき値を受ける。
func1() {
if some_condition blah blah blah...; then
echo value-1
else
echo value-2
fi
}
...
var=`func1` # func1 で echo した値が入る
関数側で echo で返したものを代入できる。
グローバル変数ではなくテンポラリファイル等の利用を検討する。
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 を存分に使用して説明を進める。