シェルプログラミングの基礎知識

UNIX のコマンドインタプリタである Bourne shell を知っておかないといろいろ困るので基本的なことをメモしておく。ここでは、コマンドをバッチ処理するための シェルスクリプトについて記述しています。

* #!/bin/sh

UNIXでスクリプトを記述する場合、一番初めの行は #!ではじめます。さらに、シェルスクリプトであることを明確にするためにコマンドをフルパスで記述します。シェルスクリプトでは、#!/bin/sh となります。Bourne shell の場合、初めの行が : (コロン)のみでもよいが、現在あまりこの記述は使われていない。また、#! /bin/sh というように #! の後に半角スペースを入れても動作する UNIX システムもあるが、互換性を考えると半角スペースは入れないほうがよい。

* コメント行と改行

# 以降コメントとなり、\ を利用して複数行を1行とみなすことができます。

#!/bin/sh
# コメント
echo "Hello, \
bourne shell world."
exit 0

という hello.sh スクリプトがある場合、以下のように評価されます。

$ chmod 755 hello.sh
$ ./hello.sh
Hello, bourne shell world.
$

スクリプトは、chmod コマンドにより実行可能に設定します。以降、スクリプトは実行パーミッションが付加されたものとして解釈してください。また、PATH環境変数にカレントディレクトリを意味する . を追加するのはお勧めできないので、ここでの例では設定されていないことが前提として記述されています。

* 変数

シェルも他の言語のように変数を持っています。宣言は必要ありません。変数は、英数字とアンダースコアのみで英字かアンダースコアから始めなければなりません。空白文字や特殊文字を代入したい場合、引用符で囲む必要があります。値の代入されていない変数は空文字となります。
値のセット(=の前後に空白文字を挿入してはいけない)

VAR=value

値の参照

$VAR
${VAR}

参照の仕方の違いは、半角スペースやスラッシュ、コロン、ハイフンといった記号で区切られていない文字列内で変数を利用する際で異なります。

#!/bin/sh
 
COLOR=green
echo "/home/who/$COLOR/here"
echo "/home/who/${COLOR}/here"
echo "This looks $COLORish"
echo "This seems ${COLOR}ish"
exit 0

これを実行すると、以下のようにスラッシュで区切られた場合どちらの変数も展開してくれる。しかし、ish を付加すると{}で括らない場合、$COLORish という変数とみなされ何も表示しない。

/home/who/green/here
/home/who/green/here
This looks
This seems greenish

以下のように、変数を参照し代入も可能です。

#!bin/sh
 
VAR=blue
VAR=$VAR:red
echo $VAR
exit 0

実行すると以下のように評価されます。

blue:red

* ローカル変数と環境変数

環境変数をスクリプトから参照する

#!/bin/sh
 
echo "This seems ${COLOR}ish"
exit 0

という test1.sh スクリプトがある場合、以下のようにきちんと export により別環境でも参照できる変数とすることでシェルスクリプトから利用できる。

$ ./test1.sh
This seems ish
$ COLOR=yellow
$ ./test1.sh
This seems ish
$ export COLOR
$ ./test1.sh
This seems yellwish
$ unset COLOR
$ ./test1.sh
This seems ish
$

では、逆はどうなのか?

#!/bin/sh
 
COLOR=green
export COLOR

という test2.sh スクリプトがある場合、

$ echo $COLOR
 
$ ./test2.sh
$ echo $COLOR
 
$

これは当然のことで、親プロセスから子供のプロセスに対して環境変数を引き継ぐことはできるが、子供のプロセスで設定した環境変数を親プロセスに反映することはできない。
これを踏まえた上で、ローカル変数と環境変数の違いを確認してみます。

#!/bin/sh
 
COLOR1=yellow
COLOR2=green
export COLOR2
/bin/sh ./child.sh
exit 0

という parent.sh スクリプトと

#!/bin/sh
 
echo "This seems ${COLOR1}ish"
echo "This seems ${COLOR2}ish"
exit 0

という child.sh スクリプトがある。 parent.sh スクリプトを実行すると export によって環境変数として引き継げる形にした変数だけ child.sh スクリプトで展開されていることがわかる。

$ ./parent.sh
$ echo $COLOR
This seems ish
This seems greenish
$

* 特別な変数

シェルスクリプトが引数を得る場合、$1, $2 といった特別な変数を利用することができる。この変数は、$0 〜 $9 までしか使えない。それ以上引数があり、順番に取得したいなら shift を利用する。

