bash 例解: 第1回 Bourneシェルの生まれ変わり(bash)による初歩のプログラミング

bash スクリプト言語によるプログラミングを習得すると、Linux での日常的な作業がとても楽しく、一層生産的になるばかりか、これまで愛用してきた標準的な UNIX 構成体 (パイプラインやリダイレクションなど) をさらに拡張することも可能になります。この 3 回シリーズの連載記事では、Daniel Robbins 氏が bash プログラミングについて実例をまじえながら説明します。まずは初心者のために基本中の基本ともいうべき事柄を取り上げてから、少しずつ詳しい機能を織り混ぜてゆくことになるでしょう。

Daniel Robbins (drobbins@gentoo.org), 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 さんとの時間を大切にする良き夫でもあります。氏の連絡先はdrobbins@gentoo.org です。



2000年 3月 01日

ところで、なぜ bash プログラミングなのだろう、という素朴な疑問があるかもしれません。それには、強力な理由があります。そのいくつかを最初に取り上げましょう。

すでに実行している

少し調べてみれば分かることですが、Linux のユーザーであればおそらく今でも bash を実行しているはずです。デフォルトのシェルを変更した場合でも、bash はやはり システムのどこかで実行中になっている可能性が高いのです。これは、標準的な Linux シェルであり、さまざまな用途があるからです。bash がすでに実行中であれば、追加の bash スクリプトを実行するとしても、元々実行中になっている bash プロセスとの間でメモリーを共用することになるので、最初からメモリー効率が高くなります。すでに効率的に動作しているシェルがあるのであれば、わざわざ 500K のインタープリターを読み込む必要があるでしょうか。


すでに使用している

bash はどこかで実行中になっているだけでなく、毎日の作業の中で実際に使用されているはずです。現に手元にあるわけですから、その潜在能力を最大限に引き出せるような使い方を勉強するというのは当然ではないでしょうか。そうすれば、bash での作業がさらに楽しく、一層生産的になります。とはいうものの、bash のプログラミング を勉強するのはなぜでしょうか。まず、簡単に習得できるという理由があります。コマンドの実行、ファイルへの遅延時間の測定、出力のパイプやリダイレクトといった操作はすでにお手のものでしょう。そうであれば、すでに使い方を知っているそうしたパワフルな高速処理の構成体を使用したり拡張したりするための言語を習得するのは、自然な流れではないでしょうか。コマンド・シェルは UNIX システムの可能性を大きく広げます。その点で bash は、Linux シェルの最高峰 と言えます。まさに、ユーザーとマシンを結び付ける高度な接着剤のようなものです。bash の知識を深めれば、Linux や UNIX での生産性は自動的に向上してゆきます。事はこれほどシンプルなのです。


bash の混乱

間違った方法で bash について勉強すると、頭が混乱してしまうかもしれません。初心者はおそらく "man bash" と入力して、bash の man ページを表示しようとするでしょうが、結局ユーザーの目の前に突き付けられるのは、シェルの機能に関する非常にそっけない技術的な説明だけです。あるいは、"info bash" と入力して、GNU の info 文書を表示する人もいるでしょう。その場合は、やはり man ページが表示されるか、ほんの少し分かりやすくなった info 文書が表示されるかのいずれかです。

初心者にとっては確かに残念なことでしょうが、標準的な bash の文書は万人向きではありません。むしろ、シェル・プログラミング全般に通じている人が対象になっています。もちろん、man ページにはすばらしい技術情報がぎっしり詰まっていますが、初心者にとって役立つかという点では、確かに十分とは言えないのです。

この隙間に狙いを付けたのがこの連載記事です。この連載では、bash プログラミングの各種構成体を実際に使用する方法を説明してゆきたいと思っています。読者が自作のスクリプトを作成するための一助になるとすれば、たいへん幸いです。ですから、技術的な観点からの説明というよりは、むしろ普通の日常的な言葉での説明を心がけていますので、さまざまな機能についてだけでなく、実際にそれらの機能をどんな場合に使用できるのかということまで掘り下げてみたいと思います。この 3 回シリーズが終わるころには、読者も込み入った bash スクリプトを自分で作成できるようになり、bash を快適に使いこなしたり、標準的な bash 文書を読みこなしたりできるようなレベルにまで到達していることでしょう。では、ご一緒に始めましょう。


環境変数

