LoginSignup
4
5

More than 5 years have passed since last update.

Raccを使ったnginx設定ファイルの解析

Last updated at Posted at 2017-08-27

はじめに

RaccはRubyで書かれたパーサジェネレータです。
Raccの構文に沿って文法を書くと、パーサを生成することができます。
最近、nginx設定ファイルの設定内容(ディレクティブと値)をDB管理に移行するため、
設定値を抜き出す必要がありRaccを使ってパーサを書きました。
※設定が少なければ手動で抜き出せばよいのですが、管理しているサーバー数が多く設定ファイルが大量にあったのでパーサを書くことにしました。

Raccの使い方

本記事ではRaccの基本的な使い方は記載しません。下記記事が参考になります。
- http://qiita.com/lnznt/items/ecbc16cae8ab9c641391

書籍では、Raccの作者である青木峰郎さんの著書であるRubyを256倍使うための本 無道編がおすすめです。
解説が分かりやすいだけでなく、語り口が軽妙でC拡張3分クッキング(謎)などもあり読み物としても楽しいです。
残念ながら新品を手に入れるのは難しそうですが、中古ならAmazonでも手に入るようです。

nginx設定ファイルについて

nginx設定ファイルのサンプルとして下記を見てみます。
パーサを作成するためには、パースする対象の文の構造について理解する必要があります。
まずは、このファイルがどんな構造になっているか見ていきます。

sample_nginx.conf
http {
  index index.html index.htm index.php;
  access_log logs/access.log  main;

  server {
    listen 80;

    location ~ \.php$ {
      fastcgi_pass 127.0.0.1:1025;
    }
  }
}

基本的な構造は次のパターンAおよびパターンBです。

<パターンA>

ディレクティブ 設定値(省略可) セミコロン

# パターンA
error_log logs/error.log;

error_logがディレクティブ、logs/error.logが設定値、;がセミコロンです。

<パターンB>

ディレクティブ 設定値(省略可) { パターンAまたはパターンBの繰り返し }

# パターンB
http {
  # パターンA
  index index.html index.htm index.php;
  access_log logs/access.log  main;

  # パターンB
  server {
    # パターンA
    listen 80;

    # パターンB
    location ~ \.php$ {
      # パターンA
      fastcgi_pass 127.0.0.1:1025;
    }
  }
}

パターンBは{}に囲まれたブロックの中にパターンB自身が登場するため、再帰構造になっている点がポイントです。
この設定ファイルの中から、ディレクティブとその設定値およびブロックの中身を上手く抜き出すにはどうしたらよいでしょうか?
正規表現によるマッチングで頑張っても何とかなりそうですが、再帰構造があるので結構複雑なコードになってしまいそうです。
このような再帰などがあって複雑な構造の文を解析するのにRaccが役に立ちます。

スキャナを書く

パースする対象についてある程度理解できたら、次にスキャナを書きます。
スキャナとはパース対象の文字列を読み込んでトークン※の並びに変換するものです。
※ディレクティブ、設定値、セミコロン、"{"、"}"など。Raccでは「:KEY」のようにRubyのシンボルでトークンを表現する。
スキャナのコードは下記のようになっています。

