Ruby との対話 : pry を使い pry をデバッグし pry のバグを直す話

1e68801494505bd0e0ce67e2e1398af3?s=47 zaru
November 29, 2018

Ruby との対話 : pry を使い pry をデバッグし pry のバグを直す話

1e68801494505bd0e0ce67e2e1398af3?s=128

zaru

November 29, 2018
Tweet

Transcript

  1. Ruby との対話 pry を使い pry をデバッグし pry のバグを直す話 2018.11.29 スタートアップテック

    vol.1 @zaru
  2. @zaru 株式会社ベーシック CTO

  3. None
  4. https://form.run

  5. Ruby との対話

  6. REPL Read Eval Print Loop

  7. Ruby の REPL といえば pry 今日は pry の edit について話します

  8. pry の edit を使っている人

  9. edit の使い方 $ pry [1] pry(main)> 100 * 10 =>

    1000 [2] pry(main)> edit edit と打つと直前に入力した内容をエディタで開いて編集できる。エ ディタの指定は $EDITOR もしくは Pry.config.editor で指定可能。
  10. edit が便利だから紹介したい

  11. コードを修正したい時 たとえば pry でコードをいくつか書きながらトライアンドエラーをする 場合、毎回似たようなコードを入力するのがダルい。コードをミスった ときに修正するのもダルい。矩形選択もだるい。そもそもキーボードか ら手を離したくない。 例えば amend-line を使う

    入力中のものであれば amend-line n..m で指定行数を編集可能。 [6] pry(main)> class Hoge [6] pry(main)* def piyo [6] pry(main)* p 'piyo' [6] pry(main)* end [6] pry(main)* amend-line 2..4
  12. 例えば ! を使う ! で入力バッファをクリアできる。すべてを無にしたい性格の人向け。 [10] pry(main)> class Mu [10]

    pry(main)* def kyomu [10] pry(main)* ! Input buffer cleared! 入力中のものを修正したい場合は amend-line か ! を使う。
  13. 例えば edit --in を使う edit --in は過去に入力したものを指定して編集可能。 [2] pry(main)> class

    RubyLT [2] pry(main)* def start [2] pry(main)* end [2] pry(main)* end => :start [3] pry(main)> edit --in 2 これで class RubyLT 自体の編集が可能。edit で編集したものはファイ ルに保存され、その場でリロードされる。便利。
  14. edit のオプション いくつかあるが、これだけ覚えておけば大丈夫。 edit : 直前に INPUT したものを編集 edit sample.rb

    : 指定ファイルを編集 edit MyClass#my_method : 指定クラスの指定メソッドを編集 edit --temp : 新規に入力 edit --in n : 指定番目に INPUT したものを編集
  15. ここまでが pry edit の紹介

  16. ここから pry のバグを見つけて pry を使い pry をデバッグし pry のバグを直す話

  17. 発端 何回もコードを実行して、ふと前に定義したメソッドを編集したくなっ た時。 [2] pry(main)> class RubyLT [2] pry(main)* def

    start [2] pry(main)* end [2] pry(main)* end . . . [113] pry(main)> [114] pry(main)> edit --in 2 2 番目を指定するも vim は空っぽ…
  18. そこで edit --in オプションの仕様を知るためにコードを確認する [2] pry(main)> edit edit def input_expression

    case opts[:i] when Range (_pry_.input_ring[opts[:i]] || []).join when Integer _pry_.input_ring[opts[:i]] || "" else raise Pry::CommandError, "Not a valid range: #{opts[:i]}" end end _pry_.input_ring というオブジェクトから取得しているっぽい
  19. Pry::Ring クラスの @buffer に配列で入力コマンドが格納されている。 @max_size という変数もあるので記録する最大値が決まっていて、その 値を超えたから、さっきの edit --in 2

    を指定しても空になったと推 測。 [5] pry(main)> _pry_.input_ring => #<Pry::Ring:0x007feb2e07ba68 @buffer= [nil, "class Hoge\n def piyo\n end\nend\n", "Hoge.new.piyo\n", "_pry_.input_ring\n @count=6, @max_size=100, @mutex=#<Thread::Mutex:0x007feb2e07ba40>>
  20. Pry::Ring の実装を確認する [6] pry(main)> edit Pry::Ring スレッドセーフ 上限値の決まっている配列クラス 上限値を超えたら古いものから上書きしていく <<

    [] to_a clear などのメソッド
  21. 手軽にデバッグできるように @max_size を少なくする。 .pryrc で指定 Pry.config.memory_size = 3

  22. 上限が決まっていて消えるのは仕方がない。memory_size を上げれば 済む話。ただし上書きされていくと、入力する番号が判断しにくいので @buffer 配列にインデックス番号をつけて出力するコマンドを実装しよ うと挙動を確認。 [1] pry(main)> 1 =>

    1 [2] pry(main)> 1 => 1 [3] pry(main)> 1 => 1 [4] pry(main)> 1 => 1 [5] pry(main)> _pry_.input_ring.to_a => ["1\n"]
  23. 消えた?!

  24. [1] pry(main)> 1 => 1 [2] pry(main)> 1 => 1

    [3] pry(main)> 1 => 1 [4] pry(main)> 1 => 1 [5] pry(main)> _pry_.input_ring.to_a => ["1\n"] 1 を4 回入力しているので @max_size が 3 の場合 ["1\n", "1\n", "1\n"] が返ってきて欲しい。
  25. オブジェクの監視には watch _pry_.input_ring の変化を知りたいので watch コマンドを使う。 [1] pry(main)> watch _pry_.input_ring

    Watching _pry_.input_ring watch: _pry_.input_ring => #<Pry::Ring:0x007fde395ab468 @buffer=[nil, ""], @count=2, @max_size=3, @mutex=#<Thread::Mutex:0x007fde395ab440>> watch で指定したオブジェクトに変更があったら、それを自動で表示し てくれる便利なやつ。
  26. 次に Pry::Ring#to_a の実装を確認する。 last_part + (@buffer - last_part) があやしい (*)

    `$` コマンドは `show-source` のエイリアス。指定クラスやオブジェクトのソースが見れる [8] pry(main)> $ Pry::Ring#to_a From: /Users/hiro/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/pry-0.12.2/lib/pry/ri Owner: Pry::Ring Visibility: public Number of lines: 6 def to_a return @buffer.dup if count <= max_size last_part = @buffer.slice(count % max_size, @buffer.size) last_part + (@buffer - last_part) end
  27. やりたい事は入力した順番通りに @buffer を並び替えたいだけ。一番最 初の入力したところから、2 つの配列に分けて前後を入れ替えて結合す るという感じ。でも配列同士の引き算は同一の要素を全部除く仕様。 [1, 1, 1] -

    [1] => [] コードをその場で修正してデバッグしてみる。 [8] pry(main)> edit Pry::Ring#to_a last_part = @buffer.slice(count % max_size, @buffer.size) last_part.concat @buffer.slice(0, count % max_size) これで OK
  28. [16] pry(main)> 1 => 1 [17] pry(main)> 1 => 1

    [18] pry(main)> 1 => 1 [19] pry(main)> 1 => 1 [20] pry(main)> _pry_.input_ring.to_a => ["1\n", "1\n", "1\n"] めでたい
  29. あとはこの配列のインデックスを使えば edit --in に利用できるはず。 [1] pry(main)> 1 => 1 [2]

    pry(main)> 2 => 2 [3] pry(main)> 3 => 3 [4] pry(main)> 4 => 4 [5] pry(main)> edit --in 0 ここでは最初に入力した 1 は消えているので 0 番目のインデックス指 定は先頭の 2 になっているはず。
  30. 次に [] の実装を確認する。 [6] pry(main)> $ Pry::Ring#[] From: /Users/hiro/Sites/pry/lib/pry/ring.rb @

    line 55: Owner: Pry::Ring Visibility: public Number of lines: 11 def [](index) @mutex.synchronize do return @buffer[(count + index) % max_size] if index.is_a?(Integer) return @buffer[index] if count <= max_size # Swap parts of array when the array turns page and starts overwriting # from the beginning, then apply the range. last_part = @buffer.slice([index.end, max_size - 1].min, count % max_size) (last_part + (@buffer - last_part))[index] end end
  31. なぜか並び替えのロジックが to_a と違う。このロジックだと場合よっ てはずれてしまうので単純に先程直した to_a を呼んでインデックスア クセスするように修正する。 def [](index) @mutex.synchronize

    do return @buffer[(count + index) % max_size] if index.is_a?(Integer) return @buffer[index] if count <= max_size to_a[index] end end これで OK
  32. edit で修正したものはファイルに反映されているので、そのまま git add & git commit すれば プルリクの作成ができて作業完了。 もしファイルに修正内容を反映したくない場合は

    edit -p オプションを 使えばメモリ上だけの反映になる。
  33. というわけで、マージされたプルリクはこちら https://github.com/pry/pry/pull/1898

  34. まとめ デバッグには edit が便利 監視には watch が便利 $ cd ls

    辺りは普通に便利 edit --in をもっと活用するためには、もうひと工夫必要 もし時間があれば紹介
  35. おまけ

  36. edit --in に使う番号がわかりにくいという課題

  37. hist コマンドで表現できれば良い… ?

  38. pry は入力履歴をどうやって管理しているのか

  39. 入力内容の記録は hist と input_ring の2種類 hist コマンドは入力改行ごとに番号が入る。つまり edit --in には使え

    ない番号。 [124] pry(main)> class Hoge [124] pry(main)* def piyo [124] pry(main)* end [124] pry(main)* end => :piyo [125] pry(main)> hist 1: hist 2: class Hoge 3: def piyo 4: end 5: end
  40. _pry_.input_ring というオブジェクトは、1 回の入力が1 つの値になって いる。つまり複数行でも1 つ。 この @buffer を hist

    のように表示すれば OK [1] pry(main)> class Hoge [1] pry(main)* def piyo [1] pry(main)* end [1] pry(main)* end => :piyo [2] pry(main)> Hoge.new.piyo => nil [3] pry(main)> _pry_.input_ring => #<Pry::Ring:0x007feb2e07ba68 @buffer=[nil, "class Hoge\n def piyo\n end\nend\n", "Hoge.new.piyo\n", "_pry_.input @count=4, @max_size=100, @mutex=#<Thread::Mutex:0x007feb2e07ba40>>
  41. 雑に hist コマンドを編集する。 [1] pry(main)> edit hist Pry::Code が番号表示用のクラスなので、そこに input_ring

    配列をつっ こむだけ。 opt.on :i, :'input-numbers', "Show input numbers" @history = if opts.present?(:'input-numbers') Pry::Code.new(_pry_.input_ring.to_a.slice(1..-1), 0) else find_history end
  42. [16] pry(main)> def hoge [16] pry(main)* 'hoge' [16] pry(main)* end

    => :hoge [17] pry(main)> 100 * 10 => 1000 [18] pry(main)> hist -i 0: def hoge 'hoge' end 1: 100 * 10 [19] pry(main)> edit --in 0 幸せ
  43. exit