0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Redmineのバージョンをrequireせずに読み取る

Posted at

とある処理を書いていて、Redmineのバージョンを知りたいな〜と思うタイミングがありました。

しかしRedmineのversion.rbはsubversionの処理への依存関係を持っており、なんかrequireしたくないなーという感じでした。

require 'redmine/scm/adapters/subversion_adapter'

そこでパースして読み取ってみることにしました。

Prism

まずはPrismを使ってみました。Ruby 3.3.0以上にはバンドルされていますが、それ以前のRubyでは自分でgem installする必要があります。

require 'prism'

class MyVisitor < Prism::Visitor
  def initialize
    @version_hash = {}
  end

  # node
  # @ ConstantWriteNode (location: (8,4)-(8,13))
  # ├── name: :MAJOR
  # ├── name_loc: (8,4)-(8,9) = "MAJOR"
  # ├── value:
  # │   @ IntegerNode (location: (8,12)-(8,13))
  # │   ├── flags: decimal
  # │   └── value: 5
  def visit_constant_write_node(node)
    super
    case node.name
    when :MAJOR then @version_hash[:major] = node.value.value
    when :MINOR then @version_hash[:minor] = node.value.value
    when :TINY then @version_hash[:tiny] = node.value.value
    end
  end

  def version
    "#{@version_hash[:major]}.#{@version_hash[:minor]}.#{@version_hash[:tiny]}"
  end
end

visitor = MyVisitor.new
Prism.parse_file('lib/redmine/version.rb').value.accept(visitor)
puts visitor.version

Prism::Visitorというクラスを継承して自分のVisitorを作ることで、自由に色々な処理を書けます。Prismのノードは色んな情報を持っているので、簡単にバージョンの情報を引っ張ってくることができました。

Ripper

古いバージョンのRubyでも動くコードを書かなければいけない場合、Ripperが使えます。RipperにはコードをパースしてS式として返す使い方か、Prismのようにイベントベースの使い方の二通りあります。
最初はイベントベースでやってみたのですが、Prismのようにノードが豊富な情報を持っているわけではないので自前で情報を頑張って取る必要があり、非常に難儀したのでS式を使うやり方を試しました。

require 'ripper'
exp = Ripper.sexp(File.read("lib/redmine/version.rb"))

# expの中身は下記のようになっている
# このうち、`[:assign, [:var_field, [:@const, "MAJOR", [8, 4]]], [:@int, "5", [8, 12]]]`となっているあたりの情報が欲しい
#
# [:program,
#  [[:command,
#   [:@ident, "require", [3, 0]],
#   [...],
#  [:module,
#   [:const_ref, [:@const, "Redmine", [5, 7]]],
#   [:bodystmt,
#    [[:void_stmt],
#     [:module,
#      [:const_ref, [:@const, "VERSION", [7, 9]]],
#      [:bodystmt,
#       [[:void_stmt],
#        [:assign, [:var_field, [:@const, "MAJOR", [8, 4]]], [:@int, "5", [8, 12]]],
#        [:assign, [:var_field, [:@const, "MINOR", [9, 4]]], [:@int, "1", [9, 12]]],
#        [:assign, [:var_field, [:@const, "TINY", [10, 4]]], [:@int, "2", [10, 12]]],
#        [...],
#        [:defs,
#         [:var_ref, [:@kw, "self", [19, 8]]],

def sexp_walk(exp, &block)
  return unless exp

  if exp.is_a?(Array) && exp[0] == :assign
    yield(exp)
  else
    exp.each do |child_exp|
      if child_exp.is_a?(Array)
        sexp_walk(child_exp, &block)
      end
    end
  end
end

version = {}
sexp_walk(exp) do |exp|
  next unless exp[1][1][0] == :@const
  next unless exp[2][0] == :@int

  const_name = exp[1][1][1]
  const_value = exp[2][1]
  version[const_name] = const_value
end

"#{version['MAJOR']}.#{version['MINOR']}.#{version['TINY']}"

うーん、読み辛い…w イベントベースで書きたくなりますね。しかしS式にしてくれるので、今回のようなバージョンの数字を取りだしたいだけならこれで十分な気もします。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?