bash 例解: 第 2 回 bash による初歩のプログラミングの続編

bash に関する第 1 回の記事では、Daniel Robbins が、このスクリプト言語の基本要素を取り上げて、bash を使用するべき理由を示しました。この第 2 回の記事では、前回の続きから始めて、条件 (if-then) 文やループをはじめとする、bash の基本的な構成体について説明していきます。

2013年 8月 08日 ― 読者からのコメントに対応し、「引数の使用」、「条件文での処理」、「文字列比較の注意点」、「ループ処理を行う構成体: for」、「関数と名前空間」の各セクションを中心に内容を更新しました。

Daniel Robbins, President and CEO, Gentoo Technologies, Inc.

アメリカはニューメキシコ州アルバカーキ (Albuquerque) 在住の Daniel Robbins 氏は、Gentoo Project の責任者、Gentoo Technologies, Inc. の CEO、Linux Advanced Multimedia Project (LAMP) の良き指導者、「Caldera OpenLinux Unleashed」、「SuSE Linux Unleashed」、「Samba Unleashed」などの Macmillan 書籍の共同執筆者という多彩な肩書きを有しています。コンピューターとのかかわりは小学校 2 年生のころからであり、Logo プログラミング言語と、ちょっと怖いパックマンとの出会いがきっかけでした。そういう生い立ちもあってか、SONY Electronic Publishing/Psygnosis のリード・グラフィック・アーティストとしても活躍中です。この春に出産を予定している愛妻 Mary さんとの時間を大切にする良き夫でもあります。



2013年 8月 08日 (初版 2000年 4月 01日)

まずはコマンドライン引数を扱う上での簡単なヒントを紹介し、そのあとで bash の基本的なプログラミング構成体について見ていきます。

引数の使用

第 1 回の記事にあるサンプル・プログラムでは、"$1" という環境変数を使用しました。この変数は、コマンドライン引数の 1 番目を参照します。同様に、"$2" や "$3" なども、スクリプトに渡される引数の 2 番目や 3 番目を参照するために使用することができます。以下はその例です。

#!/usr/bin/env bash

echo name of script is $0
echo first argument is $1
echo second argument is $2
echo seventeenth argument is ${17}
echo number of arguments is $#

上記の例は、見たとおりのものですが、細かい点を 3 つほど説明しておきます。第 1 に、"$0" はコマンドラインから呼び出されるスクリプトの名前に展開されます。第 2 に、引数の番号が 10 以上の場合には、その番号全体を波括弧で括る必要があります。第 3 に、"$#" はスクリプトに渡される引数の個数に展開されます。上記のスクリプトを使用して、いろいろなコマンドライン引数を渡してみることで、引数がどのような動作をするのか、引数を扱う上でのコツをつかんでください。

場合によっては、すべての コマンドライン引数を 1 度に参照すると便利かもしれません。この目的のために、bash では、"$@" 変数が用意されています。この変数は、スペースで区切られたすべてのコマンドライン・パラメーターに展開されます。この変数を使用した例については、この少しあとで for ループについて説明するときに取り上げます。


bash のプログラミング構成体

C、Pascal、Python、Perl のような手続き型言語でのプログラミング経験がある人であれば、if 文や for ループのような標準的なプログラミング構成体についてはご存じでしょう。bash には、そうした標準的な構成体のほとんどについて、独自の仕様があります。このあとのいくつかのセクションでは、bash の構成体をいくつか紹介しながら、他のプログラミング言語の構成体 (読者の皆さんがよくご存じの構成体) との違いについて説明します。プログラミング経験があまりない人も、特に心配はいりません。補足情報や例を多用しているので、それらを参考にすれば本文の説明についていけるはずです。


条件文での処理

C でファイル関連のコードをプログラミングした経験がある読者の方は、あるファイルが別のファイルよりも新しいかどうかを判別するには、膨大な処理が必要なことをご存じでしょう。処理が膨大になるのは、C にはそのような比較処理を実行するための組み込み構文がないからです。結局のところ、2 つの stat() 呼び出しと 2 つの stat 構造体を使用して、手作業で比較処理を実行しなければなりません。それとは対照的に、bash には標準のファイル比較演算子が組み込まれているため、たとえば /tmp/myfile が読み取り可能かどうかを判別するのは、$myvar が 4 よりも大きいかどうかを調べるのと同じぐらい簡単です。

