一、Shell スクリプトの構文#
1.1 条件テスト:test [#
コマンドtestまたは[は、条件が成立しているかどうかをテストできます。テスト結果が真であれば、そのコマンドの Exit Status は 0、テスト結果が偽であれば、コマンドの Exit Status は 1(C 言語の論理表現とは逆になります)。例えば、2 つの数の大小関係をテストする場合:
$ VAR=2
$ test $VAR -gt 1
$ echo $?
0
$ test $VAR -gt 3
$ echo $?
1
$ [ $VAR -gt 3 ]
$ echo $?
1
見た目は奇妙ですが、左方括弧 [ は実際にはコマンドの名前であり、コマンドに渡す各パラメータの間にはスペースを入れる必要があります。例えば、$VAR、-gt、3、]は[コマンドの 4 つのパラメータであり、これらの間には必ずスペースを入れる必要があります。コマンド test または [ のパラメータ形式は同じですが、test コマンドは ] パラメータを必要としません。[ コマンドの例として、一般的なテストコマンドは以下の表に示されています:
テストコマンド
| コマンド | 説明 |
|---|---|
[ -d DIR ] | DIRが存在し、かつディレクトリであれば真 |
[ -f FILE ] | FILEが存在し、かつ通常のファイルであれば真 |
[ -z STRING ] | STRINGの長さがゼロであれば真 |
[ -n STRING ] | STRINGの長さがゼロでなければ真 |
[ STRING1 = STRING2 ] | 2 つの文字列が同じであれば真 |
[ STRING1 != STRING2 ] | 文字列が異なれば真 |
[ ARG1 OP ARG2 ] | ARG1とARG2は整数または整数の値を持つ変数で、OPは-eq(等しい)-ne(等しくない)-lt(小さい)-le(以下)-gt(大きい)-ge(以上)のいずれか |
論理和、論理積、論理否定を含むテストコマンド
| コマンド | 説明 |
|---|---|
[ ! EXPR ] | EXPRは上表のいずれかのテスト条件であり、! は論理否定を示します |
[ EXPR1 -a EXPR2 ] | EXPR1とEXPR2は上表のいずれかのテスト条件であり、-aは論理積を示します |
[ EXPR1 -o EXPR2 ] | EXPR1とEXPR2は上表のいずれかのテスト条件であり、-oは論理和を示します |
例えば:
$ VAR=abc
$ [ -d Desktop -a $VAR = 'abc' ]
$ echo $?
0
注意:上記の例で $VAR 変数が事前に定義されていない場合、Shell は空文字列に展開され、テスト条件の構文エラーを引き起こします(展開されると [ -d Desktop -a = 'abc' ] になります)。良い Shell プログラミングの習慣として、常に変数の値を二重引用符で囲むべきです(展開されると[ -d Desktop -a "" = 'abc' ]になります):
$ unset VAR
$ [ -d Desktop -a $VAR = 'abc' ]
bash: [: too many arguments
$ [ -d Desktop -a "$VAR" = 'abc' ]
$ echo $?
1
1.2 if/then/elif/else/fi#
C 言語に似て、Shell では if、then、elif、else、fi というコマンドを使用して分岐制御を実現します。このフロー制御文は本質的にいくつかの Shell コマンドで構成されています。例えば、前述の
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
は実際には 3 つのコマンドです。if [ -f ~/.bashrc ]が最初のコマンドで、then . ~/.bashrcが 2 番目、fiが 3 番目です。2 つのコマンドが同じ行に書かれている場合は ; で区切る必要があります。1 行に 1 つのコマンドだけを書く場合は ; は必要ありません。また、thenの後に改行がある場合でも、このコマンドが完了していない場合、Shell は自動的に次の行をthenの後に続けて 1 つのコマンドとして処理します。
[コマンドと同様に、コマンドと各パラメータの間には必ずスペースを入れる必要があります。if コマンドのパラメータはサブコマンドを構成し、そのサブコマンドの Exit Status が 0(真を示す)であれば、thenの後のサブコマンドが実行され、Exit Status が非 0(偽を示す)であれば、elif、else、またはfiの後のサブコマンドが実行されます。ifの後のサブコマンドは通常テストコマンドですが、他のコマンドでも構いません。Shell スクリプトには{}括弧がないため、fiはif文のブロックの終了を示します。
以下の例を参照してください:
#! /bin/sh
if [ -f /bin/bash ]
then echo "/bin/bash is a file"
else echo "/bin/bash is NOT a file"
fi
if :; then echo "always true"; fi
: は特別なコマンドで、空コマンドと呼ばれ、このコマンドは何も行いませんが、Exit Status は常に真です。また、/bin/trueや/bin/falseを実行して真または偽の Exit Status を得ることもできます。
もう一つの例を見てみましょう:
#! /bin/sh
echo "Is it morning? Please answer yes or no."
read YES_OR_NO
if [ "$YES_OR_NO" = "yes" ]; then
echo "Good morning!"
elif [ "$YES_OR_NO" = "no" ]; then
echo "Good afternoon!"
else
echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
exit 1
fi
exit 0
上記の例でreadコマンドの役割は、ユーザーが 1 行の文字列を入力するのを待ち、その文字列を Shell 変数に格納することです。
さらに、Shell は&&と||構文を提供しており、C 言語に似て、ショートサーキット特性を持っています。多くの Shell スクリプトは次のように書かれます:
test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)
&&は「if...then...」に相当し、||は「if not...then...」に相当します。&& と || は 2 つのコマンドを接続するために使用されますが、上で説明した-aと-oはテスト式内で 2 つのテスト条件を接続するためにのみ使用されることに注意してください。例えば、
test "$VAR" -gt 1 -a "$VAR" -lt 3
は以下の書き方と等価です:
test "$VAR" -gt 1 && test "$VAR" -lt 3
1.3 case/esac#
caseコマンドは C 言語のswitch/case文に類似しており、esacはcase文のブロックの終了を示します。
**C 言語の case は整数または文字型定数式のみをマッチさせることができますが、Shell スクリプトの case は文字列やワイルドカードをマッチさせることができ、各マッチングブランチにはいくつかのコマンドを含むことができ、末尾は必ず;;で終了する必要があります。実行時に最初のマッチングブランチを見つけ、そのブランチに対応するコマンドを実行し、その後は直接esacにジャンプします。C 言語のようにbreakで抜ける必要はありません。
#! /bin/sh
echo "Is it morning? Please answer yes or no."
read YES_OR_NO
case "$YES_OR_NO" in
yes|y|Yes|YES)
echo "Good Morning!";;
[nN]*)
echo "Good Afternoon!";;
*)
echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
exit 1;;
esac
exit 0
case文の例はシステムサービスのスクリプトディレクトリ/etc/init.d/で見つけることができます。このディレクトリ内のスクリプトはほとんどがこの形式を持っています(例として/etc/apache2):
case $1 in
start)
...
;;
stop)
...
;;
reload | force-reload)
...
;;
restart)
...
*)
log_success_msg "Usage: /etc/init.d/apache2 {start|stop|restart|reload|force-reload|start-htcacheclean|stop-htcacheclean}"
exit 1
;;
esac
apache2サービスを起動するコマンドは
$ sudo /etc/init.d/apache2 start
$1は特別な変数で、スクリプトを実行する際に自動的に最初のコマンドライン引数(この場合はstart)の値を取ります。したがって、start)ブランチに入って関連するコマンドを実行します。同様に、コマンドライン引数がstop、reload、またはrestartに指定されると、他のブランチに入ってサービスを停止したり、設定ファイルを再読み込みしたり、サービスを再起動したりする関連コマンドを実行します。
1.4 for/do/done#
Shell スクリプトのforループ構造は C 言語とは大きく異なり、特定のプログラミング言語のforeachループに似ています。例えば:
#! /bin/sh
for FRUIT in apple banana pear; do
echo "I like $FRUIT"
done
FRUITはループ変数で、最初のループで $FRUIT の値は apple、2 回目は banana、3 回目は pear になります。さらに、現在のディレクトリにある chap0、chap1、chap2などのファイル名を chap0~、chap1~、chap2~などに変更する場合(慣例として、末尾に~文字が付いたファイル名は一時ファイルを示します)、このコマンドは次のように書くことができます:
$ for FILENAME in chap?; do mv $FILENAME $FILENAME~; done
または次のようにも書けます:
$ for FILENAME in `ls chap?`; do mv $FILENAME $FILENAME~; done
1.5 while/do/done#
whileの使い方は C 言語に似ています。例えば、パスワードを検証するスクリプト:
#! /bin/sh
echo "Enter password:"
read TRY
while [ "$TRY" != "secret" ]; do
echo "Sorry, try again"
read TRY
done
次の例では、算術演算を使用してループの回数を制御しています:
#! /bin/sh
COUNTER=1
while [ "$COUNTER" -lt 10 ]; do
echo "Here we go again"
COUNTER=$(($COUNTER+1))
done
Shell にはuntilループもあり、これは C 言語の do...while ループに似ています。この章では省略します。
演習
1、上記のパスワード検証プログラムを修正し、ユーザーが 5 回パスワードを間違えた場合にエラーを報告して終了します。
1.6 位置パラメータと特殊変数#
多くの特殊変数は Shell によって自動的に割り当てられます。すでに$?と$1に出会いましたが、ここでまとめます:
一般的な位置パラメータと特殊変数
| コマンド | 説明 |
|---|---|
$0 | C 言語のmain関数のargv[0]に相当 |
$1、$2... | これらは位置パラメータ(Positional Parameter)と呼ばれ、C 言語のmain関数のargv[1]、argv[2]... に相当 |
$# | C 言語のmain関数のargc - 1に相当、ここでの#の後はコメントを示しません |
$@ | パラメータリスト"$1" "$2" ...を示し、例えばforループのinの後に使用できます。 |
$? | 前のコマンドの Exit Status |
$$ | 現在の Shell のプロセス ID |
位置パラメータはshiftコマンドで左にシフトできます。例えば、shift 3は元の$4が現在$1になり、元の$5が現在$2になるなど、元の$1、$2、$3は捨てられ、$0は移動しません。引数なしのshiftコマンドはshift 1に相当します。例えば:
#! /bin/sh
echo "The program $0 is now running"
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The parameter list is $@"
shift
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The parameter list is $@"
1.7 関数#
C 言語に似て、Shell にも関数の概念がありますが、関数定義には戻り値もパラメータリストもありません。例えば:
#! /bin/sh
foo(){ echo "Function foo is called";}
echo "-=start=-"
foo
echo "-=end=-"
関数体の左中括弧{と後のコマンドの間にはスペースまたは改行が必要です。最後のコマンドと右中括弧}を同じ行に書く場合、コマンドの末尾には;が必要です。
foo()関数を定義する際には、関数体のコマンドは実行されず、変数を定義するのと同じように、fooという名前に定義を与えるだけです。後でfoo関数を呼び出すとき(Shell の関数呼び出しでは括弧を記述しないことに注意)に関数体のコマンドが実行されます。Shell スクリプト内の関数は、定義されてから呼び出される必要があり、一般的には関数定義をスクリプトの前に書き、関数呼び出しと他のコマンドをスクリプトの最後に書きます(C 言語のmain関数に似て、これがスクリプトが実際にコマンドを実行し始める場所です)。
Shell 関数にはパラメータリストがありませんが、パラメータを渡すことができ、実際には関数はミニスクリプトのようなもので、関数を呼び出すときに任意の数のパラメータを渡すことができ、関数内でも $0、$1、$2 などの変数を使用してパラメータを取得します。関数内の位置パラメータは関数のローカル変数に相当し、これらの変数を変更しても関数外の $0、$1、$2 などの変数には影響しません。関数内では return コマンドを使用して戻ることができ、returnの後に数字が続くと関数の Exit Status を示します。
以下のスクリプトは、複数のディレクトリを一度に作成することができ、各ディレクトリ名はコマンドライン引数として渡され、スクリプトは各ディレクトリが存在するかどうかを逐次テストし、存在しない場合は情報を表示してそのディレクトリを作成しようとします。
#! /bin/sh
is_directory()
{
DIR_NAME=$1
if [ ! -d $DIR_NAME ]; then
return 1
else
return 0
fi
}
for DIR in "$@"; do
if is_directory "$DIR"
then :
else
echo "$DIR doesn't exist. Creating it now..."
mkdir $DIR > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Cannot create directory $DIR"
exit 1
fi
fi
done
注意:is_directory()が 0 を返すと真を示し、1 を返すと偽を示します。
二、Shell スクリプトのデバッグ方法#
Shell はスクリプトのデバッグに使用できるいくつかのオプションを提供します。以下の通りです:
-n スクリプト内のコマンドを読み取りますが、実行はせず、スクリプト内の構文エラーをチェックするために使用します
-v スクリプトを実行しながら、実行されたスクリプトコマンドを標準エラー出力に印刷します
-x 実行のトレース情報を提供し、実行された各コマンドと結果を順次印刷します
これらのオプションを使用する方法は 3 つあります。1 つはコマンドラインで引数を提供することです。
$ sh -x ./script.sh
2 つ目はスクリプトの先頭で引数を提供することです。
#! /bin/sh -x
3 つ目はスクリプト内で set コマンドを使用して引数を有効または無効にすることです。
#! /bin/sh
if [ -z "$1" ]; then
set -x
echo "ERROR: Insufficient Args."
exit 1
set +x
fi
set -x と set +x はそれぞれ-xオプションを有効または無効にし、これによりスクリプトの特定の部分だけをトレースデバッグできます。