シェルスクリプトの関数を深掘りする – ローカル変数と再帰の使い方(パートVII)
前回の「シェルスクリプトにおける関数の理解と記述方法」の記事で、シェルスクリプト内に関数を記述する基本的な方法についてはご理解いただけたかと思います。今回は、ローカル変数の使い方や再帰といった、関数のより高度な機能について掘り下げていきましょう。
ローカル変数とは
変数が「ローカル」であるとはどういうことでしょうか。それは、その変数が宣言されたブロックに依存します。local として宣言された変数は、その宣言されたコードブロック内からのみアクセス可能、つまりスコープがローカルになります。この動作を理解するために、以下の例を見てみましょう。
#!/bin/bash
func( ) {
local i=10
j=20
echo "i from func = $i"
echo "j from func = $j"
}
echo "i outside func = $i"
echo "j outside func = $j"
func
echo "i outside func = $i"
echo "j outside func = $j"
exit 0
上記のスクリプトを実行すると、出力は次のようになります。
i outside func = j outside func = i from func = 10 j from func = 20 i outside func = j outside func = 20
これは、最初の2つの echo 文が実行された時点では、関数 func がまだ呼び出されていないためです。関数 func を呼び出した後、同じ2つの echo 文は異なる結果を出力します。関数 func の内部で宣言され、local を付けられていない変数 j は、その後もアクセスできる状態になっています。
したがって、j の値は 20 となります。では、ローカル変数 i はどうでしょうか。i のスコープは関数 func の内部に限定されているため、値 10 には外部からアクセスできません。なお、local を付けずに関数内で通常どおり宣言された変数 j は、デフォルトでグローバル変数として扱われる点に注意してください。
これでローカル変数と、関数ブロック内での使い方について理解できました。次に進んで、関数の中でも最も興味深いテーマである「再帰」について見ていきましょう。
再帰とは何か?
関数が自分自身を呼び出すことを、一般に「再帰」と呼びます。あるいは、「同じアルゴリズムのより単純なバージョンを使ってアルゴリズムを表現する」ことと定義することもできます。ある数の階乗を求める例を考えてみましょう。n! = 1 × 2 × 3 × … × (n-1) × n であることはご存じのとおりです。この漸化式は次のように書けます。
n! = (n-1)! × n
つまり、同じ関数を再帰的に呼び出し、各呼び出しの戻り値を前回の結果に掛けていけばよいことになります。
5! = 4! × 5 4! = 3! × 4 3! = 2! × 3 2! = 1! × 2 1! = 0! × 1
ローカル変数を使った再帰
ここでは、ローカル変数と再帰を使って、ある数の階乗を求めるスクリプトを書いてみましょう。
#!/bin/bash
fact( ) {
local num=$1
if [ $num -eq 0 ]; then
ret=1
else
temp=$((num-1))
fact $temp
ret=$((num*$?))
fi
return $ret
}
fact 5
echo "Factorial of 5 = $?"
exit 0
num は、呼び出しごとに n-1 の値を格納するためのローカル変数です。ここでの基底条件(base case)は、数値がゼロかどうかを判定します(0! = 1 であり、負の数に対しては階乗が定義されていないため)。この基底条件に到達すると、呼び出し元に値 1 を返します。この時点で num = 1、ret = 1 × 1 となります。
続いて、この 1 を呼び出し元に返します。今度は num = 2、ret = 2 × 1 となり、以下同様に処理が進みます。最終的に num = 5 のとき戻り値は 24 となり、最終結果は ret = 5 × 24 となります。この最終結果 120 が最初の呼び出し元まで渡され、画面に表示されます。
ただし、上記のスクリプトには一つ問題があります。前回の記事で説明したとおり、bash の return で返せる値は 0〜255 の範囲に限られるため、関数は大きな整数を返せません。この制約を回避する解決策を考えるのは、読者の皆さんへの課題として残しておきます。
Q. ローカル変数を使わずに再帰は可能でしょうか? 答えは「はい」です。
ローカル変数を使わない再帰
次の例は、再帰を使ってフィボナッチ数列を表示するものです。基本となる漸化式は次のとおりです。
fib(0) = 0 fib(1) = 1 else fib(n) = fib(n-1) + fib(n-2)
フィボナッチ数列を再帰で表示するスクリプトは以下のとおりです。
#!/bin/bash
fib( ) {
a=$1
if [ $a -lt 2 ]; then
echo $a
else
((--a))
b=$(fib $a)
((--a))
c=$(fib $a)
echo $((b+c))
fi
}
for i in $(seq 0 15)
do
out=$(fib $i)
echo $out
done
exit 0
上記のスクリプトでは、ローカル変数を一切使っていません。代わりに、関数の標準出力をコマンド置換 $( ) で受け取ることで、計算結果を呼び出し元へ伝えています。実行時の処理の流れは、皆さんなら理解できるでしょう。
ここで指定している値 15 は、表示するフィボナッチ数列の項数を表しています。上記スクリプトの実行について何か気づいたことはありませんか? 実行に少し時間がかかるのではないでしょうか。実は、シェルスクリプトにおける再帰は、C言語のようなコンパイル型プログラミング言語での再帰よりも処理が大幅に遅くなります。各関数呼び出しが別プロセスとして扱われるオーバーヘッドがあるためです。
以上で、シェルスクリプトにおける関数の解説はひとまず完了です。今後の配列に関する記事など、続編にもぜひご期待ください。
-
Bashシェル関数入門:実用的な6つのサンプル例で学ぶチュートリアル
Bashシェル関数とは、複数のUNIX/Linuxコマンドをひとつの名前でグループ化し、後からまとめて実行できるようにする仕組みです。Bashシェル関数は通常のUnixコマンドと同じように呼び出すことができ、現在のシェルコンテキスト内で実行されるため、解釈用の新しいプロセスを生成する必要がありません。bashのエイリアス(alias)と関数はどちらも、長いコマンドや複雑なコマンドに対するショートカットを定義するのに役立ちます。しかし、エイリアスでは制御フローや引数の扱いなど、関数ならではの高度な処理を行うことはできません。本記事では、その違いも含めて詳しく解説していきます。この記事は、進行中の
-
実用的な5つの例で学ぶBashスクリプト入門チュートリアル
Unix SedやUnix Awkシリーズと同様に、本シリーズではBashスクリプティングに関する記事を複数回にわたって公開します。実用的なサンプルを交えながら、Bashスクリプティングのテクニックを幅広く解説していきます。 シェルとは、ユーザーが入力したコマンドを解釈して実行するプログラムです。コマンドはユーザーが直接入力するか、「シェルスクリプト」と呼ばれるファイルから読み込まれます。 ユーザーからの入力を直接読み取る場合、そのシェルは対話型(インタラクティブ)シェルと呼ばれます。 一方、ファイルからコマンドを読み込んで実行する場合は非対話型(ノンインタラクティブ)シェルと呼ばれます。この