以下の表は、最もよく使われる bash の比較演算子を一覧にしたものです。各比較演算子の正しい使い方についても、具体的な例を挙げておきました。それぞれの例は、if の直後に入れるような形になっています。たとえば、次のようになります。

if [ -z "$myvar" ]
then
        echo "myvar is not defined"
fi

注: 角括弧とその前後のテキストとの間には、スペースを入れて区切る必要があります。

bash の一般的な比較演算子
演算子説明
-z文字列の長さがゼロである[ -z "$myvar" ]
-n文字列の長さがゼロではない[ -n "$myvar" ]
=文字列が等しい[ "abc" = "$myvar" ]
!=文字列が等しくない[ "abc" != "$myvar" ]
-eq数値が等しい[ 3 -eq "$myinteger" ]
-ne数値が等しくない[ 3 -ne "$myinteger" ]
-lt数値がより小さい[ 3 -lt "$myinteger" ]
-le数値がより小さいか等しい[ 3 -le "$myinteger" ]
-gt数値がより大きい[ 3 -gt "$myinteger" ]
-ge数値がより大きいか等しい[ 3 -ge "$myinteger" ]
-f通常のファイルとして存在する[ -f "$myfile" ]
-dディレクトリーとして存在する[ -d "$mydir" ]
-nt左に記述されたファイルが、右に記述されたファイルよりも新しい[ "$myfile" -nt ~/.bashrc ]
-ot左に記述されたファイルが、右に記述されたファイルよりも古い[ "$myfile" -ot ~/.bashrc ]

場合によっては、同じ比較をいくつかの異なる方法で行うことができます。たとえば、次の 2 つのコード・スニペットは同じ働きをします。

if [ "$myvar" -eq 3 ]
then 
    echo "myvar equals 3"
fi


if [ "$myvar" = "3" ]
then
    echo "myvar equals 3"
fi

上に記述したコードは算術比較演算子を使用しているのに対し、下に記述したコードは文字列比較演算子を使用しています。$myvar が整数の場合は、これら 2 つのコードは比較を行った結果としてまったく同じ処理を行うことになります。$myvar が整数でない場合は、上のコードは失敗してエラーとなります。


文字列比較の注意点

大抵の場合、文字列や文字列変数を囲む二重引用符は省略しても構わないのですが、必ずしもほめられた方法とは言えません。なぜかと言えば、環境変数にスペースやタブが含まれていなければ、コードは完璧な動作をするはずですが、スペースやタブが含まれている場合、bash が混乱してしまうからです。以下は、bash が混乱してしまう比較処理の例です。

if [ $myvar = "foo bar oni" ]
then
    echo "yes"
fi

上記の例では、$myvar の値が foo の場合には、コードは期待通りの動作をして何も出力しませんが、$myvar の値が foo bar oni の場合には、コードは失敗して次のエラーが表示されます。