bash をはじめ、ほとんどのシェルでは、ユーザーが環境変数を定義できます。環境変数は、内部的には ASCII 文字列として保管されます。環境変数の一番難しいところは、環境変数が UNIX プロセス・モデルの一部として組み込まれる点にあります。つまり、環境変数はシェル・スクリプトだけの専用変数ではなく、コンパイルされている標準的なプログラムからも使用されるということです。bash で環境変数を "エクスポート" した場合、生成されたプログラムを実行すると、シェル・スクリプトであってもなくても、その設定が読み込まれることになります。この点を説明するのに都合のよい例は vipw コマンドです。このコマンドは普通、root からシステムのパスワード・ファイルを表示します。EDITOR 環境変数を自分好みのテキスト・エディターの名前に設定しておくと、vipw は vi の代わりにそのエディターを使用するようになります。xemacs に慣れていて、vi があまり好きでないユーザーにとっては、たいへん便利な設定でしょう。

bash で環境変数を定義するための標準的な方法は次のとおりです。

$ myvar='This is my environment variable!'

上記のコマンドでは、"myvar" という環境変数を定義し、その変数に "This is my environment variable!" という文字列を入れました。ここでいくつかの点を確認しておきたいと思いますが、まず "=" 記号の両側にスペースが入っていません。実際にやってみれば分かりますが、スペースを入れるとエラーになります。次に確認したいのは引用符です。1 つの単語だけを定義する場合は引用符は不要ですが、環境変数の値が複数の単語からなる場合 (つまり、スペースやタブが入る場合) は引用符が必要になります。

bash での引用符の使い方については、非常に詳しい説明が bash の man ページの "QUOTING" セクションにあります。他の値に "展開される" (置き換えられる) 特殊文字シーケンスを入れると、bash での文字列の扱い方が非常に複雑になります。この連載では、ごく一般的な引用符機能だけを取り上げます。

3 つ目の点として、単一引用符と二重引用符の区別について確認しておきましょう。普通は単一引用符ではなく二重引用符を使用しますが、上記の例で二重引用符を使用するとエラーになります。なぜでしょうか。単一引用符を使用すると、bash の展開機能がオフになります。展開機能というのは、特殊文字や特殊文字シーケンスを他の値で置き換える機能です。たとえば、"!" という文字は履歴展開文字です。つまり、bash ではこの文字が、直前に入力したコマンドに置き換えられます。(この連載では、履歴展開について詳しく触れません。bash プログラミングではそれほど頻繁に使用されないからです。詳細については、bash の man ページの "HISTORY EXPANSION" セクションを参照してください。) このマクロのような機能は確かに便利ですが、上記の例では、環境変数の最後の部分に文字通りの感嘆符を入れたいわけで、特にマクロとして使う意図はないのです。

さて次に、環境変数の実際の使い方を見てみることにしましょう。以下はその例です。

$ echo $myvar
This is my environment variable!

環境変数の名前の前に $ を付けてあるのは、その部分を myvar の値で置き換えるためです。bash の用語では、これを "変数展開" といいます。では、次のような場合はどうでしょうか。

$ echo foo$myvarbar
foo

このコマンドでは、"fooThis is my environment variable!bar" と表示したかったわけですが、そのようにはゆきませんでした。どこがまずかったのでしょうか。簡単に言えば、bash の変数展開機能が混乱してしまったということです。つまり、展開対象の変数が $m なのか、$my なのか、$myvar なのか、$myvarbar なのかを判別できなかったわけです。では、どの変数かを明示するには、どうしたらよいでしょうか。次のコマンドを試してみてください。

$ echo foo${myvar}bar
fooThis is my environment variable!bar

お分かりのとおり、環境変数名が周りのテキストからはっきりと分離されていない場合は、環境変数名を中括弧で囲めばよいわけです。確かに $myvar と入力するほうが簡単ですし、大抵はそれでうまくゆくはずですが、${myvar} と入力すれば、ほとんどの状況で正確に構文解析が行われることになります。それ以外の点では、その両者はまったく同じ機能を果たします。この連載では、両方の形式の変数展開が登場するはずです。それでも、環境変数が空白 (スペースやタブ) によって周りのテキストから分離されていない場合は、中括弧による明示的な形式を使用するとよいでしょう。