$0 スクリプト名
$1 〜 $9 引数1番目から9番目
$* すべての引数("$*"とした場合、"$1 $2 …"のように展開)
$@ すべての引数(ダブルクォートで囲んだ場合の処理が $* と異なり個別に引用される)
$# 引数の数

サンプルで見てみよう

#!/bin/sh
 
echo "script name: $0"
echo "arg num : $#"
echo "arg 1   : $1"
echo "arg 2   : $2"
echo "arg 3   : $3"
echo "arg 4   : $4"
echo "arg 5   : $5"
echo "arg 6   : $6"
echo "arg 7   : $7"
echo "arg 8   : $8"
echo "arg 9   : $9"
echo "arg 10? : $10"
echo "all arg : $*"
exit 0

を実行すると以下のように $9 まで展開されていることがわかる

$ ./cmd.sh a b c d e f g h i j k l
script name: ./cmd.sh
arg num : 12
arg 1   : a
arg 2   : b
arg 3   : c
arg 4   : d
arg 5   : e
arg 6   : f
arg 7   : g
arg 8   : h
arg 9   : i
arg 10? : a0
all arg : a b c d e f g h i j k l
$

このほかに以下のようなものがある。

$? 直前に実行したコマンドのexit値
$$ シェル自身のプロセスID
$- シェルの起動時のフラグの一覧
$! シェルが最後に起動したバックグラウンドプロセスのプロセスID

また、環境変数などは、特別な処理でない限り変数として利用するのは避けたほうがよい(使ってはいけない)。

PATH シェルが実行可能なコマンドを検索するディレクトリを指定。ピリオドがカレントディレクトリを意味するが、設定はしないほうがよい。明確にカレントディレクトリのコマンドを実行する場合、./script.sh という具合に実行するほうが確実に意図したスクリプトやコマンドが実行できる。
IFS 区切り文字として扱う文字を指定。

基本的には、シェル上で引数なしのsetを実行した際に表示される環境変数(PATH, USER, UID, PRINTER, TERM, TZ, etc.)をローカル変数として利用するのは避けたほうがよい。

* 準変数

変数を参照する際に、変数がNULLであった場合の評価を設定することができます。

${VAR:-expression} 値がセットされていない(NULL)場合、:-以降の式を評価結果を返す。
${VAR:+expression} 値がセットされている(NONE-NULL)場合、:+以降の式を評価結果を返す。
${VAR:=expression} 値がセットされていない(NULL)場合、:=以降の式を評価結果を返し変数に代入。
${VAR:?[expression]} 値がセットされていない(NULL)場合、式が標準エラーに出力。

サンプルで見てみよう

#!/bin/sh
 
echo "[ null variable ]"
VAR=
EXPAND=${VAR:-blue}
echo "-var    : $VAR"
echo "-expand : $EXPAND"
EXPAND=${VAR:+yellow}
echo "+var    : $VAR"
echo "+expand : $EXPAND"
EXPAND=${VAR:=green}
echo "=var    : $VAR"
echo "=expand : $EXPAND"
echo ""
echo "[ non-null variable ]"
VAR=red
EXPAND=${VAR:-blue}
echo "-var    : $VAR"
echo "-expand : $EXPAND"
EXPAND=${VAR:+yellow}
echo "+var    : $VAR"
echo "+expand : $EXPAND"
EXPAND=${VAR:=green}
echo "=var    : $VAR"
echo "=expand : $EXPAND"
exit 0

を実行すると以下のように評価されます。

$ ./var.sh
[ null variable ]
-var    :
-expand : blue
+var    :
+expand :
=var    : green
=expand : green
 
[ non-null variable ]
-var    : red
-expand : red
+var    : red
+expand : yellow
=var    : red
=expand : red
$

* 特殊文字

以下の文字は特殊文字として扱われます

; | & ^ $ ? * < > ( ) [ ] { } ` " ' TAB SPACE NEWLINE

これら特殊文字を表示するには、エスケープする必要があります。エスケープの方法は、バックスラッシュを利用して一文字単位で行うか、シングルクオーテーションで囲むことで行えます。ダブルクオーテーションで囲んだ場合、$ ` \ は評価されます。つまり、変数やコマンドは処理されます。

