SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

シェルスクリプトの基本テクニックを盗め!

アーカイブの展開を題材にしたシェルスクリプト作成のコツ


  • X ポスト
  • このエントリーをはてなブックマークに追加

4. extract(1) - メインの処理 for, case

 シェルスクリプトの場合、最終的に実行される処理はファイルの後方に記述されていることが多くなります。このシェルスクリプトのメイン処理部分はリスト4.1です。ソースコードを読む場合、まずその処理の本質部分を探し当てて、何をしているか把握することが大切となります。枝葉末節は後から調べれば問題ありません。

 リスト4.1の処理は、for構文で引数に指定されたアーカイブに対して順次処理を適用するというものです。適応する処理は、拡張子を判定基準にしてcase構文で分岐させています。この例では、「tar+GNU zip」「tar+bzip2」「GNU zip」「ZIP」「LHa」をそれぞれ拡張子から判別して展開処理を行っています。

リスト4.1 extract(1) - 処理のメイン部分
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つのやり方がありますが、ここではその一つのやり方を紹介します。便利ですが忘れやすいので、これは一度どこかに書いたものをコピー&ペーストして使い回せばよいでしょう。

リスト5.1 シェルスクリプトへのオプションを処理する
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)コマンドの出力結果を解析することでそれを実現しています。

リスト6.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のように改行を含めてそのままメッセージを変数に格納しておくとよいです。変数を展開してメッセージに含めておきたい場合は、シングルクォーテーションではなくダブルクォーテーションで囲っておきます。

リスト7.1 改行を含んだテキストの保持
usage_msg='usage:
    extract files...
     -h             print help message
     -v             print version'

 このように複数行にまたがるメッセージや空白改行が意味を持っている変数を使う場合、リスト7.2のように、変数を使う場合にはダブルクォーテーションで囲むことを忘れないようにします。こうしないと、せっかく格納した改行や空白改行が無視されてしまうからです。

リスト7.2 改行空白を含んだテキストの保持
echo "${usage_msg}"

 標準入力から値を持って来る場合、readを使うことが多くなります。この場合はIFS環境変数の使い方を覚えておきましょう。IFSは引数の区切りとなる文字を指定するためのもので、例えばリスト7.3のような使い方をします。IFSはすべてに影響するので、大抵はリスト7.3のようにして、その行のコマンドに対してのみ設定して使います。リスト7.3の場合、設定がクリアされているので、区切りの文字列がなく、つまり一行まるまるが変数に格納されるという使い方をされていることになります。IFSは馴れないと使いにくいですが、これが使えるようになると文字列の扱いの幅がぐっと広がるので、覚えておきましょう。

リスト7.3 空白改行を含んだ入力を一括する方法
IFS= read directory
IFS= read eol

 後はシェルスクリプトの特性上、一時ファイルや一時ディレクトリを作成して作業を行いたいことがままあります。こんな場合はリスト7.4のようにmktemp(1)コマンドを使うようにするとよいでしょう。mktemp(1)コマンドを使うと、重複することなくファイルやディレクトリを用意することができます。このコマンドの使い方も覚えておきましょう。

リスト7.4 一時的なファイルやディレクトリの作成方法
EXTRACTDIR=$(mktemp -d "$(basename $workdir/*)".XXXXXX) || return 1

8. まとめ

 以上、アーカイブを展開するコマンドextract(1)を例にあげながら、シェルスクリプトのテクニックについて説明しました。他人が作成したシェルスクリプトは読みにくいことが多いです。平易に書くように努めれば他人が読みやすいものも作成できますが、読みにくいものを書くこともできます。

 他人の書いたシェルスクリプトを読んでみて、読めないと嘆くことはありません。多分それは誰が読んでも読みにくいし、書いた本人が読んでも頭をひねるものでしょう。分かるところからテクニックを参考にしていけばよく、それを広げていけばいいのです。馴れてくれば多くのシェルスクリプトが読めるようになるし、使えるテクニックも広がっていきます。

 本稿が読者の参考になれば幸いです。

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
特集記事連載記事一覧

もっと読む

この記事の著者

後藤 大地(ゴトウ ダイチ)

有限会社オングス代表取締役。FreeBSDやJavaに関する著作多数。http://www.ongs.co.jp/

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/387 2008/09/02 12:08

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング