ぎょーぼのぶろぐ

IT系の話を書いていくブログです。今はRubyの勉強中。

【ツール作成】Inifileモジュールを作る

前置き

設定をiniファイル形式のテキストファイルに書いておき、読み込むためのモジュールを作ります。

Inifile gem を使わなかった理由

最初は、gem にInifileというそのまんま、iniファイルの設定を読み込むための gem があったので、それを使おうと思ってました。

しかし、gem をインストールして、色々試してみたところ、一つの仕様で悩むことになりました。

  • 行の末尾が半角「¥」円マークで終わっている場合、次の行と結合される。

どういうことかというと・・・

以下のような iniファイルを用意して、

src_dir1=c:\hoge\hogehoge\
src_dir2=c:\nyao\nyaonyao\
src_dir3=

これを Inifile gem で読み込ませると、、、

src_dir1 の値 : c:\hoge\hogehogesrc_dir2=c:\nyao\nyaonyaosrc_dir3=

となってしまうのです。

【問題点】

  • 最後の文字が\の場合に、問答無用に次の行と結合されてしまう。
    せめて、前にスペースが一つでもついてるときだけ行結合( \)、とかにしてくれてたら良かったんですが・・・
  • 行結合に使われた\記号は、取り込んだ文字列からは無くなってしまう。
    このため、どこで行結合されたのか、結果の文字列から判断するのが非常に困難になっています。

ディレクトリパスの最後に\を付けなければいいじゃないか、とも思ったのですが、ディレクトリパスの書き方として、最後に区切り文字をつけるのは通常の、問題ない書き方なので、このiniファイル内だけダメ、というのは不自然です。区切り文字を/にしてしまえば、とも思いましたが、今回は「Windows環境」で使おうとしているので、ディレクトリパスで\が使えない、というのは、けっこう不便です。(エクスプローラー上でパスをコピーした後、置換しないといけない・・・)

悩んだ結果、「もう、自作しちゃおうか」ということになりました。完璧な、汎用的なものは難しいですが、このツールで普通に使えるレベルのものは作れるかなー、と思ったので。

実装する仕様

実装する仕様は、基本的にiniファイルの仕様に従います。

INIファイル - Wikipedia

細かいところとしては、以下の通り。

  • \ で行結合はしない。
    ここが今回の肝です。
  • ;記号の後は、問答無用でコメントと判断する。(引用符で括るとかエスケープとか考えない)
    値の後ろにコメント、とか書いてもいいように。
  • キー名、値の大文字小文字は区別する。
    区別しないようにも作れますが、今回は区別しないようにする理由がないから。
  • 引用符は括っても括らなくても可。(入っていた場合は問答無用で取り除く)
    今回のツールでは、シングル・ダブルクォーテーションは値に入ってこないので。
  • 空行は許す。
    どっちでもいいですが、どうせ読み飛ばすだけなので。
  • キー名の重複は、後勝ち。
    今回はそんなにたくさんの設定入れないので、簡単な方にします。設定が多ければ、ちゃんとエラーメッセージだして終了した方が親切なんですけどね。
  • 読み込むiniファイルのエンコードUTF-8 とする。
    Windows だと標準は S-JIS なのですが、UTF-8 の方が一般的かなー、ということで。

実装

実装は以下のようになりました。

module Inifile
    #
    # read
    #   引数で指定されたIniファイルパスの内容をハッシュ配列にして返す。
    #   引数 :iniファイルパス(絶対パス、相対パスどちらも可)
    #   戻り値:2次元ハッシュ
    #           セクション無しの項目は、key => value の1次元、
    #           セクション有りの項目は、section => { key => value, ...} の2次元で格納。
    #   その他:Iniファイルの文字コードは、utf-8 固定。
    #           key が重複した場合は、後の値で上書きする。
    #           Iniファイル内で`;`のコメント使用可。`;`以降の文字列は無視する。
    #   注意 :I18n のrequireとロケールファイル設定がされていない場合、
    #           例外メッセージが正しく表示されないので注意。
    #           
    def read(path) 

        # ローカル変数の初期化
        section = ""
        key = ""
        value = ""
        hash = Hash.new { |h,k| h[k] = {} }

        # ini ファイルを、読み取り専用でオープン
        File.open(path, "r:utf-8").each do |line| 
            # `;`記号がある場合はコメントと判断し、最初の`;`記号以降を除外する。
            line.sub!(/;.*/m,"") if line.match(/;/)            
            
            # 前後の空白を除去
            line.strip!

            # セクション行の場合は、セクションを設定してスキップ
            if line.match(/^\[.*\]$/) then
                # `[]` と空白を除去して、セクション名にする。
                section = line.gsub!(/[\[\]\s]/,"")
                next
            end

            # その行が`=`を含まなければ、スキップする。
            next unless line.match(/=/) 

            # 最初の`=`で2分割して、key, value に入れる。
            # `=`の前後に空白がある場合は、それも除去する。
            key, value =  line.split(/\s*=\s*/, 2)

            # シングルクォート、ダブルクォートは全て除外。
            value = value.gsub(/[\'\"]/,"")

            # key, value の値のどちらかが空の場合は、スキップする。
            next unless key.length > 0 && value.length > 0

            # セクションがある場合は、2次元ハッシュにする。
            if section.length == 0 then
                hash[key] = value
            else
                hash[section][key] = value
            end
        end

        # ハッシュ配列を返す。
        return hash

    rescue Errno::ENOENT => ex
        puts I18n.t(:err01, path: path)
        exit(false)
    rescue ArgumentError
        puts I18n.t(:err02, path: path)
        exit(false)
    rescue StandardError => ex
        puts I18n.t(:exception, method: __method__, class: ex.class, message: ex.message, backtrace: ex.backtrace.join("\n"))
        exit(false)
    end
end

Inifileモジュールですが、メソッドはreadのみです。後ででてくる Settingクラス(設定オブジェクト用のクラス)に Mix-in する予定なので、インスタンスメソッドとして定義しています。
iniファイルの形式は、セクション定義があると一段、入れ子の形になります。ですので、先に空の2次元ハッシュを生成しておき、1行ずつ読み込んで、キーと値の組み合わせが確定したら、ハッシュに格納していきます。

細かい処理の流れは、コード中のコメントを読んでください。

あと、例外についてですが、 2パターンについて制御しています。トラップしたら、該当のエラーメッセージを表示して、エラー終了させます。 - Errno::ENOENT : 「指定したiniファイルが見つからなかった場合」
- ArgumentError : 「iniファイルの文字コードUTF-8 以外の場合」  

想定外のエラー発生のために、念のため StandardError もトラップしてますが、たぶんココには来ないハズ。。。

I18n は、多言語対応モジュールで、今回は文言ファイルの集約化を目的に使用しています。I18n については、また別の記事で説明します。

今日はここまで・・・