* ZERO またはすべての文字にマッチします。また、引数で利用した場合、すべてのファイル名を展開
? 一文字にマッチ
[range] 文字範囲にマッチ
\ 特殊文字をエスケープ
' ' 囲まれたすべてを文字列として扱う
" " $ ` \ を評価し囲まれたすべてを文字列として扱う
` ` コマンドを評価

* Built-in コマンド

Bourne shell が持っている内部コマンド。

{ commands ; }, ( commands ) サブシェルでコマンドを実行
: 何もしない
. filename ファイルを読み込む
bg [job], fg [job] ジョブをバックグラウンドで実行。バックグラウンドのジョブをカレントジョブとして実行。job は、%numberとして割り当てられたバックグラウンドプロセス。
cd [dir] ディレクトリーの移動
pwd 現在の作業ディレクトリを表示
echo args 引数を標準出力に表示
eval args 引数でわたされた式を評価
exec command コマンドを実行
exit [n] 現在実行しているスクリプトを終了。終了コードを指定可能。デフォルトはZERO。
kill [-sig] %job 指定したジョブにシグナルを送る
read name... 標準入力からの入力1行を name 変数に代入。name が複数ある場合、ワードごとに代入。
set [+/-flag] [arg] 引数がない場合、すべての変数を表示。
-でシェルのオプションをON。+でシェルのオプションをOFF。
引数はコマンドライン引数として設定。
test expression boolean式として評価
trap [command sig]... 割り込み発生時にコマンドを実行
ulimit システムのリミットを設定
umask [nnn] 作成されるファイルのデフォルトのパーミッションを8進数のマスクで設定
wait [n] バックグラウンドプロセスの終了を待つ

* フローコントロール

* if 文

条件によって処理を分岐させる。

if 条件; then
     コマンド
elif 条件; then
     コマンド
else
     コマンド
fi

以下のサンプルは、スーパーユーザーでなければ警告を出力して終了する。

#!/bin/sh
#
USER_ID=`/usr/bin/id -u`
 
if [ $USER_ID -ne 0 ]; then
    echo "You must be super-user to execute $0"
    exit 1
fi
 
echo "Welcome to $0 script world"
 
exit 0

* while/until 文

whileは、条件が真(TRUE)である間はループする。untilは、条件が偽(FALSE)である間はループする。

{while|until} 条件
do

     コマンド
done

以下のサンプルは、10回ループして終了する。

#!/bin/sh
 
COUNT=1
while [ $COUNT -le 10 ];
do
    echo "$COUNT"
    COUNT=`expr $COUNT + 1`
done

* for 文

リスト内のすべての要素の処理が終えるまでループする。

for 変数 in リスト
do
     コマンド
done

以下のサンプルは、ホストリストに記述されたホストの数だけループする。

#!/bin/sh
 
HOST_LIST="h1.example.com h2.example.com h3.example.com"
for HOST in $HOST_LIST
do
    echo "$HOST"
done

* case 文

変数が一致するパターンのコマンドを実行。パターンには正規表現を利用することができる。

case 変数 in
    パターン1)
        コマンド
        ;;
    パターン2)
        コマンド
        ;;
    *)
        コマンド
        ;;
esac

以下のサンプルは、引数を処理する。

while [ $# -ge 1 ]; do
    case $1 in
        -p*)    PREFIX=`echo $1 | cut -c3-`;;
        -p)     shift; PREFIX=$1 ;;
        -*)     echo $Usage; exit 1 ;;
        *)      FILE=$*; break ;;
    esac

    shift
done

* [ および test コマンド

このコマンドは、条件を評価するコマンドです。

-e filename ファイルが存在していれば真
-d filename ディレクトリであれば真
-f filename 通常ファイルであれば真
-h filename シンボリックリンクであれば真
-r filename 読むことが可能なファイルであれば真
-w filename 書き込み可能なファイルであれば真
-n string 文字列の長さが 0 でなければ真
-z string 文字列の長さが 0 であれば真
string 文字列で空でなければ真
s1 = s2 文字列s1 と文字列s2 が同じであれば真
s1 != s2
文字列s1 と文字列s2 が異なれば真
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 以下であれば真
! expression 評価した式が偽であれば真
expr1 -a expr2 式expr1 と式expr2 が真であれば真
expr1 -o expr2 式expr1 と式expr2 のいずれかが真であれば真

* I/O リダイレクト

コマンドからの標準入力(0)や標準出力(1)、標準エラー出力(2)を別のコマンドやファイルにリダイレクトすることができます。すべてのコマンドは、この三つファイルデスクリプタを持っています。

< filename ファイルの内容を標準入力に渡す
> filename 標準出力をファイルに書き込む
>> filename 標準出力をファイルに追加する
<<word 指定したワードで始まる行まで標準入力に渡す
<&digit 指定したデスクリプタを標準入力として扱う
>&digit 指定したデスクリプタを標準出力として扱う
- 以下は標準エラー出力を標準出力として扱いファイルに記録する
  command 2>&1 > filename
- 以下は標準出力を標準エラー出力として扱いう
  echo "I'm sorry Dave, I'm afraid I can't do that." 1>&2
<&- 標準入力を閉じる
>&- 標準出力を閉じる
command1 | command2 コマンド1の標準出力をコマンド2の標準入力に渡す
command1 && command2 コマンド1が正常終了すればコマンド2を実行
command1 || command2 コマンド1が正常終了でなければコマンド2を実行

* スクリプト内でよく使うコマンド

* basename

パス名の部分的な抽出(ベース名)を行います。

$ basename /usr/local/bin/gcc
gcc
$

* dirname

パス名の部分的な抽出(ディレクトリ名)を行います。

$ dirname /usr/local/bin/gcc
/usr/local/bin
$

* awk

パターン操作/処理言語。-F オプションでセパレーターを指定します。

$ cat /etc/passwd | awk -F: '{print $1, $3 }'
root 0
daemon 1
bin 2
sys 3
adm 4
...
$

* sed

正規表現を利用できるストリームエディタ。

$ echo "Hello, new world" | sed -e 's/new/script/'
Hello, script world
$

* tee

標準出力のされたものをファイルに記録します。標準出力にはそのまま出力されます。

* 関数

たびたび繰り返されるコマンド群は、関数として定義することで見やすく小さなスクリプトを作ることができます。以下のように定義します。

name () {
    コマンド
}

関数の引数を扱う場合も $1, $2, ... $9 が利用できます。

関数の利用

name args ...

シェルの関数は他の言語の関数と違い、関数内の変数がローカル変数にはならない。よって、関数で設定した値はそのまま関数の外でも有効です。また、関数を宣言する前に設定した変数もそのまま関数で利用できます。

* ちょっとしたテクニックや注意点

echo で改行したくない
System V は、\c を利用し、BSD系は -n オプションを利用する。どちらでも利用できるようにするには、
if [ "`echo -n`" = "-n" ]; then
    n=""
    c="\c"
else
    n="-n"
    c=""
fi
echo $n "messgae: $c"
echo "done"
 
変数がNULLかわからないので
変数が null だった場合、以下の条件式ではエラーになります。
 
    if [ $answer = yes ]; then
 
これは $answer がから文字列だった場合、 if [ = yes ]; then と評価されエラーになります。
ではダブルクォーテーションで囲めばよいのか?
 
    if [ "$answer" = yes ]; then
 
実はこれでもまだ安全ではありません。もし $answer が -f だった場合 if [ -f = yes ]; then となり
エラーと判断されるかもしれません。
最も安全場記述方法は以下のように確実に文字列として評価されるように記述することです。
 
    if [ x"$answer" = xyes ]; then
 
コマンドを直接利用するのではなく変数に代入して利用
スクリプト内で多くのコマンドを利用することになります。その際、直接コマンドを記述するのではなく、
いったん変数に代入しておくと便利です。
 
    #!/bin/sh
    CMD_SCP="/usr/local/bin/scp"
    CMD_RSYNC="/usr/local/bin/rsync"
    ...
    $CMD_SCP foo host:/tmp/foo
    $CMD_RSYNC -a /usr/local/data/ host:/usr/local/data/
 
このようにしておくことで、CMD_RSYNC="echo /usr/local/bin/rsync" と宣言しなおすことで
デバッグ等が容易になります。
 
FreeBSD でロックファイルを作らずに多重起動を防ぐ
FreeBSD で多重起動を制御するにはどうしたらよいか?といろいろ考えてみた。
自分のプロセスを調べ、自分自身と子プロセスは排除し、それでもスクリプトが起動されているようであれば多重起動となる。
 
    #!/bin/sh
    CMD_PS="/bin/ps"
    CMD_GREP="/usr/bin/grep"
    CMD_BASENAME="/usr/bin/basename"
 
    SELFPID=$$
    SELFNAME=`$CMD_BASENAME $0`
 
    RUN_SELF=`$CMD_PS ajx | $CMD_GREP -v "grep" | $CMD_GREP -v " $SELFPID " | $CMD_GREP -c $SELFNAME`
    if [ $RUN_SELF -gt 0 ]; then
        echo "$SELFNAME script still working."
        exit 1
    fi
 
ううむ、何か忘れているような。。。。