関数は共通部品になる:ステップ・バイ・ステップ・シェルスクリプト(6)
プログラムを組んでいくと、なんども同じような処理を繰り返すことがあります。例えば、画面に決まったフォーマットで表示する、といった機能などです。こうしたよく使う機能を関数としてまとめることは、シェルスクリプトでも可能です。
ユーザー定義関数の使い方
今回は関数のお話をしましょう。シェルスクリプトでも関数が使えます。ところで「関数」とはなんでしょうか? 通常プログラムの世界で関数というと、ほかのプログラムから呼ばれる「共通部品」のことを指しますね。オブジェクト指向プログラミング(C++、Javaなど)の普及で古い言葉になってしまいましたが、昔は「サブルーティン」などと呼んでいました。もちろん厳密な意味では少し違うところもありりますが。
ではシェルスクリプトの世界ではどうでしょう? ここでも関数は同じようにシェルスクリプトから呼ばれる共通部品のことです。シェルスクリプトから呼ばれるものというと? そうですシェルスクリプトの世界では、関数はただのコマンドと同じ扱いとなるのです。実は関数はシェルスクリプトだけでなくコマンドライン(インタラクティブなシェル)でも使用できます。では早速やってみましょう。
hello () { echo $1 }
これを「func_hello」というファイルに記述してセーブします。関数を、現在起動しているシェルに登録してみましょう。コマンドラインから以下のように実行します。
$ . func_hello
ピリオド(.)のあとにスペースを空けて、上記で作成したファイル名を記述します。ファイルfunc_helloに実行権をつける必要はありません。またCシェルなどと同じようにsourceコマンドを使っても同じことができます。
$ source func_hello
これで関数の登録ができました。早速登録した関数を実行してみましょう
$ hello "Hello World" Hello World
これは第2章でやったように、シェルスクリプトとして実行した場合と同じでしょうか? 実は見た目は同じですが、少しだけ違います。シェルスクリプトを起動した場合は、起動する前のシェルとは別のシェルが起動され、指定されたスクリプトが実行されますが、関数の場合は実行したシェル内で起動されます。そういった意味で関数は自分で新たな「内部コマンド」を作成したのと同じ状況となるのです。
もし上記で作成したファイル func_hello に実行権(chmod +x)を付与して直接実行したら、どうなるでしょうか? テストのために違う関数名を登録してみます。ファイル名を「func_hello2」、関数名をhello2とします。
func_hello2の中身 hello2 () { echo $1 } $ chmod +x func_hello2 $ ./func_hello2 $ hello2 "Hello WORLD" bash: hello2: command not found
ピリオド(.)やsourceコマンドを使う代わりに実行権を付けて、前回でやったようにfunc_hello2を実行してみました。シェルスクリプトの実行そのものはうまくいったようですが、上記の結果を見るかぎり関数はただしく登録されていないようです。
これはなぜかというと、先ほどもすこし触れたようにシェルスクリプトとして実行されたコマンドは、まず別のプロセスとしてbashを起動します。上記では新たに起動されたシェルに対して関数hello2が登録され、シェルスクリプトfunc_hello2が終了し、元のシェルに戻って来た時点で消えてなくなってしまったということです。
元のシェル(親シェル)にとってみてば、起動されたシェル(子どもシェル)の中でどのような関数が新たに登録されようが、知らぬ存ぜぬなので、上記のように「command not find」という表示になったしまったわけです。ですから、関数を登録する時は、「ピリオドかsourceを使って行なう」と覚えておいておけばよいと思います。
シェルスクリプトで関数を使うために、各シェルごとにかならず登録しなければならないのは面倒な場合があります。そのような場合には、親シェルで登録した関数を子どもに引き継ぐようにすることができます。前回御紹介した変数の場合と同じように「export」を使います。
$ export -f hello
こうすることにより、今後このシェルより実行するシェルスクリプトでも関数を使用することができるようになります。この機能はもともとのボーンシェル(sh)にはなく、bashによる拡張機能です。
引数の取り扱い
関数に対して、実行される場面によっていろいろな引数を渡すにはどうすればいいのでしょうか? 実は上の例ですでに行なっているとおり、関数が実行されたときに渡された関数は、$1、$2、$3……で参照することができます。これは前回紹介したとおり、シェルスクリプトに引数を渡す場合と全く同じやりかたです。
func_argtestというファイル名で以下のような関数を作成してみます。
argtest () { i=1 while [ "$1" != "" ] ; do echo "Argument $i is " $1 shift 1 i=`expr $i + 1` done }
登録して実行してみましょう。
$ source func_argtest $ argtest a b c d Argument 1 is a Argument 2 is b Argument 3 is c Argument 4 is d
特に難しいところはありませんね。「shift」コマンド等の使い方は前回の「シェルの変数に慣れる」を参照してください。
関数と変数
関数は上記で説明しましたとおり、呼び出したシェルの内部で実行されます。そのため、関数内においても起動したシェルの変数を(exportしなくても)参照することができます。また関数内で設定した変数は元のシェルでも参照することができます。つまり呼出元シェルと関数は変数を共有しているのです。このことは便利な反面、関数内で使用された変数が呼出元シェルでも使われていた場合は、その値は関数の実行後に書き換えられてしまう事態が発生しえます。これはプログラム言語的に見ても変数の「カプセル化」ができないということになり、予測されない事態が起きる可能性があります。デバッグも非常にやりにくくなります。これを避けるため、bashには「local」という関数用のコマンドが用意されています。使い方は簡単で、以下のように「宣言」するだけです。
local name
または値の代入も一緒にやってしまうこともできます。
local name=value
このようにしておけば、変数nameはこの関数内だけで参照できるローカル変数となり、起動元のシェルの変数を書き換えてしまうことはありません。関数内で変数を使用する場合は、できるだけこのlocalを使うようにしましょう。上記の例のargtestを以下のように修正しておきます。
argtest () { local i=1 while [ "$1" != "" ] ; do echo "Argument $i is " $1 shift 1 i=`expr $i + 1` done }
aliasと関数
bashにはCシェル(csh)と同じように「alias」がサポートされています。これは入力されたコマンドを変換して実行するもので、入力を簡略化されるためによく使われます。例えば、
alias ls='ls -CF --color=tty'
としておくと、コマンドラインやシェルスクリプトからlsを実行した場合に実行される前に
$ ls -CF --color=tty
のように置き換えられて実行されます。これも実行前に置き換えが行なわれます。もちろんlsの後に引数を書くこともできます。このalias機能はCシェルとの互換性のために用意されています。というよりもCシェルユーザがbashに移行する場合のためだけだと考えてよいかと思います。bashを含むボーンシェルには関数が用意されているため、aliasより柔軟性にとんだ関数を使う方がずっと便利なのです。上記の例を関数で記述すると以下のようになります。
ls () { command ls -CF --color=tty $@ ; }
関数を1行で書いてしまう場合には最後に(;)セミコロンが必要です、注意してください。また関数では引数を処理する必要があるので、最後に「$@」が必要です。これは前回の変数のところででてきましたが、与えられた引数をそのまま羅列する変数です。そして「command」というコマンドがでてきました。これは登録されている関数「以外」のコマンドを実行する、というコマンドです。つまり関数は実行の優先度が高いため、なにも指定しないと関数内でも関数の「ls」を呼んでしまいます。これでは無限ループにはいってしまいますので、「command」を使って関数を起動しないようにしているのです。もちろんフルパス(/bin/ls)で指定してもかまいません。こちらの方が普通だと思います。今までCシェルを使っていた人でaliasに馴染みがある人も、徐々に関数に移行していくようにしましょう。
関数の戻り値
数はその実行結果を「戻り値」にセットして返すことができます。関数の戻り値は関数内で最後に実行されたコマンドの戻り値となります。またreturnを使えば明示的に戻り値をセットすることができます。また他のコマンドと同じように呼び出したシェルにおいては「$?」で戻り値を参照することができます。前出の関数argtestを少し書き換えてみましょう。
argtest () { [ $# -ne 0 ] || echo "There is no argument." ; return 1 local i=1 while [ "$1" != "" ] ; do echo "Argument $i is " $1 shift 1 i=`expr $i + 1` done return 0 }
少し変わった書き方をしてみました。
[ $# -ne 0 ] || echo "There is no argument." ; return 1
見ためがわかりにくいのであまりお勧めはしないのですが、シェルスクリプトにおける伝統的な書き方でもあります。[]の中は条件式の評価が行なわれます。|| は前の条件式が「偽」の場合に以降(右側)のコマンドを実行するという意味です。
[ 条件式 ] || (ここのコマンドは条件が「偽」なら実行される) [ 条件式 ] && (ここのコマンドは条件が「真」なら実行される)
となります。本題と少しずれてしまいましたが、上述の関数の1行めでは「引数の数($#)が0でない」という条件式が「偽」なら、つまり引数が0なら後ろのコマンドが実行され戻り値1で終了します。それ以外の場合は引数表示が実行され、戻り値0で正常終了(return 0)しています。なお、(;)セミコロンは1行にコマンドを続けて記述する場合に使用する区切り文字です。ただ単に左から右へ順番にコマンドが実行されます。
条件式の部分はシェルの内部コマンド「test」が前に省略されていると考えればわかりやすいでしょう。条件式の記述方法については、前章「制御文で道先案内」を参照してみてください。
ここまででシェルやシェルスクリプトに関する基本的な機能の解説を終了し、次回は少し実践的な具体例をあげて解説していきたいと思います。
Copyright © ITmedia, Inc. All Rights Reserved.