[: too many arguments

この場合は、$myvar の値 (つまり、foo bar oni) にスペースが含まれていたために bash が混乱してしまったのです。つまり、bash が $myvar を展開した後に行う比較処理は、次のようになるからです。

[ foo bar oni = "foo bar oni" ]

同様にして、$myvar が空の文字列の場合には、引数が少なすぎる結果となり、コードは失敗して次のエラーが表示されます。

[: =: unary operator expected

ここでは、環境変数の値が二重引用符で括られていないため、bash は、角括弧の中にある引数が多すぎる (あるいは少なすぎる) とみなしたわけです。この問題は、文字列引数を二重引用符で括っておくことで簡単に回避することができます。文字列引数と環境変数の値は、すべて二重引用符で括るように習慣づけておけば、同じようなプログラミング・エラーの多くを防げるということを覚えておいてください。以下は、"foo bar oni" との比較を行う本来のコードです。

if [ "$myvar" = "foo bar oni" ]
then
    echo "yes"
fi

引用符の使い方の続編

環境変数を展開する場合は、単一引用符ではなく二重引用符で括る必要があります。単一引用符で括ると、変数 (さらには履歴) の展開ができなくなります。

上記のコードは期待通りの動作をするはずであり、望ましくない予期せぬ結果にはなりません。


ループ処理を行う構成体: for

条件文についてはこれぐらいにして、bash のループ構成体の話に移りましょう。まずは標準的な for ループです。基本的な例を以下に示します。

#!/usr/bin/env bash

for x in one two three four
do
    echo number $x
done

output:
number one
number two 
number three 
number four

実際にはどのような処理が行われているのでしょう? for ループの for x のブロックでは、$x という新しい環境変数 (ループ制御変数とも言います) を定義しています。この変数には、one、two、three、four という値が連続的に設定されています。それぞれの値の設定後には、ループの本体 (つまり、do と done の間のコード) が 1 度実行されます。この本体では、標準的な変数展開構文を使用して、ループ制御変数 $x をその他の環境変数と同じように参照しています。さらに確認しておきたいのは、for ループが常に、in ステートメントの後の一種の単語リストを受け入れているという点です。この場合は、4 つの英単語を指定しましたが、単語リストでは、ディスク上のファイルやファイル用のワイルドカードを参照しても構いません。以下の例では、シェルにおける標準的なワイルドカードの使用例を示しています。

#!/usr/bin/env bash

for myfile in /etc/r*
do
    if [ -d "$myfile" ] 
    then
      echo "$myfile (dir)"
    else
      echo "$myfile"
    fi
done

output:

/etc/rc.d (dir)
/etc/resolv.conf
/etc/resolv.conf~
/etc/rpc

上記のコードは、名前が r で始まる /etc ディレクトリー内のすべてのファイルをループ処理しています。そのために、bash ではループ処理を実行する前に、まずワイルドカード /etc/r* を取り込んで展開し、/etc/rc.d /etc/resolv.conf /etc/resolv.conf~ /etc/rpc という文字列に置き換えます。ループ処理の内部では、-d 条件演算子を使用して $myfile がディレクトリーであるかどうかを判断し、その結果に基づいて 2 つの別々のアクションを実行しています。$myfile がディレクトリーである場合には、出力行の行末に「(dir)」が付けられるようにしています。

単語リストの中では、複数のワイルドカードや環境変数を使用することも可能です。

for x in /etc/r??? /var/lo* /home/drobbins/mystuff/* /tmp/${MYPATH}/*
do
    cp $x /mnt/mydir
done

bash は、ワイルドカードと変数の展開を、該当するすべての場所で実行して、単語リストを作成します。この単語リストは、非常に長いリストになる可能性があります。

上記のワイルドカード展開の例ではすべて絶対パスを使用していますが、以下のように相対パスを使用することも可能です。

for x in ../* mystuff/*
do
    echo $x is a silly file
done

上記の例で bash は、まさにコマンドラインで相対パスを使用する場合と同じように、現在作業しているディレクトリーに対する相対パスを使用してワイルドカード展開を実行しています。ワイルドカード展開をいろいろと試してみてください。ワイルドカードを使用している単語リストで絶対パスが使用されている場合、bash がワイルドカードを展開すると、絶対パスの単語リストが生成されます。そうでない場合、bash で生成される単語リストには相対パスが使用されます。現在の作業ディレクトリー内のファイルを参照するだけであれば (たとえば、「for x in *」と入力する場合)、結果として生成されるファイル・リストは、ファイル名の先頭にパス情報が含まれていないリストになります。先頭のパス情報は、basename 実行プログラムを使って取り除くこともできます。その場合は、次のようにします。

for x in /var/log/*
do
    echo `basename $x` is a file living in /var/log
done

もちろん、スクリプトのコマンドライン引数に対してループ処理を実行すると、便利な場合が多いです。以下は、この記事の冒頭で紹介した “$@” 変数の使用方法を示す例です。

#!/usr/bin/env bash

for thing in "$@"
do
    echo you typed ${thing}.
done

output:

$ allargs hello there you silly
you typed hello.
you typed there.
you typed you.
you typed silly.

シェルでの算術演算

ループ処理を行う構成体の 2 番目のタイプについて調べる前に、シェルで算術演算を実行する方法を習得しておくのが賢明です。簡単な整数計算であれば、シェルの構成体で実行できるのです。算術式を「$((」と「))」で囲むだけで、bash は式の評価を行います。以下にいくつかの例を示します。

$ echo $(( 100 / 3 ))
33
$ myvar="56"
$ echo $(( $myvar + 12 ))
68
$ echo $(( $myvar - $myvar ))
0
$ myvar=$(( $myvar + 1 ))
$ echo $myvar
57

算術演算を実行する方法がわかったところで、今度は bash でループ処理を行うさらに 2 つの構成体、つまり while と until を取り上げます。


ループ処理を行うさらに 2 つの構成体: while と until

while 文は、指定された条件が満たされている限り、処理の実行を続けます。while 文の形式は次のとおりです。

while [ condition ]
do
    statements
done

while 文は、ある回数だけループ処理を行うために使用するのが一般的です。たとえば、次の例では、ループ処理をちょうど 10 回実行します。

myvar=0
while [ $myvar -ne 10 ]
do
    echo $myvar
    myvar=$(( $myvar + 1 ))
done

ここでは算術展開を使用することによって、最終的に条件が満たされなくなり、ループ処理が終了するようにしてあることが、おわかりかと思います。

until 文は、while 文とは逆の機能を提供します。つまり、一定の条件が満たされない限り、処理が繰り返し実行されます。以下は、前の while ループとまったく同じ機能を実現する until ループです。

myvar=0
until [ $myvar -eq 10 ]
do
    echo $myvar
    myvar=$(( $myvar + 1 ))
done

case 文

case 文も、条件処理を行う便利な構成体です。以下にコード・スニペットのサンプルを示します。

case "${x##*.}" in
     gz)
           gzunpack ${SROOT}/${x}
           ;;
     bz2)
           bz2unpack ${SROOT}/${x}
           ;;
     *)
           echo "Archive format not recognized."
           exit
           ;;
esac

上記コードで bash は、まず "${x##*.}" を展開します。このコードで “$x” はファイルの名前であり、"${x##.*}" はファイル名から一番右のピリオドに続くテキスト以外をすべて取り除く働きをします。bash では、そのようにして得られた文字列と、「)」の左に記述されている値を比較します。上記コードの場合は、"${x##.*}" がまず「gz」と比較され、次に「bz2」と、そして最後に「*」と比較されます。"${x##.*}" がいずれかの文字列やパターンと一致するなら、その一致した値に続く「)」の直後から「;;」に至るまでの行が実行されます。bash はそこまで実行すると、続けて、終端の esac の後の行を実行します。どのパターンや文字列とも一致しない場合はどのコード行も実行されませんが、上記のコード・スニペットの場合は、少なくとも 1 つのコード・ブロックが実行されます。というのは、「gz」や「bz2」と一致しなかったものは、すべて「*」パターンで検出されることになるからです。


関数と名前空間

bash では、Pascal や C といった他の手続き型言語の場合と同じように、関数を定義することもできます。しかも bash の場合、スクリプトでコマンドライン引数を使用するのと非常に似通った方法で、関数が引数を取ることさえ可能です。ここでは、関数定義のサンプルを見てから、話を進めることにします。

tarview() {
    echo -n "Displaying contents of $1 "
    if [ ${1##*.} = tar ]
    then
        echo "(uncompressed tar)"
        tar tvf $1
    elif [ ${1##*.} = gz ]
    then
        echo "(gzip-compressed tar)"
        tar tzvf $1
    elif [ ${1##*.} = bz2 ]
    then
        echo "(bzip2-compressed tar)"
        cat $1 | bzip2 -d | tar tvf -
    fi
}

もう 1 つの case

上記のコードは、case 文で記述することも可能です。どのようにすればよいか、おわかりですか?

上記のコードでは、tarview という関数を定義しています。この関数は、一種の tarball を引数に取ります。この関数を実行すると、引数である tarball のタイプ (非圧縮、 gzip 圧縮、bzip2 圧縮のいずれか) が判別され、1 行の情報メッセージとして出力されてから、tarball の中身が表示されます。以下に示すのは、上記の関数を呼び出す方法です (コードを入力するか、貼り付けるか、参照するかした後、スクリプトかコマンドラインのいずれかから呼び出すことになります)。

$ tarview shorten.tar.gz
Displaying contents of shorten.tar.gz (gzip-compressed tar)
drwxr-xr-x ajr/abbot         0 1999-02-27 16:17 shorten-2.3a/
-rw-r--r-- ajr/abbot      1143 1997-09-04 04:06 shorten-2.3a/Makefile
-rw-r--r-- ajr/abbot      1199 1996-02-04 12:24 shorten-2.3a/INSTALL
-rw-r--r-- ajr/abbot       839 1996-05-29 00:19 shorten-2.3a/LICENSE
....

対話モードで使用する方法

上記サンプルのような関数は、~/.bashrc または ~/.bash_profile に含めておくことで、bash 環境でいつでも使用できるようにしておけることを忘れないでください。

関数定義の内部では、コマンドライン引数を参照する場合と同じ仕組みで、引数を参照できることがおわかりかと思います。さらに、$# マクロは引数の個数に展開されます。完全には期待通りの動作をしない可能性があるのは、唯一 $0 変数のみです。この変数は、"bash" という文字列 (シェルから対話形式で関数を実行した場合) か、関数の呼び出し元のスクリプトの名前に展開されます。


名前空間

関数の内部で環境変数を作成しなければならない場合も少なくないでしょう。もちろん、それは可能ですが、いくつかの技術的なポイントを押さえておく必要があります。ほとんどのコンパイル言語 (C など) では、関数の内部に変数を作成すると、その変数が別個のローカル名前空間に置かれます。したがって、C で myfunction という関数を定義して、その関数の中で x という変数を定義するならば、x というグローバル変数 (関数の外部にある変数) は影響を受けないので、副次作用が生じないようにすることができます。

これは、C に関して言えることですが、bash の場合は違います。bash では、関数の内部に環境変数を作成すると、その変数がグローバル名前空間に追加されます。つまり、その変数によって関数の外部にあるグローバル変数が上書きされ、関数の実行が終了した後でも、その変数は存在し続けることになります。

#!/usr/bin/env bash

myvar="hello"

myfunc() {

    myvar="one two three"
    for x in $myvar
    do
        echo $x
    done
}

myfunc

echo $myvar $x

このスクリプトを実行すると、「one two three three」という出力が生成されます。これはつまり、この関数内で定義されている $myvar がグローバル変数 $myvar を上書きしていること、そしてこの関数の実行が終了した後もループ制御変数 $x が存在し続けていること (しかも、グローバル変数 $x が定義されていたとすれば、その変数も上書きされたであろうこと) を示しています。

この簡単なサンプルでは、バグを見つけるのは容易であり、代わりの変数名を使ってバグに対処するのも難しくありません。しかし、それは正しいやり方とは言えません。この問題を解決する最善の方法は、最初からグローバル変数を上書きする可能性をなくしてしまうことです。そのためには、local コマンドを使用します。local コマンドを使用して、関数の内部に変数を作成する場合は、変数がローカル名前空間に置かれることになるので、グローバル変数を上書きすることはありません。以下は、グローバル変数が上書きされることがないように、上記のコードを実装する方法を示したものです。

#!/usr/bin/env bash

myvar="hello"

myfunc() {
    local x
    local myvar="one two three"
    for x in $myvar
    do
        echo $x
    done
}

myfunc

echo $myvar $x

この関数からは、「hello」という出力が生成されます。グローバル変数である $myvar は上書きされませんし、myfunc の外部で $x が存在し続けることもありません。この関数の最初の行では、後で使用するローカル変数 x を作成しているのに対し、2 行目 (local myvar="one two three") では、ローカルの myvar を作成し、その変数に値を代入しています。最初の行は、ループ制御変数をローカル変数に保つのに役立ちます。というのも、for local x in $myvar というコードにすることができないからです。そういうわけで、この関数はグローバル変数を上書きしません。関数を設計する場合は、いつでもこの方法を使用することをお勧めします。local を使用すべきでないのは、グローバル変数を明示的に変更する場合のみです。


まとめ

今回は、bash の最も重要な機能を取り上げたので、そろそろ bash ベースのアプリケーションを開発する方法に話を進めてもよいでしょう。次回の記事では、まさにその点を説明する予定です。それでは、次回をお楽しみに!

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=286822
ArticleTitle=bash 例解: 第 2 回 bash による初歩のプログラミングの続編
publish-date=08082013