binding.pry
してますか?
普段何気なくbinding.pry
を使っているけど、その中身についてはブラックボックスな認識のまま使っていたので、一部のみですが仕組みをソースコードリーディングしながら追ってみました。
alias は @
です。
このコマンドは pry が現在実行しているコンテキストのソースコードを ruby 上に表示してくれます。
debug していると「今どの部分のコード(コンテキスト)を実行しているんだっけ?」となったときに「@
」と入力すれば、その付近のコードを出力してくれます。
pry-byebug と組み合わせてnext
コマンドを使ったりすると思いますが、いまどこにいるのかわからなくなったりするときに確認できて便利です。
pry> whereami
From: TestController#index:
8: def index
9: binding.pry
=> 10: end
alias は $
です。
このコマンドは pry の REPL 上で引数にクラスやメソッドを渡して実行するとその定義場所を教えてくれます。
例えば、あらゆる箇所で呼び出されるcurrent_user
メソッドがどこで定義されているのか(show-source
したときに呼ばれるcurrent_user
メソッドはどこのメソッドが呼ばれているのか)を見つけてみましょう。
pry> show-source current_user
From: /app/controllers/application_controller.rb:188:
Owner: ApplicationController
Visibility: public
Signature: current_user
Number of lines: 7
def current_user
return @_current_user if @_current_user
if current_user
@_current_user = User.find_by(id: user_id)
end
end
ちゃんと gem の内部のクラスやメソッドも定義場所を教えてくれます。
pry> show-source ActiveModel
From: /.asdf/installs/ruby/2.7.6/lib/ruby/gems/2.7.0/gems/activemodel-6.1.6.1/lib/active_model/gem_version.rb:2
Module name: ActiveModel
Number of monkeypatches: 3. Use the `-a` option to display all available monkeypatches
Number of lines: 15
module ActiveModel
# Returns the version of the currently loaded \Active \Model as a <tt>Gem::Version</tt>
def self.gem_version
Gem::Version.new VERSION::STRING
end
module VERSION
実は pry の REPL 上で cat コマンドが使えます。 別にエディタ開けば良いじゃんという感じですが、integration や production などのサーバー上で rails ruby を起動している場合にサクッと確認したいファイルの内容などがあるときにたまに使います。
pry> cat User.rb
class User < ApplicationRecord
has_many ...
end
紹介した 3 つの他にも shell command で使えるものや、便利なコマンドがあったりします。 https://github.com/pry/pry/tree/master/lib/pry/commands あたりを眺めて実行してみてください。
binding.pry
を仕込んでから ruby で REPL が立ち上がるまで本記事での対象の pry gem の version は 0.13.1
、Ruby は2.7.6
です。
Ruby の組み込みクラスに Binding
クラスというものがあります。
https://docs.ruby-lang.org/ja/latest/class/Binding.html
ローカル変数のテーブルと self、モジュールのネストなどの情報を保持するオブジェクトのクラスです。
説明の通りこのクラスから生成されるオブジェクトは、Ruby を実行している中で「そのある時点の変数やメソッドのコンテキストを含んだオブジェクト」です。
大体この時点でbinding.pry
の仕組みの察しがつきますね。
Kernal.#binding
を実行するとBinding
クラスのオブジェクトが返ります。
pry> binding
#<Binding:0x00007f3eed13cb70>
pry> binding.class
Binding < Object
Kernel
モジュールは Ruby のすべてのクラスから参照することができるメソッドを定義しているモジュールであり、Object クラスはこのモジュールを include しています。つまり、binding
オブジェクトは Ruby 上のほぼすべての箇所から参照可能です。
https://docs.ruby-lang.org/ja/2.4.0/class/Kernel.html
binding.pry
で止まった箇所の時点でのローカル変数やスコープが参照できるのはこのBinding
クラスのおかげのようです。
binding
オブジェクトのメソッドにsource_location
というものがあります。
これはbinding.source_location
を実行した箇所におけるself
のソースファイル名と行番号を返してくれるメソッドです。
https://docs.ruby-lang.org/ja/latest/class/Binding.html#I_SOURCE_LOCATION
irb(#<ApplicationController:0x00007fc1f4c51e40>):001:0> binding.source_location
=> ["/app/controllers/application_controller.rb", 1]
まさにソースコードの定義場所を割り当てるのにちょうど良さそうなメソッドですね。
前述したshow-source
メソッドなんかはこのbinding.source_location
を拡張していたりするのでしょうか?
show-source binding.pry
をしてみるメソッドなどの定義場所を探し出してくれるshow-source
メソッドですが、binding.pry
の仕組みを追いたいのであればshow-source
してみればいいじゃないかということで実行してみました。
pry> show-source binding.pry
From: /.asdf/installs/ruby/2.7.6/lib/ruby/gems/2.7.0/gems/pry-0.13.1/lib/pry/core_extensions.rb:25:
Owner: Object
Visibility: public
Signature: pry(object=?, hash=?)
Number of lines: 7
def pry(object = nil, hash = {})
if object.nil? || Hash === object # rubocop:disable Style/CaseEquality
Pry.start(self, object || {})
else
Pry.start(object, hash)
end
end
実行してみた結果、binding.pry
メソッドの実体は pry gem の中のlib/pry/core_extensions.rbにあるようです。
定義箇所のクラスを見てみると、Object
クラスのメソッドとして定義されていますね。
Ruby においてObject
クラスはすべてのクラスの継承元になりますから、このpry
メソッドはすべてのクラスで実行することが可能になります。
つまり、Object
クラスを継承したBinding
クラスのオブジェクトであるbinding
オブジェクトもpry
メソッドを実行することができるため、binding.pry
という記述が可能になります。
(binding
だけでなくても1.pry
や'hoge'.pry
なども実行できるということになりますね。)
# lib/pry/core_extensions.rb
class Object
# Start a Pry REPL on self.
#
# If `self` is a Binding then that will be used to evaluate expressions;
# otherwise a new binding will be created.
#
# @param [Object] object the object or binding to pry
# (__deprecated__, use `object.pry`)
# @param [Hash] hash the options hash
# @example With a binding
# binding.pry
# @example On any object
# "dummy".pry
# @example With options
# def my_method
# binding.pry :quiet => true
# end
# my_method()
# @see Pry.start
def pry(object = nil, hash = {})
if object.nil? || Hash === object # rubocop:disable Style/CaseEquality
Pry.start(self, object || {})
else
Pry.start(object, hash)
end
end
pry
メソッドは内部でPry.start
というクラスメソッドを実行しており、これによってlib/pry/pry_class.rb の self.start メソッドが呼び出されます。
# lib/pry/pry_class.rb
# Start a Pry REPL.
# This method also loads `pryrc` as necessary the first time it is invoked.
# @param [Object, Binding] target The receiver of the Pry session
# @param [Hash] options
# @option options (see Pry#initialize)
# @example
# Pry.start(Object.new, :input => MyInput.new)
def self.start(target = nil, options = {})
return if Pry::Env['DISABLE_PRY']
if Pry::Env['FAIL_PRY']
raise 'You have FAIL_PRY set to true, which results in Pry calls failing'
end
options = options.to_hash
if in_critical_section?
output.puts "ERROR: Pry started inside Pry."
output.puts "This can happen if you have a binding.pry inside a #to_s " \
"or #inspect function."
return
end
unless mutex_available?
output.puts "ERROR: Unable to obtain mutex lock."
output.puts "This can happen if binding.pry is called from a signal handler"
return
end
options[:target] = Pry.binding_for(target || toplevel_binding)
initial_session_setup
final_session_setup
# Unless we were given a backtrace, save the current one
if options[:backtrace].nil?
options[:backtrace] = caller
# If Pry was started via `binding.pry`, elide that from the backtrace
if options[:backtrace].first =~ /pry.*core_extensions.*pry/
options[:backtrace].shift
end
end
driver = options[:driver] || Pry::REPL
# Enter the matrix
driver.start(options)
rescue Pry::TooSafeException
puts "ERROR: Pry cannot work with $SAFE > 0"
raise
end
最後のdriver.start(options)
で、また何かを start していますが、driver にはPry::REPL
クラスが入ってくるので、最終的には pry の REPL が起動され、いつも見ているbinding.pry
の画面になります。
# pryのREPL
pry>
REPL の start メソッドはlib/pry/repl.rbにあります。
ここで、prologue
メソッドが実行されているのですが、その中身を見てみるとpry.exec_hook
メソッドが実行されているのが分かります。
# lib/pry/repl.rb
# Start the read-eval-print loop.
# @return [Object?] If the session throws `:breakout`, return the value
# thrown with it.
# @raise [Exception] If the session throws `:raise_up`, raise the exception
# thrown with it.
def start
prologue
Pry::InputLock.for(:all).with_ownership { repl }
ensure
epilogue
end
private
# Set up the repl session.
# @return [void]
def prologue
pry.exec_hook :before_session, pry.output, pry.current_binding, pry
return unless pry.config.correct_indent
# Clear the line before starting Pry. This fixes issue #566.
output.print(Helpers::Platform.windows_ansi? ? "\e[0F" : "\e[0G")
end
引数にpry.current_binding
という値が渡されていて、どうやらここらへんで現在の binding オブジェクトが渡されているようですね。
pry.exec_hook
メソッドはlib/pry/hooks.rbに実装があります。
少々難しいですが、前述のprologue
メソッドで:before_session
という Symbol が渡されているので、@hooks[:before_session.to_s]
が map されます。
# lib/pry/hooks.rb
# Execute the list of hooks for the `event_name` event.
# @param [Symbol] event_name The name of the event.
# @param [Array] args The arguments to pass to each hook function.
# @return [Object] The return value of the last executed hook.
def exec_hook(event_name, *args, &block)
@hooks[event_name.to_s].map do |_hook_name, callable|
begin
callable.call(*args, &block)
rescue RescuableException => e
errors << e
e
end
end.last
end
この@hooks
インスタンス変数にどんな値が入っているのかというと、pry が initialize されるときにPry::Hooks.default
というメソッドがすでに実行されており、add_hook メソッドを通じて@hooks
インスタンス変数に対して:before_session
という key とその value(配列)が格納されています。
irb(#<Pry::Hooks:0x0000565023aaddd0>):001:0> @hooks
{
"before_session" => [
[
:default,
#<Proc:0x0000565023aadc18 /.asdf/installs/ruby/2.7.6/lib/ruby/gems/2.7.0/gems/pry-0.13.1/lib/pry/hooks.rb:18>
]
]
}
exec_hook
メソッドではevent_name
が:before_session
で渡ってきますから、callable.call(*args, &block)
はlib/pry/hooks.rb:18の Proc が実行されます。
# lib/pry/hooks.rb
def self.default
hooks = new
hooks.add_hook(:before_session, :default) do |_out, _target, pry_instance|
next if pry_instance.quiet?
pry_instance.run_command('whereami --quiet')
end
hooks
end
ここで、pry_instance.run_command('whereami --quiet')
が実行されている通り、前述したwhereami
メソッドが実行され、binding.pry
を仕込んだ箇所のソースコードが表示されるのです。
そして REPL の input が表示されます。
# binding.pryを仕込んだ箇所で止まったときの表示
From: /app/controllers/test_controller.rb:10 TestController#index:
From: TestController#index:
8: def index
9: binding.pry
=> 10: end
pry(#<TestController>)[2.7.6]>
ここまでで、binding.pry
を仕込んだ箇所の Binding クラスのオブジェクトに対して pry の REPL 環境を立ち上げているため、ローカル変数などその時点でのコンテキストを参照できるというのがbinding.pry
メソッドの仕組みであることがわかりました。
binding.pry
を仕込んだ箇所で REPL を起動させるロジックの流れを読んでみましたがshow-souce
メソッドを実行したとき、巨大なコードベースなアプリケーションでもそこまで load 時間は長くありません。
つまり、すべてのファイルを事前に load しておいて、すべての定義箇所を記憶するなどという重たい処理はやっていないと考えられます。
前述で、「pry におけるshow-source
メソッドは、Binding クラスのオブジェクトが持っているbinding.source_location
を拡張しているのではないか」という推測をしましたが、show-source current_company
などを実行してもcurrent_company
メソッドに対してbinding
メソッドを仕込んでいるわけではないため、binding.source_location
が使えないはずで、拡張しているわけではなさそうです。(「binding.source_location
を使っている」 == 「current_company
メソッド内部にbinding.source_location
が実行されていないと見つけられない」)
Q. show-source
メソッドは巨大なコードベースであっても実行するとすぐに該当定義箇所を見つけて返してくれますが、どのように実現しているのでしょうか?
binding.pry
の仕組みがわかったので、次はshow-source
メソッドの仕組みを追ってみましょう。
show-source
メソッドの実装を追うshow-source show-source
を実行してみましょう。(なんかメタいですね)
ちゃんと結果が返ってきます。
※ pry-doc が Gemfile にある状態で「show-source show-source」を実行すると、今回の意図とは外れる pry-doc 内部のshow-source
メソッドが参照されます
pry> show-source show-source
From: /.asdf/installs/ruby/2.7.6/lib/ruby/gems/2.7.0/gems/pry-0.13.1/lib/pry/commands/show_source.rb
Number of lines: 107
class ShowSource < Command::ShowInfo
include Pry::Helpers::DocumentationHelpers
match 'show-source'
group 'Introspection'
description 'Show the source for a method or class.'
banner <<-'BANNER'
Usage: show-source [OPTIONS] [METH|CLASS]
Aliases: $, show-method
Show the source for a method or class. Tries instance methods first and then
methods by default.
これでshow-source
メソッドがは pry の中の lib/pry/commands/show_source.rb に実装があることがわかりました。
113 行目あたりを見てみると、show-source
という文字列が pry のコマンドとして add されているのがわかりますね。
# lib/pry/commands/show_source.rb
Pry::Commands.add_command(Pry::Command::ShowSource)
Pry::Commands.alias_command 'show-method', 'show-source'
Pry::Commands.alias_command '$', 'show-source'
Pry::Commands.alias_command '?', 'show-source -d'
ShowSource
の中身はoptions
メソッドやprocess
メソッドが定義されていますが、特別な処理は特にしていないのと、process
メソッド内部でsuper
が実行されているので、親クラスを見に行くと何かがわかるようです。
# lib/pry/commands/show_source.rb
def options(opt)
opt.on :e, :eval, "evaluate the command's argument as a ruby " \
"expression and show the class its return value"
opt.on :d, :doc, 'include documentation in the output'
super(opt)
end
def process
if opts.present?(:e)
obj = target.eval(args.first)
self.args = Array.new(1) { obj.is_a?(Module) ? obj.name : obj.class.name }
end
super
end
ShowSource
というクラスは継承関係を追っていくと ShowSource
< ShowInfo
< ClassCommand
< Command
のようになっています。
1 つ親のShowInfo
クラスのprocess
メソッドを見てみましょう。
# lib/pry/commands/show_info.rb
def process
code_object = Pry::CodeObject.lookup(obj_name, pry_instance, super: opts[:super])
raise CommandError, no_definition_message unless code_object
@original_code_object = code_object
if !obj_name && code_object.c_module? && !opts[:all]
result = "You're inside an object, whose class is defined by means of " \
"the C Ruby API.\nPry cannot display the information for this " \
"class."
if code_object.candidates.any?
result += "\nHowever, you can view monkey-patches applied to this " \
"class.\n.Just execute the same command with the '--all' " \
"switch."
end
elsif show_all_modules?(code_object)
# show all monkey patches for a module
result = content_and_headers_for_all_module_candidates(code_object)
else
# show a specific code object
co = code_object_with_accessible_source(code_object)
result = content_and_header_for_code_object(co)
end
set_file_and_dir_locals(code_object.source_file)
pry_instance.pager.page result
end
Pry::CodeObject.lookup
あたりのメソッドがなにやらそれっぽいですね。
code から参照したいメソッドの定義場所を lookup してくれているように見えます。CodeObject.lookup
の中身を見てみましょう。
https://github.com/pry/pry/lib/pry/code_object.rb#L69-L74
# lib/pry/code_object.rb
class CodeObject
....
class << self
def lookup(str, pry_instance, options = {})
co = new(str, pry_instance, options)
co.default_lookup || co.method_or_class_lookup ||
co.command_lookup || co.empty_lookup
end
end
end
end
co.method_or_class_lookup
がそれっぽいメソッドな予感がします。さらに見てみましょう。
https://github.com/pry/pry/lib/pry/code_object.rb#L136-L148
# lib/pry/code_object.rb
def method_or_class_lookup
obj =
case str
when /\S+\(\)\z/
Pry::Method.from_str(str.sub(/\(\)\z/, ''), target) ||
Pry::WrappedModule.from_str(str, target)
else
Pry::WrappedModule.from_str(str, target) ||
Pry::Method.from_str(str, target)
end
lookup_super(obj, super_level)
end
Pry::Method.from_str
メソッドを実行しており、さらにその中身を見てみるとfrom_obj
メソッド内でPry::Method#lookup_method_via_binding
メソッドを実行しています。どうやらここらへんがshow-source
メソッドにおけるコアロジックのようです。
https://github.com/pry/pry/lib/pry/method.rb#L107-L126
# lib/pry/method.rb
# In order to support 2.0 Refinements we need to look up methods
# inside the relevant Binding.
# @param [Object] obj The owner/receiver of the method.
# @param [Symbol] method_name The name of the method.
# @param [Symbol] method_type The type of method: :method or :instance_method
# @param [Binding] target The binding where the method is looked up.
# @return [Method, UnboundMethod] The 'refined' method object.
def lookup_method_via_binding(
obj, method_name, method_type, target = TOPLEVEL_BINDING
)
Pry.current[:obj] = obj
Pry.current[:name] = method_name
receiver = obj.is_a?(Module) ? "Module" : "Kernel"
target.eval(
"::#{receiver}.instance_method(:#{method_type})" \
".bind(Pry.current[:obj]).call(Pry.current[:name])"
)
ensure
Pry.current[:obj] = Pry.current[:name] = nil
end
パッと見何をやっているのかわからないですね。
例えばとある Rails controller に仕込んだbinding.pry
上でshow-source current_company
を実行したとき、Pry.current
には以下のような値が入ってきます。
> Pry.current
{
obj: #<Api::Private::HogeController:0x0000001ae6630>,
name: "current_company",
}
obj
はbinding.pry
を実行しているself
が、name にはshow-source
の引数(定義場所を探したいメソッド名)が入ってきます。
receiver
にはKernel
が、method_type
には:method
という Symbol が入ってくるため::#{receiver}.instance_method(:#{method_type}).bind(Pry.current[:obj]).call(Pry.current[:name])
は以下のようになります。
# lib/pry/method.rb
> ::Kernel.instance_method(:method).bind(self).call("current_company")
#current_company(?)
> ::Kernel.instance_method(:method).bind(self).call("current_company").class
Method < Object
Kernel
(Object
)の instance_method であるmethod
メソッドをself
で bind し、引数として”current_company”という文字列を渡して実行しています。
https://docs.ruby-lang.org/ja/latest/method/Object/i/method.html
最終的には Ruby の組み込みクラスであるMethod
クラスのインスタンスが生成されます。
https://docs.ruby-lang.org/ja/latest/class/Method.html
そして、そのMethod
クラスのインスタンスをPry::Method
クラスの引数として渡し、Pry::Method
クラスのインスタンスを生成しています。
https://github.com/pry/pry/lib/pry/method.rb#L152
# lib/pry/method.rb
def from_obj(obj, name, target = TOPLEVEL_BINDING)
new(lookup_method_via_binding(obj, name, :method, target))
rescue StandardError
nil
end
Ruby の組み込みクラスであるMethod
クラスにはsource_location
というメソッドが存在しており、ソースコードのファイル名と行番号を配列で返します。
https://docs.ruby-lang.org/ja/latest/class/Method.html#I_SOURCE_LOCATION
試しに pry 上で Method クラスに対してsource_location
を実行してみましょう。
pry(#<ApplicationController>)[2.7.6]> method('current_user).source_location
[
[0] "/app/controllers/application_controller.rb",
[1] 188
]
ここまでくればもうおわかりですね。
pry のshow-source
メソッドは、Ruby の組み込みクラスMethod
クラスのインスタンスメソッドであるMethod#source_location
メソッドを利用してメソッドの定義場所を割り出しているのです。
上述したShowSource
クラスのprocess
メソッドに戻ると、内部でcode_object.source_file
を実行しており、その内部でMethod.source_loation
を呼び出しています。
# lib/pry/method.rb
# @return [String, nil] The name of the file the method is defined in, or
# `nil` if the filename is unavailable.
def source_file
if source_location.nil?
if source_type == :c
info = pry_doc_info
info.file if info
end
else
source_location.first
end
end
あとは、割り出した定義箇所の情報を用いて file から該当コード周辺を取り出し、整形して ruby に出力しているという流れになります。
binding.pry
のbinding
は Ruby の組み込みBinding
クラスのインスタンスのこと
Object
クラスを拡張しているのでBinding
クラスのオブジェクトであるbinding
オブジェクトに対して pry メソッドが使えるshow-source
メソッドは Ruby の Method.source_location
を活用したもので、Method#source_location
+ Binding
インスタンスの組み合わせで実現されている。何も知らずに binding.pry を使っていたときは pry gem の中でゴリゴリの独自実装がされていると思っていましたが、ソースコードを読んでみると思った以上に Ruby の力を最大限に使っていました。
Ruby は奥が深いですね。
また、binding.irb
という pry を Ruby 本体に逆輸入する動きもあるようで、今後はgem install pry
をしなくても pry 相当の環境がビルトインで手に入る未来がすぐそこまで来ています。(ruby 2.7 などではbinding.irb
がすでに使えます。)
https://k0kubun.hatenablog.com/entry/2021/04/02/211455
使ってみると補完の UI がリッチですね。 まだ挙動が安定しない箇所がありそうでしたが、今後に期待できそうです。