とある処理を書いていて、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式にしてくれるので、今回のようなバージョンの数字を取りだしたいだけならこれで十分な気もします。