シェルプログラミングの基礎知識
UNIX のコマンドインタプリタである Bourne shell を知っておかないといろいろ困るので基本的なことをメモしておく。ここでは、コマンドをバッチ処理するための シェルスクリプトについて記述しています。
#!/bin/sh
UNIXでスクリプトを記述する場合、一番初めの行は #!
(shebang)ではじめます。さらに、シェルスクリプトであることを明確にするためにコマンドをフルパスで記述します。シェルスクリプトでは、#!/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
ファイル(file.txt)から1行ずつ読み込む場合は以下の様にする。
#!/bin/sh while read READ_LINE; do echo "$READ_LINE"; done < file.txt
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 |
書き込み可能なファイルであれば真/td> |
-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 > filename 2>&1 - 以下は標準出力を標準エラー出力として扱いう 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
標準出力のされたものをファイルに記録します。標準出力にはそのまま出力されます。
tr
テキスト中のある文字を別の文字に置き換えたり、テキスト中のある文字のみ削除することができます。
$ tr -d '\r' < windows.txt > unix.txt
xargs
通常、多くのコマンドは非常に長い引数のリストを受け取ることが出来ません。そこで、find コマンドの -execオプションや xargsコマンドが利用されます。xargsが便利なのは、ファイルを検索しフィルタを通した後であるコマンドを実行したい時に以下の様に指定する事で実現できます。
$ find . -type f | grep -v '/.svn' | xargs chmod 664
関数
たびたび繰り返されるコマンド群は、関数として定義することで見やすく小さなスクリプトを作ることができます。以下のように定義します。
name () { コマンド }
関数の引数を扱う場合も $1, $2, ... $9
が利用できます。
関数の利用
name args ...
シェルの関数は他の言語の関数と違い、関数内の変数がローカル変数にはならない。よって、関数で設定した値はそのまま関数の外でも有効です。また、関数を宣言する前に設定した変数もそのまま関数で利用できます。
ちょっとしたテクニックや注意点
スクリプト自身の PATHを知るには? |
#!/bin/sh BASEDIR=$(cd $(/usr/bin/dirname $0); pwd) これで BASEDIRに、このスクリプトが置いてあるディレクトリが入ります。 |
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 ううむ、何か忘れているような。。。。 素直に flock 使ったほうがいいのかもしれませんね。 |