ところで、変数を "エクスポート" できるという点に先ほど触れました。環境変数をエクスポートすると、それ以降に実行するスクリプトや実行可能プログラムの環境では、その環境変数が自動的に使用されることになります。シェル・スクリプトは、そのシェルの組み込み環境変数サポートによってその環境変数に "到達" しますが、C プログラムの場合は、getenv() 関数呼び出しを使用します。以下は、サンプルの C コードです。C の観点から環境変数を理解するために、このサンプルを実際に入力して、コンパイルしてみましょう。

myvar.c -- a sample environment variable C program
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  char *myenvvar=getenv("EDITOR");
  printf("The editor environment variable is set to %s\n",myenvvar);
}

上記のソースを myenv.c というファイルに保存してから、次のコマンドでコンパイルしてください。

$ gcc myenv.c -o myenv

ここで読者のディレクトリーに生成されるのは、EDITOR 環境変数の値を出力するための実行可能プログラムです。そのプログラムを筆者のマシンで実行してみると、次のような出力が得られます。

$ ./myenv
The editor environment variable is set to (null)

これはつまり、EDITOR 環境変数が設定されていないので、ヌルが出力されたというわけです。その環境変数に値を設定してみましょう。

$ EDITOR=xemacs
$ ./myenv
The editor environment variable is set to (null)

この場合は、myenv によって "xemacs" という値が出力されるはずでしたが、そのようにはなりませんでした。なぜかと言えば、EDITOR 環境変数をまだエクスポートしていないからです。次はうまくゆくはずです。

$ export EDITOR
$ ./myenv
The editor environment variable is set to xemacs

そういうわけで、環境変数をエクスポートするまでは、別のプロセス (この場合はサンプルの C プログラム) で環境変数を表示できないことを確かめました。ちなみに、環境変数の定義とエクスポートを 1 行で済ませることも可能です。たとえば、次のようにします。

$ export EDITOR=xemacs

このコマンドは、上記の 2 行のコマンドと同じ機能を果たします。この際、せっかくですから、unset を使って環境変数を消去する方法も示しておきましょう。

$ unset EDITOR
$ ./myenv
The editor environment variable is set to (null)

dirname と basename

dirname と basename はいずれも、ディスク上のファイルやディレクトリーをまったく参照しません。むしろ、この 2 つは純粋な文字列操作コマンドです。


文字列切り分け操作の概要

文字列切り分け操作 (つまり、元の文字列をいくつかの部分に分割する操作) は、普通のシェル・スクリプトで日常的に実行される操作です。シェル・スクリプトでは、完全修飾パスによって終端のファイルやディレクトリーを見つけるのが普通です。確かに、bash で実際にコーディングするのも可能ですし、たいへん楽しいことなのですが、UNIX 標準の basename 実行可能プログラムはこの点でたいへん便利な機能を果たします。

$ basename /usr/local/share/doc/foo/foo.txt
foo.txt
$ basename /usr/home/drobbins
drobbins

basename は、文字列を切り分けるための便利なツールです。その姉妹版ともいうべき dirname は、basename が切り捨てた "反対側" のパスを戻します。

$ dirname /usr/local/share/doc/foo/foo.txt
/usr/local/share/doc/foo
$ dirname /usr/home/drobbins/
/usr/home

コマンド代入

実行可能コマンドの結果を組み込んだ環境変数を作成する方法も、知っておくと便利でしょう。方法はとても簡単です。

$ MYDIR=`dirname /usr/local/share/doc/foo/foo.txt`
$ echo $MYDIR
/usr/local/share/doc/foo

上記の処理は "コマンド代入" といいます。この例で、いくつかの点を確認しておきましょう。まず 1 行目ですが、実行するコマンドを逆引用符 で囲んでおきました。逆引用符は標準の単一引用符とは違います。この文字は、Tab キーの上にあるキーで入力するのが普通です。bash のコマンド代入構文でも、まったく同じ処理を実行することができます。

$ MYDIR=$(dirname /usr/local/share/doc/foo/foo.txt)
$ echo $MYDIR
/usr/local/share/doc/foo

これでお分かりのとおり、bash には、同じ処理を実行するための方法が複数用意されています。コマンド代入機能によって、任意のコマンドやコマンド・パイプラインを ` ` または $( ) の中に入れて、環境変数に割り当てることができます。本当に便利な機能ですね。以下は、コマンド代入でパイプラインを使用する方法を示した例です。

MYFILES=$(ls /etc | grep pa)
bash-2.03$ echo $MYFILES
pam.d passwd

プロ並みの文字列切り分け操作

