4. extract(1) - メインの処理 for, case
シェルスクリプトの場合、最終的に実行される処理はファイルの後方に記述されていることが多くなります。このシェルスクリプトのメイン処理部分はリスト4.1です。ソースコードを読む場合、まずその処理の本質部分を探し当てて、何をしているか把握することが大切となります。枝葉末節は後から調べれば問題ありません。
リスト4.1の処理は、for
構文で引数に指定されたアーカイブに対して順次処理を適用するというものです。適応する処理は、拡張子を判定基準にしてcase
構文で分岐させています。この例では、「tar+GNU zip」「tar+bzip2」「GNU zip」「ZIP」「LHa」をそれぞれ拡張子から判別して展開処理を行っています。
for target do WORKDIR=$(mktemp -d "$EXTRACTHOMEDIR"/XXXXXX) case "$target" in *.[Tt][Gg][Zz]|*.[Tt][Aa][Rr].[Gg][Zz]|*.[Tt][Aa][Rr].[Zz]) tar vzxf "$target" -C "$WORKDIR" move_to_currentpath "$WORKDIR" "$target" ;; *.[Tt][Bb][Zz]|*.[Tt][Aa][Rr].[Bb][Zz]2) tar vjxf "$target" -C "$WORKDIR" move_to_currentpath "$WORKDIR" "$target" ;; *.[Gg][Zz]) gzip -d "$target" move_to_currentpath "$WORKDIR" "$target" ;; *.[Zz][Ii][Pp]|*.[JjWw][Aa][Rr]) check_required_program unzip /usr/ports/archivers/unzip/ unzip "$target" -d "$WORKDIR" move_to_currentpath "$WORKDIR" "$target" ;; *.[Ll][Zz][Hh]) check_required_program lha /usr/ports/archivers/lha/ lha ew="$WORKDIR" "$target" move_to_currentpath "$WORKDIR" "$target" ;; *) echo "unkown compress type: $target" ;; esac rm -r -f "$WORKDIR" done
リスト4.1で使っているfor
構文ではin
以降が指定されていません。このようにfor
構文を使った場合、コマンド引数が変数に代入されて使われます。引数に対して順次処理をおこないたい場合の常套手段です。便利なので覚えておくとよいでしょう。
次に、引数に指定されたファイル名をベースにして、case
構文で処理を分岐させています。ここで注目するべきはcase
構文で使われているパターンです。「拡張子を調べる」「大文字と小文字を区別しない」「複数のパターンのどれかに一致すればいい」といったパターンで、case
構文のパターンで使われる代表的なものです。この使い方も覚えておくとよいでしょう。
例えば「 *.[Tt][Gg][Zz]|*.[Tt][Aa][Rr].[Gg][Zz]|*.[Tt][Aa][Rr].[Zz])」ですが、まず先頭の空白タブは無視されます。つまりパターンは「*.[Tt][Gg][Zz]|*.[Tt][Aa][Rr].[Gg][Zz]|*.[Tt][Aa][Rr].[Zz]」となります。パイプでパターンが区切られているので、実際は「*.[Tt][Gg][Zz]」「*.[Tt][Aa][Rr].[Gg][Zz]」「*.[Tt][Aa][Rr].[Zz]」のどれかのパターンに一致すれば、ということになります。
*はワイルドカードなので、この場合はファイル名の最後が.[Tt][Gg][Zz]のものは、という指定になります。また、[Tt]はTまたはtはという意味なので、つまり「.tgz」「.TGZ」「.tgZ」「.TGz」「.tGz」といったような拡張子のファイル名に一致することを表しています。要するに、大文字を小文字を区別せずに拡張子を調べています。
このような指定はcase
構文のパターン指定ではよく用いられるものなので覚えておくとよいでしょう。[Yy][Ee][Ss]|[Yy]や[Nn][Oo]|[Nn]といったパターンもです。こういったシェルスクリプトのfor
構文やcase
構文は便利で強力なので、早めに習得してしまうとよいでしょう。
5. シェルスクリプトへのオプションを処理 - getopts
簡単なシェルスクリプトであれば必要ありませんが、作ったシェルスクリプトを便利に使い始めると、オプションを追加して挙動を指定できるようにしたくなることも多くなります。シェルスクリプトにおいてオプションを処理する方法には大きく分けて2つのやり方がありますが、ここではその一つのやり方を紹介します。便利ですが忘れやすいので、これは一度どこかに書いたものをコピー&ペーストして使い回せばよいでしょう。
while getopts hv option do case "$option" in h) echo "${usage_msg}" exit 0 ;; v) echo "version 1.9" exit 0 ;; esac done shift $(($OPTIND - 1))
リスト5.1がオプションを処理している部分です。-h
で使い方を表示してプログラムを終了し、-v
でバージョンを表示してプログラムを終了します。--help
とか--device=/dev/acd0
のようなオプションは、この方法では指定することができません。-h
とか-d /dev/acd0
のように旧来の方法で指定する必要があります。
覚えておくのは「getopts hv
」という部分です。これは-h
と-v
というオプションがあることを指定しています。指定できるオプションは1文字で構成される必要があります。オプションで、さらに値も持たせたい場合、例えば「-s 2
」といった指定を行いたいのであれば「getopts s:hv
」のように指定します。値はその都度${OPTARG}
変数に格納されているので、それを使います。
getopts
にオプションで使う文字を指定して、while
で処理を切り出し、case
でオプションごとに処理を分岐します。最後に、この処理以降でオプションを飛ばして、他の引数だけをコマンドの引数として処理するためにシフトを実行しています。処理の流れは少々難しいので、これはこういうものだと思っておいた方がよいかもしれません。興味がある方はgetoptsの動作をマニュアルで調べてみましょう。どのように処理が進むのかが分かるでしょう。
6. 関数やグルーピング
シェルスクリプトでは処理を関数としてまとめておくことができます。作成した関数は通常のコマンドのように使うことができます。処理の実態からすると、関数というよりはコマンドのサブルーチンと呼んだ方がよいかもしれません。
作成した関数の返り値は、return
で指定するか、または関数内で最後に実行されたコマンドの返り値がそのまま渡されます。リスト6.1が関数の例ですが、これは少々難しい例です。この関数は指定したディレクトリがひとつのディレクトリを持っていた場合には真値を、そうでない場合には偽値を返すもので、ls(1)
コマンドの出力結果を解析することでそれを実現しています。
target_has_topdir() { exec ls "$1" | { IFS= read directory IFS= read eol if [ -d "$1/$directory" -a -z "$eol" ] then return 0 fi return 1 } }
ls(1)
コマンドの出力結果がパイプで渡されてる先が{ }
で囲まれていますが、これは次の処理をグルーピング化するためです。こうすることで、そこにさらにシェルスクリプトを展開するように処理を記述することができます。{ }
の代わりに( )
を使えば、処理はサブシェルを生成してそちらのプロセスで実行されるようになります。この辺りは使い分けが必要ですが、よく分からなければ使わなくても問題ありません。
exec
を指定して処理を委譲してしまっているのも、グルーピング化した内部のreturn
の値を反映させたいからですが、確かにこの辺りの処理は難しいかもしれません。別にこの方法が正解ではないので、自分が理解しずらいようであれば、自分が分かりやすい方法で関数を作成し直してしまいましょう。
7. その他のテクニック
その他によく使うテクニックをいくつか紹介します。まずusageメッセージなど複数行にまたがるメッセージを使う場合ですが、リスト7.1のように改行を含めてそのままメッセージを変数に格納しておくとよいです。変数を展開してメッセージに含めておきたい場合は、シングルクォーテーションではなくダブルクォーテーションで囲っておきます。
usage_msg='usage: extract files... -h print help message -v print version'
このように複数行にまたがるメッセージや空白改行が意味を持っている変数を使う場合、リスト7.2のように、変数を使う場合にはダブルクォーテーションで囲むことを忘れないようにします。こうしないと、せっかく格納した改行や空白改行が無視されてしまうからです。
echo "${usage_msg}"
標準入力から値を持って来る場合、read
を使うことが多くなります。この場合はIFS環境変数の使い方を覚えておきましょう。IFSは引数の区切りとなる文字を指定するためのもので、例えばリスト7.3のような使い方をします。IFSはすべてに影響するので、大抵はリスト7.3のようにして、その行のコマンドに対してのみ設定して使います。リスト7.3の場合、設定がクリアされているので、区切りの文字列がなく、つまり一行まるまるが変数に格納されるという使い方をされていることになります。IFSは馴れないと使いにくいですが、これが使えるようになると文字列の扱いの幅がぐっと広がるので、覚えておきましょう。
IFS= read directory IFS= read eol
後はシェルスクリプトの特性上、一時ファイルや一時ディレクトリを作成して作業を行いたいことがままあります。こんな場合はリスト7.4のようにmktemp(1)
コマンドを使うようにするとよいでしょう。mktemp(1)
コマンドを使うと、重複することなくファイルやディレクトリを用意することができます。このコマンドの使い方も覚えておきましょう。
EXTRACTDIR=$(mktemp -d "$(basename $workdir/*)".XXXXXX) || return 1
8. まとめ
以上、アーカイブを展開するコマンドextract(1)
を例にあげながら、シェルスクリプトのテクニックについて説明しました。他人が作成したシェルスクリプトは読みにくいことが多いです。平易に書くように努めれば他人が読みやすいものも作成できますが、読みにくいものを書くこともできます。
他人の書いたシェルスクリプトを読んでみて、読めないと嘆くことはありません。多分それは誰が読んでも読みにくいし、書いた本人が読んでも頭をひねるものでしょう。分かるところからテクニックを参考にしていけばよく、それを広げていけばいいのです。馴れてくれば多くのシェルスクリプトが読めるようになるし、使えるテクニックも広がっていきます。
本稿が読者の参考になれば幸いです。