conf_parse.y
  def parse(f) # fにはパース対象の文字列が入る
    @q = []
    key_found = nil
    f.each do |line|
      until line.empty? do
        unless key_found
          case line
          when /\A\s+/           # 空白を読み飛ばす
            ;
          when /\A#.*/           # コメントを読み飛ばす
            ;
          when /\A[^;\s{}]+/     # ディレクティブ
            @q.push [:KEY, $&]
            key_found = 1
          when /\A[{}]/          # "{"または"}"
            @q.push [$&, $&]
          else
            raise RuntimeError, "must not happen"
          end
        else
          # ディレクティブが見つかったあとは以下の処理
          case line
          when /\A\s+/
            ;
          when /\A#.*/
            ;
          when /\A[^;\s{}]+[^;{}]*[^;\s{}]/  # 設定値
            @q.push [:VALUE, $&]
          when /\A[^;\s{}]/                  # 設定値(1文字)
            @q.push [:VALUE, $&]
          when /\A;/                         # セミコロン
            @q.push [:EOS, :EOS]
            key_found = nil
          when /\A{/                         # "{"
            @q.push [$&, $&]
            key_found = nil
          else
            raise RuntimeError, "must not happen"
          end
        end

        # 次の1文字を取得
        line = $'
      end
      # 行末
      @q.push [:EOL, :EOL]
    end
    # スキャン終了
    @q.push [false, '$']

    # パース処理の開始
    do_parse
  end

  # パーサがトークンを取り出す時に使用する
  def next_token
    @q.shift
  end

パース対象の文字列を1行ずつ読み込んで、正規表現でマッチングし、@q[シンボル, マッチした値]という配列で追加しています。
key_foundを使ってディレクティブが見つかったかどうかでマッチング処理を変更しています。
これは、nginxの設定ではディレクティブと設定値を区切るための文字というものがなく、
最初に登場した文字列がディレクティブになるためです。
スキャナの処理が完了すると、@qにパース対象文字列から変換したトークンの並びが含まれることになります。
最後に、do_parseというメソッドを実行するとパース処理が開始します。

文法を書く

次に、Raccが生成するパーサの元となる文法を書きます。
前述した「ディレクティブ 設定値 セミコロン」のような規則をRaccが理解できるように書いていきます。
文法のコードは下記です。

conf_parse.y
class CFParser
rule

  file    : kv_list opteol

  kv_list : opteol KEY val EOS
          | opteol KEY val block
          | kv_list opteol KEY val EOS
          | kv_list opteol KEY val block

  block   : '{' kv_list opteol '}'

  val     : 
          | VALUE
          | val eols VALUE

  eols    : EOL
          | eols EOL

  opteol  :
          | eols

end

例えば、「ディレクティブ 設定値 セミコロン」の規則はopteol KEY val EOSと表現されています。opteolはディレクティブの前に空行がある場合を想定して入っています。
ディレクティブ 設定値 { パターンAまたはパターンBの繰り返し }」のような再帰構造は次のように表現しています。

  kv_list : opteol KEY val EOS
          | opteol KEY val block
          # 中略
  block   : '{' kv_list opteol '}'

blockは'{'と'}'で囲まれた中にkv_listが入っています。そして、kv_listにはblockが含まれています。
opteol KEY val EOSを前述のパターンA、opteol KEY val blockをパターンBと考えると分かりやすいかと思います。

アクションを書く

実は、文法ファイルを書いただけではパーサは完成しません。
今回の目的は、「ディレクティブと値を取り出す」ことなので、文法にマッチした時にその処理(アクション)を実行してあげる必要があります。
文法ファイルにアクション({}で囲まれた処理)を追加したものが下記です。

conf_parse.y
class CFParser
rule

  file    : kv_list opteol

  kv_list : opteol KEY val EOS
              {
                result = [ [ val[1], val[2], nil ] ]
              }
          | opteol KEY val block
              {
                result = [ [ val[1], val[2], val[3] ] ]
              }
          | kv_list opteol KEY val EOS
              {
                val[0].push [val[2], val[3], nil]
              }
          | kv_list opteol KEY val block
              {
                val[0].push [val[2], val[3], val[4]]
              }

  block   : '{' kv_list opteol '}'
              {
                result = val[1]
              }

  val     : 
          | VALUE
          | val eols VALUE
              {
                result = val[0] + "\n" + val[2]
              }

  eols    : EOL
          | eols EOL

  opteol  :
          | eols

end

例えば、下記アクションはKEYの値(val[1])、valの値(val[2])、nilの配列を作成して、resultという変数に代入しています。
ここで、「KEYの値」といっているのはスキャナで[シンボル, マッチした値]という配列を作った時の「マッチした値」に該当するものです。
resultがkv_listの値となり、最終的にfileのval[0]であるkv_listの値がfileの値になります。(fileのようにアクションが定義されていない場合はval[0]がその規則の値になる)
そして、fileの値がパーサの返却値、つまりdo_parseメソッドの返却値になります。

  file    : kv_list opteol

  kv_list : opteol KEY val EOS
              {
                result = [ [ val[1], val[2], nil ] ]
              }

パーサを実行する

スキャナとパーサ(の元となる文法)が完成したので、パーサを生成してnginx設定ファイルをパースしてみます。

# パーサを生成
bundle exec racc conf_parse.y -o parse.rb

# パースを実行
ruby exec.rb sample_nginx.conf
# パース結果が表示される
[["error_log", "logs/error.log", nil],
 ["http",
  nil,
  [["index", "index.html index.htm index.php", nil],
   ["access_log", "logs/access.log  main", nil],
   ["server",
    nil,
    [["listen", "80", nil],
     ["location", "~ \\.php$", [["fastcgi_pass", "127.0.0.1:1025", nil]]]]]]]]

exec.rbはファイルをオープンしてパーサに渡すことと、結果の表示だけをやっています。

exec.rb
require './parse.rb'
require 'pp'

parser = CFParser.new
begin
  File.open(ARGV[0]) do |f|
    conf_list = parser.parse(f)
    pp conf_list
  end
rescue Racc::ParseError => e
  $stderr.puts e
end

パース結果を見てみると、当初の目的である「ディレクティブと値を取り出す」が達成されていることがわかります。
ブロックがない場合は配列の3番目の要素にnilが入り、ブロックがある場合はブロック内の設定が入ります。

おわりに

Raccを使用することで比較的低コストでnginx設定ファイルの解析をすることができました。
RaccはYaccという有名なパーサジェネレータを元に作成されており、文法の書き方はほとんど同じです。
YaccはCRubyインタプリタでも使われているので、CRubyのソースコードに興味がある方は
Yacc入門としてRaccを使ってみるのもいいのではないかと思います。
なお、作成したnginx設定ファイルのパーサはgithubで公開していますので、よかったら使ってみてください。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5