シェルスクリプト作成のテクニック Part 3

Hiroshi Tomioka
(2008年9月 9日 12:00) |

※このエントリーは、developerWorks : AIX and UNIXの「Speaking UNIX: More shell scripting techniques」を翻訳したエントリーのPart 3です。

【訳注】 「シェルスクリプト作成のテクニック Part 2」の続きのエントリーです。

文書化、文書化、文書化

自分のキャリアを振り返ってみた時、この問題の犠牲にならなかった人がいないはずがありません。もしかしたらあなたも、すでに退職された方が書いた10年前のスクリプトを調べてもらえないかとお願いされたことはありませんか?その時、「ええ、いいですよ」と快諾出来ましたか?問題ない場合もあるでしょう。しかし、そのスクリプトが複雑だったり、あなたにとって馴染みのないコマンドが使われていたり、あなたが慣れ親しんだものとはまったく異なるスタイルだったり、単純に動かなかったりした場合、作者がスクリプトを作成した時に何を意図していたかヒントが記述されていれば、本当に助かるものです。あるいは、貴方がスクリプトを作成し、それは一回限りのもので二度と使うことはないとします。その場合も文書化は必要ないと言い切れるでしょうか。また、数週間の用途で巨大なスクリプトを作成し、あなたはその動作のすべてを把握している場合でも、他の人がそれを見ることになった場合、その人は困惑してしまうかもしれません。ここではスクリプトの文書化がユーザーだけでなく開発者にとっても重要である理由をいくつかの例から紹介していきたいと思います。

Listing 6はあるコードに含まれる関数を表示しています。

Listing 6: コメントのないスクリプトの例

confirm_and_exit() {
  [[ ${_DEBUG_LEVEL} -ge 3 ]] && set -x
  while [[ -z ${_EXIT_ANS} ]]
  do
    cup_echo "Are you sure you want to exit? [Y/N]  
        \c" ${_PROMPT_ERR_ROW} ${_PROMPT_ERR_COL}
    ${_TPUT_CMD} cnorm
    read ${_NO_EOL_FLAG:+${_READ_FLAG:-'-n'}} ${_NO_EOL_FLAG} _EXIT_ANS
    ${_TPUT_CMD} civis
  done

  case ${_EXIT_ANS} in
    [Nn])  unset _EXIT_ANS; return 0;;
    [Yy])  exit_msg 0 1 "Exiting Script";;
       *)  invalid_selection ${_EXIT_ANS}; unset _EXIT_ANS;;
  esac
  return 0
}

あなたがスクリプトに精通されている方なら、これを読むことが出来ると思います。しかし、スクリプトの勉強中の方がこれを見た場合、この関数が何をしているのか分からないかもしれません。数分間の時間を確保してスクリプトにコメントを追加するだけで、状況は一変します。Listing 7ではコメントを付けた同じ関数を表示しています。

Listing 7: コメントの付いたスクリプトの例

#########################################
# 関数名 confirm_and_exit
#########################################
confirm_and_exit() {
  # デバッグレベルが 3 以上に設定されている場合は、評価されたすべての行が標準出力に出力されます
  [[ ${_DEBUG_LEVEL} -ge 3 ]] && set --x

  # ユーザーが正しい回答を入力されるまで、プロンプトを表示し続ける
  while [[ -z ${_EXIT_ANS} ]]
  do
    # スクリプトを抜けるかどうかユーザーにプロンプトを表示する
    # 関数 cup_echo は tpu cup   をコールします
    # 構文:
    # cup_echo <表示させる文字列> <表示させる行番号> <表示させる列番号>
    cup_echo "Are you sure you want to exit? [Y/N]  
        \c" ${_PROMPT_ERR_ROW} ${_PROMPT_ERR_COL}

    # tput を使ってカーソルを通常に戻す
    ${_TPUT_CMD} cnorm

    # ユーザーが入力した値を読み取る
    # 変数 _NO_EOL_FLAG が設定されている場合は、_READ_FLAG の値か、"-n"を使う
    # 変数 _NO_EOL_FLAG が設定されている場合は、read から読み込まれた値を文字列として用いる
    # ユーザーが入力した値を変数 _EXIT_ANS に設定する
    read ${_NO_EOL_FLAG:+${_READ_FLAG:-'-n'}} ${_NO_EOL_FLAG} _EXIT_ANS

    # tput を使ってカーソルを非表示にする
    ${_TPUT_CMD} civis
  done

  # ユーザーが"n"を入力した場合は、リターンコード 0 を返し、コードの一個前のブロックに戻る
  # ユーザーが"y"を入力した場合は、スクリプトを抜ける
  # ユーザーがそれ以外の値を入力した場合は、関数 invalid_selection を実行する 
  case ${_EXIT_ANS} in
    [Nn])  unset _EXIT_ANS; return 0;;
    [Yy])  exit_msg 0 1 "Exiting Script";;
       *)  invalid_selection ${_EXIT_ANS}; unset _EXIT_ANS;;
  esac

  # リターンコード 0 を返して関数を抜ける
  return 0
}