確かに、basename と dirname はすばらしいツールですが、標準的なパス名操作よりもさらに高度な文字列 "切り分け" 操作を実行しなければならない場合もあることでしょう。さらにパンチを利かせたい場合は、bash の高度な組み込み変数展開機能を活用できます。標準の変数展開 (${MYVAR}) についてはすでに見たとおりですが、bash には独自の便利な文字列切り分け機能があります。以下の例を見てみましょう。

$ MYVAR=foodforthought.jpg
$ echo ${MYVAR##*fo}
rthought.jpg
$ echo ${MYVAR#*fo}
odforthought.jpg

この最初の例では、${MYVAR##*fo} と入力しました。これはいったいどういう意味でしょうか。基本的に、${ } の中には、環境変数の名前と 2 つの # とワイルドカード ("*fo") を入力しました。この場合、bash は MYVAR を取り込んで、"foodforthought.jpg" という文字列の先頭から、ワイルドカード "*fo" と一致する最長の サブ文字列を見つけ出し、その部分を元の文字列の先頭から切り取った残りを表示しました。確かに、最初は理解しにくいところなので、この特殊な "##" オプションの働きの "感じ" をつかむために、この展開の様子を最後まで見てみましょう。まず、"foodforthought.jpg" の先頭から、"*fo" ワイルドカードと一致するサブ文字列を検索します。検出されるサブ文字列は以下のとおりです。

f	
fo		MATCHES *fo
foo	
food
foodf		
foodfo		MATCHES *fo
foodfor
foodfort	
foodforth
foodfortho	
foodforthou
foodforthoug
foodforthought
foodforthought.j
foodforthought.jp
foodforthought.jpg

この中で、一致項目が 2 つ見つかっています。そのうちの長いほうを選択し、元の文字列の先頭からその部分を切り捨ててから、残りの結果を戻したことになります。

変数展開の 2 番目の例は最初の例と同じように見えますが、"#" が 1 つだけ使用されている点が違います。確かに、bash はほとんど 同じプロセスを実行します。まず検出するサブ文字列のセットは最初の例とまったく同じです。ところが、元の文字列から切り捨てられるのは、最短の 一致項目であり、その残りの結果が表示されています。したがって、"fo" というサブ文字列を検出した時点で、すぐに元の文字列から "fo" を切り捨て、残りの "odforthought.jpg" を出力したことになります。

このままではポイントがはっきりしないかもしれないので、この機能をしっかり覚えるための簡単な方法を示しましょう。最長の一致項目を検索するときは、## を使います (## のほうが # よりも長いですね)。一方、最短の一致項目を検索するときは、# を使ってください。こんなふうにまとめれば、それほど難しくないでしょう。それにしても、'#' を使って文字列の "先頭" から一致項目を切り捨てるということは、どうやって覚えたらよいのでしょうか。これも簡単です。キーボードを見てください。Shift+4 が "$" になっていますね。これはつまり、bash の変数展開文字です。さて、キーボードで "$" のすぐ左にあるのが "#" です。ということは、"#" は "$" の "先頭" にあるということにならないでしょうか。したがって、この記憶術によれば、"#" は文字列の先頭から文字を切り捨てるということになるわけです。ところで、文字列の末尾から文字を切り捨てるにはどうしたらよいでしょうか。勘を働かせてみてください。キーボードで "$" のすぐ右 (末尾の方向) にある文字 ("%") を使えばよいのではないでしょうか。そのとおりです。以下は、文字列の末尾部分を切り捨てる方法を示した簡単な例です。

$ MYFOO="chickensoup.tar.gz"
$ echo ${MYFOO%%.*}
chickensoup
$ echo ${MYFOO%.*}
chickensoup.tar

お分かりのとおり、変数展開オプションの % と %% は、元の文字列の末尾からワイルドカード一致項目を切り捨てるという点以外は、# と ## の場合とまったく同じ働きをしています。ちなみに、末尾からサブ文字列を切り捨てる場合は、"*" を入れなくても同じ結果が得られます。

MYFOOD="chickensoup"
$ echo ${MYFOOD%%soup}
chicken

この例では、"%%" を使うか "%" を使うかで違いは生じません。一致項目が 1 つしかないからです。そして、念のためにもう一度付け加えておきますが、万一 "#" と "%" の区別を忘れてしまった場合でも、キーボードの 3、4、5 のキーを見れば、すぐに思い出せますね。

ところで、変数展開にはもう 1 つの形式があります。つまり、文字のオフセットや長さに基づいてサブ文字列を選択する形式です。bash で以下の行を入力してみてください。

$ EXCLAIM=cowabunga
$ echo ${EXCLAIM:0:3}
cow
$ echo ${EXCLAIM:3:7}
abunga

この形式の文字列切り分け操作もたいへん便利です。先頭の文字とサブ文字列の長さを指定して、全体を引用符で囲めばそれでよいのです。


文字列切り分け操作の応用

文字列切り分け操作についての説明が終わったので、実際に簡単なシェル・スクリプトを作成してみましょう。このスクリプトでは引き数として 1 つのファイルを取って、それが tarball (tar ファイル) のように見えるかどうかを判別して、結果を出力します。それが tarball であるかどうかの判定基準は、ファイルの末尾に ".tar" が付いているかどうかという点になります。以下に示すのがそのスクリプトです。

mytar.sh -- a sample script
#!/bin/bash
if [ "${1##*.}" = "tar" ]
then
	echo This appears to be a tarball.
else
	echo At first glance, this does not appear to be a tarball.
fi

このスクリプトを実行するには、mytar.sh というファイルにそのスクリプトを入力してから、"chmod 755 mytar.sh" というコマンドによってそのファイルを実行可能プログラムにします。まず、1 つの tarball で試してみてください。

$ ./mytar.sh thisfile.tar
This appears to be a tarball.
$ ./mytar.sh thatfile.gz
At first glance, this does not appear to be a tarball.

確かに、きちんと動作しています。が、それほど機能的であるとは言えませんね。もう少し便利なスクリプトにする前に、上記の "if" ステートメントを見てみましょう。その中には、ブール式が入っています。bash の場合、"=" 比較演算子は、文字列が等しいかどうかをチェックします。また、bash の場合、すべてのブール式は、大括弧で囲むことになっています。それにしても、このブール式では実際に何をチェックするのでしょうか。この式の左側に注目してください。文字列切り分け操作についてすでに学習したとおり、"${##1*.}" は、環境変数 "1" に入っている文字列の先頭から、"*" の最長一致項目を切り捨て、残りの結果を戻します。この場合は、ファイル名の最後の "." の後の文字列が戻されるわけです。したがって、ファイルの末尾が ".tar" であれば、結果として "tar" が戻され、ブール式の条件が満たされることになります。

ところで、そもそも "1" 環境変数とは何物かという疑問が沸くかもしれません。これも非常に簡単です。$1 はスクリプトの最初のコマンド行引き数、$2 は 2 番目のコマンド行引き数という具合になります。というわけで、この機能の説明についてはこれぐらいにして、"if" ステートメントの解説に移ることにしましょう。


IF ステートメント

ほとんどの言語と同じように、bash にも独自の条件形式があります。条件を指定する場合は、上記の形式をしっかり守ってください。つまり、"if" と "then" を別々の行に入れ、"else" と終端の "fi" (必須) をそれらと同じ桁そろえにするという点です。このような形式はたいへん読みやすく、デバッグも容易です。"if" ステートメントには、"if、else" 以外の形式もあります。

if [ condition ]
then
	action
fi

この場合は、condition の条件が満たされた場合にのみアクションが実行され、それ以外の場合はアクションなしで、"fi" 以降の行の処理が続けられることになります。

if [ condition ]
then 
	action
elif [ condition2 ]
then
	action2
.
.
.
elif [ condition3 ]
then
else
	actionx
fi

上記の "elif" 形式では、各条件が連続的にチェックされ、最初に適合した 条件に設定されているアクションが実行されます。どの条件も適合しなかった場合は、"else" でアクションが指定されていればそのアクションが実行され、"if、elif、else" ステートメント以降の行の処理が続けられます。


次回の予告

今回は、bash の初歩の初歩ともいうべき機能を取り上げたので、そろそろペースを上げて、実際のスクリプトを作成するための準備をしましょう。次回の記事では、ループ構成体、関数、名前空間などの大切な要素について説明する予定です。それをマスターすれば、もっと複雑なスクリプトを作成する準備ができるはずです。3 回目の記事では、非常に複雑なスクリプトや関数、さらには 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=287114
ArticleTitle=bash 例解: 第1回 Bourneシェルの生まれ変わり(bash)による初歩のプログラミング
publish-date=03012000