【ツール作成】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ファイルのエンコードは 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
については、また別の記事で説明します。
今日はここまで・・・