こんな小さな関数に対してやるには極めて退屈で過剰な作業かもしれませんが、コメントに記載されている内容は、初心者のシェルスクリプターや関数の内容を調べる人間にとって極めて有益な情報と言えます。

他にシェルスクリプトのコメントの有効活用の例としては、リターンコードと変数の説明が挙げられます。

Listing 8はあるシェルスクリプトの冒頭部分です。

Listing 8: 文書化されていない変数の例

#!/usr/bin/bash
trap 'exit_msg 1 0 "Signal Caught. Exiting..."' HUP INT QUIT KILL ABRT
trap 'window_size_changed' WINCH

_MSG_SLEEP_TIME=3
_RETNUM_SIZE=6
_DEBUG_LEVEL=0
_TMPDIR="/tmp"
_SP_LOG="${0##*/}.log"
_SP_REQUESTS="${HOME}/sp_requests"
_MENU_ITEMS=15
LESS="-P LINE\: %l"
export _SP_REQUESTS _TMPDIR _SP_LOG _DB_BACKUP_DIR 
    export _DEBUG_LEVEL _NEW_RMSYNC _RMTOTS_OFFSET_COL

繰り返しになりますが、これらの変数が何を目的として、どんな値が入るのかを理解するのは極めて困難です。スクリプトを全部読んでみない限り、これらの変数だけでは何も分かりません。それに加えて、このスクリプトで使われているリターンコードに関する言及はありません。こうなるとシェルスクリプトのトラブルシューティングは必要以上に難しいものとなります。Listing 8にリターンコードの意味を説明したセクションを追加して、紛らわしさをなくしましょう。Listing 9を見て下さい。

Listing 9: 文書化された変数の例

#!/usr/bin/bash
#########################################################################
# trap
#########################################################################
# ユーザーがスクリプトを抜ける際のtrap
trap 'exit_msg 1 0 "Signal Caught. Exiting..."' HUP INT QUIT KILL ABRT
trap 'window_size_changed' WINCH                # ユーザーがウインドウをリサイズした時のtrap
#########################################################################

#########################################################################
# 定義済み変数/export された変数
#########################################################################
_MSG_SLEEP_TIME=3             	       # すべてのメッセージで使われる sleep の秒数
                                      # (未定義の場合、デフォルトは 3 秒になる)
_CUSTNUM_SIZE=6                       # この場所での顧客番号の長さ
                                      # (未定義の場合、デフォルトは 6 になる)
_DEBUG_LEVEL=0                        # デバッグメッセージをログする。ログレベルは累積
                                      # (例: 1 は 1、2 は 1 と 2、3 は 1 と 2 と 3 )
                                      # (未定義の場合、デフォルトは 0 になる)
                                      # ログレベル:
                                      #  0 = メッセージなし
                                      #  1 = 簡単なメッセージ (スクリプトの開始やエラーなど)
                                      #  2 = 環境の設定 ( set や env )
                                      #  3 = set -x を実行 (しつこいくらいメッセージが出る)
_TMPDIR="/tmp"                        # 作業ファイルや一次ファイルを格納するディレクトリ
                                      # (未定義の場合、デフォルトは /tmp になる)
_SP_LOG="${0##*/}.log"                # スクリプトファイルのログ
_SP_REQUESTS="${HOME}/sp_requests"
			               # 顧客レコード要求のファイル
                                      # 開始時に読み込まれる
_MENU_ITEMS=15                        # ページ単位に表示するアイテムのデフォルト数
                                      # (未定義の場合、デフォルトは 10 になる)
LESS="-P LINE\: %l"                   # 'less'プロンプトをフォーマットする。詳しくは MAN less を参照


# 上記で定義した変数を export する
export _MSG_SLEEP_TIME _CUSTNUM_SIZE _DEBUG_LEVEL _TMPDIR 
    _SP_LOG _SP_REQUESTS _MENU_ITEMS
#########################################################################

分かりやすくなりましたか?すべての変数が組織化と詳細化されたため、初めてこのスクリプトを読む人でもこのプログラムが何であるか判断しやすくなったはずです。

【訳注】 「シェルスクリプト作成のテクニック Part 4」に続きます。