Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,103 @@
# frozen_string_literal: true
class Pry
class Command
class AmendLine < Pry::ClassCommand
match(/amend-line(?: (-?\d+)(?:\.\.(-?\d+))?)?/)
group 'Editing'
description 'Amend a line of input in multi-line mode.'
command_options interpolate: false, listing: 'amend-line'
banner <<-'BANNER'
Amend a line of input in multi-line mode. `amend-line N`, where the N represents
line to replace. Can also specify a range of lines using `amend-line N..M`
syntax. Passing "!" as replacement content deletes the line(s) instead.
amend-line 1 puts 'new' # replace line 1
amend-line 1..4 ! # delete lines 1..4
amend-line 3 >puts 'bye' # insert before line 3
amend-line puts 'appended' # no line number modifies immediately preceding line
BANNER
def process
raise CommandError, "No input to amend." if eval_string.empty?
eval_string.replace(amend_input)
run "fix-indent"
run "show-input"
end
private
# @return [String] A new string with the amendments applied to it.
def amend_input
input_array = eval_string.each_line.to_a
if arg_string == "!"
delete_from_array(input_array, line_range)
elsif arg_string.start_with?(">")
insert_into_array(input_array, line_range)
else
replace_in_array(input_array, line_range)
end
input_array.join
end
def delete_from_array(array, range)
array.slice!(range)
end
def insert_into_array(array, range)
insert_slot = Array(range).first
array.insert(insert_slot, arg_string[1..-1] << "\n")
end
def replace_in_array(array, range)
array[range] = arg_string + "\n"
end
# @return [Fixnum] The number of lines currently in `eval_string` (the
# input buffer)
def line_count
eval_string.lines.count
end
# Returns the (one-indexed) start and end lines given by the user.
# The lines in this range will be affected by the `amend-line`.
# Returns `nil` if no lines were specified by the user.
# @return [Array<Fixnum>, nil]
def start_and_end_line_number
start_line_number, end_line_number = args
end_line_number ||= start_line_number.to_i
[start_line_number.to_i, end_line_number.to_i] if start_line_number
end
# Takes two numbers that are 1-indexed, and returns a range (or
# number) that is 0-indexed. 1-indexed means the first element is
# identified by 1 rather than by 0 (as is the case for Ruby arrays).
# @param [Fixnum] start_line_number One-indexed number.
# @param [Fixnum] end_line_number One-indexed number.
# @return [Range] The zero-indexed range.
def zero_indexed_range_from_one_indexed_numbers(start_line_number, end_line_number)
# FIXME: one_index_number is a horrible name for this method
one_index_number(start_line_number)..one_index_number(end_line_number)
end
# The lines (or line) that will be modified by the `amend-line`.
# @return [Range, Fixnum] The lines or line.
def line_range
start_line_number, end_line_number = start_and_end_line_number
if start_line_number
zero_indexed_range_from_one_indexed_numbers(start_line_number,
end_line_number)
else
line_count - 1
end
end
end
Pry::Commands.add_command(Pry::Command::AmendLine)
end
end
@@ -0,0 +1,24 @@
# frozen_string_literal: true
class Pry
class Command
class Bang < Pry::ClassCommand
match(/^\s*!\s*$/)
group 'Editing'
description 'Clear the input buffer.'
command_options use_prefix: false, listing: '!'
banner <<-'BANNER'
Clear the input buffer. Useful if the parsing process goes wrong and you get
stuck in the read loop.
BANNER
def process
output.puts 'Input buffer cleared!'
eval_string.replace('')
end
end
Pry::Commands.add_command(Pry::Command::Bang)
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class Pry
class Command
class BangPry < Pry::ClassCommand
match '!pry'
group 'Navigating Pry'
description 'Start a Pry session on current self.'
banner <<-'BANNER'
Start a Pry session on current self. Also works mid multi-line expression.
BANNER
def process
target.pry
end
end
Pry::Commands.add_command(Pry::Command::BangPry)
end
end
@@ -0,0 +1,70 @@
# frozen_string_literal: true
class Pry
class Command
class Cat < Pry::ClassCommand
match 'cat'
group 'Input and Output'
description "Show code from a file, Pry's input buffer, or the last exception."
banner <<-'BANNER'
Usage: cat FILE
cat --ex [STACK_INDEX]
cat --in [INPUT_INDEX_OR_RANGE]
`cat` is capable of showing part or all of a source file, the context of the
last exception, or an expression from Pry's input history.
`cat --ex` defaults to showing the lines surrounding the location of the last
exception. Invoking it more than once travels up the exception's backtrace, and
providing a number shows the context of the given index of the backtrace.
BANNER
def options(opt)
opt.on :ex, "Show the context of the last exception",
optional_argument: true, as: Integer
opt.on :i, :in, "Show one or more entries from Pry's expression history",
optional_argument: true, as: Range, default: -5..-1
opt.on :s, :start, "Starting line (defaults to the first line)",
optional_argument: true, as: Integer
opt.on :e, :end, "Ending line (defaults to the last line)",
optional_argument: true, as: Integer
opt.on :l, :'line-numbers', "Show line numbers"
opt.on :t, :type, "The file type for syntax highlighting " \
"(e.g., 'ruby' or 'python')",
argument: true, as: Symbol
end
def process
output =
if opts.present?(:ex)
ExceptionFormatter.new(
pry_instance.last_exception, pry_instance, opts
).format
elsif opts.present?(:in)
InputExpressionFormatter.new(pry_instance.input_ring, opts).format
else
FileFormatter.new(args.first, pry_instance, opts).format
end
pry_instance.pager.page output
end
def complete(search)
super | load_path_completions
end
def load_path_completions
$LOAD_PATH.flat_map do |path|
Dir[path + '/**/*'].map do |f|
next if File.directory?(f)
f.sub!(path + '/', '')
end
end
end
end
Pry::Commands.add_command(Pry::Command::Cat)
end
end
@@ -0,0 +1,32 @@
# frozen_string_literal: true
class Pry
class Command
class Cat
class AbstractFormatter
include Pry::Helpers::CommandHelpers
include Pry::Helpers::BaseHelpers
private
def decorate(content)
content.code_type = code_type
content.between(*between_lines)
.with_line_numbers(use_line_numbers?).highlighted
end
def code_type
opts[:type] || :ruby
end
def use_line_numbers?
opts.present?(:'line-numbers') || opts.present?(:ex)
end
def between_lines
[opts[:start] || 1, opts[:end] || -1]
end
end
end
end
end
@@ -0,0 +1,90 @@
# frozen_string_literal: true
class Pry
class Command
class Cat
class ExceptionFormatter < AbstractFormatter
attr_reader :ex
attr_reader :opts
attr_reader :pry_instance
include Pry::Helpers::Text
def initialize(exception, pry_instance, opts)
@ex = exception
@opts = opts
@pry_instance = pry_instance
end
def format
check_for_errors
set_file_and_dir_locals(
backtrace_file, pry_instance, pry_instance.current_context
)
code = decorate(
Pry::Code.from_file(backtrace_file)
.between(*start_and_end_line_for_code_window)
.with_marker(backtrace_line)
)
"#{header}#{code}"
end
private
def code_window_size
pry_instance.config.default_window_size || 5
end
def backtrace_level
@backtrace_level ||=
begin
bl =
if opts[:ex].nil?
ex.bt_index
else
ex.bt_index = absolute_index_number(opts[:ex], ex.backtrace.size)
end
increment_backtrace_level
bl
end
end
def increment_backtrace_level
ex.inc_bt_index
end
def backtrace_file
Array(ex.bt_source_location_for(backtrace_level)).first
end
def backtrace_line
Array(ex.bt_source_location_for(backtrace_level)).last
end
def check_for_errors
raise CommandError, "No exception found." unless ex
return if backtrace_file
raise CommandError, "The given backtrace level is out of bounds."
end
def start_and_end_line_for_code_window
start_line = backtrace_line - code_window_size
start_line = 1 if start_line < 1
[start_line, backtrace_line + code_window_size]
end
def header
unindent(
"#{bold 'Exception:'} #{ex.class}: #{ex.message}\n" \
"--\n" \
"#{bold('From:')} #{backtrace_file}:#{backtrace_line} @ " \
"#{bold("level: #{backtrace_level}")} of backtrace " \
"(of #{ex.backtrace.size - 1}).\n\n"
)
end
end
end
end
end
@@ -0,0 +1,77 @@
# frozen_string_literal: true
class Pry
class Command
class Cat
class FileFormatter < AbstractFormatter
attr_reader :file_with_embedded_line
attr_reader :opts
attr_reader :pry_instance
def initialize(file_with_embedded_line, pry_instance, opts)
unless file_with_embedded_line
raise CommandError, "Must provide a filename, --in, or --ex."
end
@file_with_embedded_line = file_with_embedded_line
@opts = opts
@pry_instance = pry_instance
@code_from_file = Pry::Code.from_file(file_name)
end
def format
set_file_and_dir_locals(file_name, pry_instance, pry_instance.current_context)
decorate(@code_from_file)
end
def file_and_line
file_name, line_num = file_with_embedded_line.split(%r{:(?!/|\\)})
[file_name, line_num ? line_num.to_i : nil]
end
private
def file_name
file_and_line.first
end
def line_number
file_and_line.last
end
def code_window_size
pry_instance.config.default_window_size || 7
end
def decorate(content)
if line_number
super(content.around(line_number, code_window_size))
else
super
end
end
def code_type
opts[:type] || detect_code_type_from_file(file_name)
end
def detect_code_type_from_file(file_name)
code_type = @code_from_file.code_type
if code_type == :unknown
name = File.basename(file_name).split('.', 2).first
case name
when "Rakefile", "Gemfile"
:ruby
else
:text
end
else
code_type
end
end
end
end
end
end
@@ -0,0 +1,48 @@
# frozen_string_literal: true
class Pry
class Command
class Cat
class InputExpressionFormatter < AbstractFormatter
attr_accessor :input_expressions
attr_accessor :opts
def initialize(input_expressions, opts)
@input_expressions = input_expressions
@opts = opts
end
def format
raise CommandError, "No input expressions!" if numbered_input_items.empty?
if numbered_input_items.length > 1
content = ''
numbered_input_items.each do |i, s|
content += "#{Helpers::Text.bold(i.to_s)}:\n"
content += decorate(Pry::Code(s).with_indentation(2)).to_s
end
content
else
decorate(Pry::Code(selected_input_items.first))
end
end
private
def selected_input_items
input_expressions[normalized_expression_range] || []
end
def numbered_input_items
@numbered_input_items ||= normalized_expression_range.zip(selected_input_items)
.reject { |_, s| s.nil? || s == "" }
end
def normalized_expression_range
absolute_index_range(opts[:i], input_expressions.count)
end
end
end
end
end
@@ -0,0 +1,48 @@
# frozen_string_literal: true
class Pry
class Command
class Cd < Pry::ClassCommand
match 'cd'
group 'Context'
description 'Move into a new context (object or scope).'
banner <<-'BANNER'
Usage: cd [OPTIONS] [--help]
Move into new context (object or scope). As in UNIX shells use `cd ..` to go
back, `cd /` to return to Pry top-level and `cd -` to toggle between last two
scopes. Complex syntax (e.g `cd ../@x/@y`) also supported.
cd @x
cd ..
cd /
cd -
https://github.com/pry/pry/wiki/State-navigation#wiki-Changing_scope
BANNER
command_options state: %i[old_stack]
def process
state.old_stack ||= []
if arg_string.strip == "-"
unless state.old_stack.empty?
pry_instance.binding_stack, state.old_stack =
state.old_stack, pry_instance.binding_stack
end
else
stack = ObjectPath.new(arg_string, pry_instance.binding_stack).resolve
if stack && stack != pry_instance.binding_stack
state.old_stack = pry_instance.binding_stack
pry_instance.binding_stack = stack
end
end
end
end
Pry::Commands.add_command(Pry::Command::Cd)
end
end
@@ -0,0 +1,34 @@
# frozen_string_literal: true
class Pry
class Command
class ChangeInspector < Pry::ClassCommand
match 'change-inspector'
group 'Input and Output'
description 'Change the current inspector proc.'
command_options argument_required: true
banner <<-BANNER
Usage: change-inspector NAME
Change the proc used to print return values. See list-inspectors for a list
of available procs and a short description of what each one does.
BANNER
def process(inspector)
unless inspector_map.key?(inspector)
raise Pry::CommandError, "'#{inspector}' isn't a known inspector!"
end
pry_instance.print = inspector_map[inspector][:value]
output.puts "Switched to the '#{inspector}' inspector!"
end
private
def inspector_map
Pry::Inspector::MAP
end
Pry::Commands.add_command(self)
end
end
end
@@ -0,0 +1,51 @@
# frozen_string_literal: true
class Pry
class Command
class ChangePrompt < Pry::ClassCommand
match 'change-prompt'
group 'Input and Output'
description 'Change the current prompt.'
command_options argument_required: true
banner <<-BANNER
Usage: change-prompt [OPTIONS] [NAME]
Change the current prompt.
BANNER
def options(opt)
opt.on(:l, :list, 'List the available prompts')
end
def process(prompt)
if opts.present?(:l)
list_prompts
else
change_prompt(prompt)
end
end
private
def list_prompts
prompts = Pry::Prompt.all.map do |name, prompt|
"#{bold(name)}#{red(' (selected)') if pry_instance.prompt == prompt}\n" +
prompt.description
end
output.puts(prompts.join("\n" * 2))
end
def change_prompt(prompt)
if Pry::Prompt[prompt]
pry_instance.prompt = Pry::Prompt[prompt]
else
raise Pry::CommandError,
"'#{prompt}' isn't a known prompt. Run `change-prompt --list` " \
"to see the list of known prompts."
end
end
Pry::Commands.add_command(self)
end
end
end
@@ -0,0 +1,20 @@
# frozen_string_literal: true
class Pry
class Command
class ClearScreen < Pry::ClassCommand
match 'clear-screen'
group 'Input and Output'
description 'Clear the contents of the screen/window Pry is running in.'
def process
if Pry::Helpers::Platform.windows?
pry_instance.config.system.call(pry_instance.output, 'cls', pry_instance)
else
pry_instance.config.system.call(pry_instance.output, 'clear', pry_instance)
end
end
Pry::Commands.add_command(self)
end
end
end
@@ -0,0 +1,182 @@
# frozen_string_literal: true
class Pry
class Command
class CodeCollector
include Helpers::CommandHelpers
attr_reader :args
attr_reader :opts
attr_reader :pry_instance
# The name of the explicitly given file (if any).
attr_accessor :file
class << self
attr_accessor :input_expression_ranges
attr_accessor :output_result_ranges
end
@input_expression_ranges = []
@output_result_ranges = []
def initialize(args, opts, pry_instance)
@args = args
@opts = opts
@pry_instance = pry_instance
end
# Add the `--lines`, `-o`, `-i`, `-s`, `-d` options.
def self.inject_options(opt)
@input_expression_ranges = []
@output_result_ranges = []
opt.on :l, :lines, "Restrict to a subset of lines. Takes a line number " \
"or range",
optional_argument: true, as: Range, default: 1..-1
opt.on :o, :out, "Select lines from Pry's output result history. " \
"Takes an index or range",
optional_argument: true, as: Range, default: -5..-1 do |r|
output_result_ranges << (r || (-5..-1))
end
opt.on :i, :in, "Select lines from Pry's input expression history. " \
"Takes an index or range",
optional_argument: true, as: Range, default: -5..-1 do |r|
input_expression_ranges << (r || (-5..-1))
end
opt.on :s, :super, "Select the 'super' method. Can be repeated to " \
"traverse the ancestors",
as: :count
opt.on :d, :doc, "Select lines from the code object's documentation"
end
# The content (i.e code/docs) for the selected object.
# If the user provided a bare code object, it returns the source.
# If the user provided the `-i` or `-o` switches, it returns the
# selected input/output lines joined as a string. If the user used
# `-d CODE_OBJECT` it returns the docs for that code object.
#
# @return [String]
def content
@content ||=
begin
if bad_option_combination?
raise CommandError,
"Only one of --out, --in, --doc and CODE_OBJECT may " \
"be specified."
end
content = if opts.present?(:o)
pry_output_content
elsif opts.present?(:i)
pry_input_content
elsif opts.present?(:d)
code_object_doc
else
code_object_source_or_file
end
restrict_to_lines(content, line_range)
end
end
# The code object
#
# @return [Pry::WrappedModule, Pry::Method, Pry::Command]
def code_object
Pry::CodeObject.lookup(obj_name, pry_instance, super: opts[:super])
end
# Given a string and a range, return the `range` lines of that
# string.
#
# @param [String] content
# @param [Range, Fixnum] range
# @return [String] The string restricted to the given range
def restrict_to_lines(content, range)
Array(content.lines.to_a[range]).join
end
# The selected `pry_instance.output_ring` as a string, as specified by
# the `-o` switch.
#
# @return [String]
def pry_output_content
pry_array_content_as_string(
pry_instance.output_ring,
self.class.output_result_ranges,
&:pretty_inspect
)
end
# The selected `pry_instance.input_ring` as a string, as specified by
# the `-i` switch.
#
# @return [String]
def pry_input_content
pry_array_content_as_string(
pry_instance.input_ring, self.class.input_expression_ranges
) { |v| v }
end
# The line range passed to `--lines`, converted to a 0-indexed range.
def line_range
opts.present?(:lines) ? one_index_range_or_number(opts[:lines]) : 0..-1
end
# Name of the object argument
def obj_name
@obj_name ||= args.empty? ? "" : args.join(" ")
end
private
def bad_option_combination?
[opts.present?(:in), opts.present?(:out),
!args.empty?].count(true) > 1
end
def pry_array_content_as_string(array, ranges)
all = ''
ranges.each do |range|
if convert_to_range(range).first == 0
raise CommandError, "Minimum value for range is 1, not 0."
end
ranged_array = Array(array[range]) || []
ranged_array.compact.each { |v| all += yield(v) }
end
all
end
def code_object_doc
(code_object && code_object.doc) || could_not_locate(obj_name)
end
def code_object_source_or_file
(code_object && code_object.source) || file_content
end
def file_content
if File.exist?(obj_name)
# Set the file accessor.
self.file = obj_name
File.read(obj_name)
else
could_not_locate(obj_name)
end
end
def could_not_locate(name)
raise CommandError, "Cannot locate: #{name}!"
end
def convert_to_range(range)
return range if range.is_a?(Range)
(range..range)
end
end
end
end
@@ -0,0 +1,31 @@
# frozen_string_literal: true
class Pry
class Command
class DisablePry < Pry::ClassCommand
match 'disable-pry'
group 'Navigating Pry'
description 'Stops all future calls to pry and exits the current session.'
banner <<-'BANNER'
Usage: disable-pry
After this command is run any further calls to pry will immediately return `nil`
without interrupting the flow of your program. This is particularly useful when
you've debugged the problem you were having, and now wish the program to run to
the end.
As alternatives, consider using `exit!` to force the current Ruby process
to quit immediately; or using `edit -p` to remove the `binding.pry`
from the code.
BANNER
def process
ENV['DISABLE_PRY'] = 'true'
pry_instance.run_command "exit"
end
end
Pry::Commands.add_command(Pry::Command::DisablePry)
end
end
@@ -0,0 +1,101 @@
# frozen_string_literal: true
class Pry
Pry::Commands.instance_eval do
command(%r{!s/(.*?)/(.*?)}, "") do |source, dest|
eval_string.gsub!(/#{source}/) { dest }
run "show-input"
end
command "east-coker", "" do
txt = %(
--
Now the light falls
Across the open field, leaving the deep lane
Shuttered with branches, dark in the afternoon,
Where you lean against a bank while a van passes,
And the deep lane insists on the direction
Into the village, in the electric heat
Hypnotised. In a warm haze the sultry light
Is absorbed, not refracted, by grey stone.
The dahlias sleep in the empty silence.
Wait for the early owl.
-- T.S Eliot
)
output.puts txt
txt
end
command "cohen-poem", "" do
txt = %(
--
When this American woman,
whose thighs are bound in casual red cloth,
comes thundering past my sitting place
like a forest-burning Mongol tribe,
the city is ravished
and brittle buildings of a hundred years
splash into the street;
and my eyes are burnt
for the embroidered Chinese girls,
already old,
and so small between the thin pines
on these enormous landscapes,
that if you turn your head
they are lost for hours.
-- Leonard Cohen
)
output.puts txt
txt
end
command "pessoa-poem", "" do
output.puts <<-TEXT
--
I've gone to bed with every feeling,
I've been the pimp of every emotion,
All felt sensations have bought me drinks,
I've traded glances with every motive for every act,
I've held hands with every urge to depart,
..
Rage, foam, the vastness that doesn't fit in my handkerchief,
The dog in heat howling in the night,
The pond from the farm going in circles around my insomnia,
The woods as they were, on our late-afternoon walks, the rose,
The indifferent tuft of hair, the moss, the pines,
The rage of not containing all this, not retaining all this,
O abstract hunger for things, impotent libido for moments,
Intellectual orgy of feeling life!
-- Fernando Pessoa
TEXT
end
command "test-ansi", "" do
prev_color = pry_instance.config.color
pry_instance.config.color = true
picture = unindent <<-'OUTPUT'.gsub(/[[:alpha:]!]/) { |s| red(s) }
____ _______________________
/ \ | A W G |
/ O O \ | N I O N ! |
| | | S S R I ! |
\ \__/ / __| I K ! |
\____/ \________________________|
OUTPUT
move_up =
if Helpers::Platform.windows_ansi?
proc { |n| "\e[#{n}F" }
else
proc { |n| "\e[#{n}A\e[0G" }
end
output.puts "\n" * 6
output.puts picture.lines.map(&:chomp).reverse.join(move_up[1])
output.puts "\n" * 6
output.puts "** ENV['TERM'] is #{ENV['TERM']} **\n\n"
pry_instance.config.color = prev_color
end
end
end
@@ -0,0 +1,225 @@
# frozen_string_literal: true
class Pry
class Command
class Edit < Pry::ClassCommand
match 'edit'
group 'Editing'
description 'Invoke the default editor on a file.'
banner <<-'BANNER'
Usage: edit [--no-reload|--reload|--patch] [--line LINE] [--temp|--ex|FILE[:LINE]|OBJECT|--in N]
Open a text editor. When no FILE is given, edits the pry input buffer.
When a method/module/command is given, the code is opened in an editor.
Ensure `Pry.config.editor` or `pry_instance.config.editor` is set to your editor of choice.
edit sample.rb edit -p MyClass#my_method
edit sample.rb --line 105 edit MyClass
edit MyClass#my_method edit --ex
edit --method edit --ex -p
https://github.com/pry/pry/wiki/Editor-integration#wiki-Edit_command
BANNER
command_options state: %i[dynamical_ex_file]
def options(opt)
opt.on :e, :ex, "Open the file that raised the most recent exception " \
"(_ex_.file)",
optional_argument: true, as: Integer
opt.on :i, :in, "Open a temporary file containing the Nth input " \
"expression. N may be a range",
optional_argument: true, as: Range, default: -1..-1
opt.on :t, :temp, "Open an empty temporary file"
opt.on :l, :line, "Jump to this line in the opened file",
argument: true, as: Integer
opt.on :n, :"no-reload", "Don't automatically reload the edited file"
opt.on :c, :current, "Open the current __FILE__ and at __LINE__ (as " \
"returned by `whereami`)"
opt.on :r, :reload, "Reload the edited code immediately (default for " \
"ruby files)"
opt.on :p, :patch, "Instead of editing the object's file, try to edit " \
"in a tempfile and apply as a monkey patch"
opt.on :m, :method, "Explicitly edit the _current_ method (when " \
"inside a method context)."
end
def process
if bad_option_combination?
raise CommandError, "Only one of --ex, --temp, --in, --method and " \
"FILE may be specified."
end
if repl_edit?
# code defined in pry, eval'd within pry.
repl_edit
elsif runtime_patch?
# patch code without persisting changes, implies future changes are patches
apply_runtime_patch
else
# code stored in actual files, eval'd at top-level
file_edit
end
end
def repl_edit?
!opts.present?(:ex) && !opts.present?(:current) && !opts.present?(:method) &&
filename_argument.empty?
end
def repl_edit
content = Pry::Editor.new(pry_instance).edit_tempfile_with_content(
initial_temp_file_content,
initial_temp_file_content.lines.count
)
pry_instance.eval_string = content
Pry.history.push(content)
end
def file_based_exception?
opts.present?(:ex) && !opts.present?(:patch)
end
def runtime_patch?
!file_based_exception? &&
(opts.present?(:patch) ||
previously_patched?(code_object) ||
pry_method?(code_object))
end
def apply_runtime_patch
if patch_exception?
ExceptionPatcher.new(
pry_instance, state, file_and_line_for_current_exception
).perform_patch
elsif code_object.is_a?(Pry::Method)
code_object.redefine(
Pry::Editor.new(pry_instance).edit_tempfile_with_content(
code_object.source
)
)
else
raise NotImplementedError, "Cannot yet patch #{code_object} objects!"
end
end
def ensure_file_name_is_valid(file_name)
unless file_name
raise CommandError, "Cannot find a valid file for #{filename_argument}"
end
return unless not_a_real_file?(file_name)
raise CommandError, "#{file_name} is not a valid file name, cannot edit!"
end
def file_and_line_for_current_exception
FileAndLineLocator.from_exception(pry_instance.last_exception, opts[:ex].to_i)
end
def file_and_line
file_name, line =
if opts.present?(:current)
FileAndLineLocator.from_binding(target)
elsif opts.present?(:ex)
file_and_line_for_current_exception
elsif code_object
FileAndLineLocator.from_code_object(code_object, filename_argument)
else
# when file and line are passed as a single arg, e.g my_file.rb:30
FileAndLineLocator.from_filename_argument(filename_argument)
end
[file_name, opts.present?(:line) ? opts[:l].to_i : line]
end
def file_edit
file_name, line = file_and_line
ensure_file_name_is_valid(file_name)
Pry::Editor.new(pry_instance).invoke_editor(file_name, line, reload?(file_name))
set_file_and_dir_locals(file_name)
return unless reload?(file_name)
silence_warnings { load(file_name) }
end
def filename_argument
args.join(' ')
end
def code_object
@code_object ||=
!probably_a_file?(filename_argument) &&
Pry::CodeObject.lookup(filename_argument, pry_instance)
end
def pry_method?(code_object)
code_object.is_a?(Pry::Method) &&
code_object.pry_method?
end
def previously_patched?(code_object)
code_object.is_a?(Pry::Method) &&
Pry::Method::Patcher.code_for(code_object.source_location.first)
end
def patch_exception?
opts.present?(:ex) && opts.present?(:patch)
end
def bad_option_combination?
[
opts.present?(:ex), opts.present?(:temp),
opts.present?(:in), opts.present?(:method),
!filename_argument.empty?
].count(true) > 1
end
def input_expression
case opts[:i]
when Range
(pry_instance.input_ring[opts[:i]] || []).join
when Integer
pry_instance.input_ring[opts[:i]] || ""
else
raise Pry::CommandError, "Not a valid range: #{opts[:i]}"
end
end
def reloadable?
opts.present?(:reload) || opts.present?(:ex)
end
def never_reload?
opts.present?(:'no-reload') || pry_instance.config.disable_auto_reload
end
def reload?(file_name = "")
(reloadable? || file_name.end_with?(".rb")) && !never_reload?
end
def initial_temp_file_content
if opts.present?(:temp)
""
elsif opts.present?(:in)
input_expression
elsif eval_string.strip != ""
eval_string
else
pry_instance.input_ring.to_a.reverse_each.find { |x| x && x.strip != "" } || ""
end
end
def probably_a_file?(str)
[".rb", ".c", ".py", ".yml", ".gemspec"].include?(File.extname(str)) ||
str =~ %r{/|\\}
end
end
Pry::Commands.add_command(Pry::Command::Edit)
end
end
@@ -0,0 +1,29 @@
# frozen_string_literal: true
class Pry
class Command
class Edit
class ExceptionPatcher
attr_accessor :pry_instance
attr_accessor :state
attr_accessor :file_and_line
def initialize(pry_instance, state, exception_file_and_line)
@pry_instance = pry_instance
@state = state
@file_and_line = exception_file_and_line
end
# perform the patch
def perform_patch
file_name, = file_and_line
lines = state.dynamical_ex_file || File.read(file_name)
source = Pry::Editor.new(pry_instance).edit_tempfile_with_content(lines)
pry_instance.evaluate_ruby source
state.dynamical_ex_file = source.split("\n")
end
end
end
end
end
@@ -0,0 +1,47 @@
# frozen_string_literal: true
class Pry
class Command
class Edit
module FileAndLineLocator
class << self
def from_binding(target)
if target.respond_to?(:source_location)
target.source_location
else
target.eval("[__FILE__, __LINE__]")
end
end
def from_code_object(code_object, filename_argument)
unless File.exist?(code_object.source_file.to_s)
raise CommandError, "Cannot find a file for #{filename_argument}!"
end
[code_object.source_file, code_object.source_line]
end
def from_exception(exception, backtrace_level)
raise CommandError, "No exception found." if exception.nil?
file_name, line = exception.bt_source_location_for(backtrace_level)
raise CommandError, "Exception has no associated file." if file_name.nil?
if Pry.eval_path == file_name
raise CommandError, "Cannot edit exceptions raised in REPL."
end
[file_name, line]
end
# when file and line are passed as a single arg, e.g my_file.rb:30
def from_filename_argument(filename_argument)
f = File.expand_path(filename_argument)
l = f.sub!(/:(\d+)$/, "") ? Regexp.last_match(1).to_i : 1
[f, l]
end
end
end
end
end
end
@@ -0,0 +1,47 @@
# frozen_string_literal: true
class Pry
class Command
class Exit < Pry::ClassCommand
match 'exit'
group 'Navigating Pry'
description 'Pop the previous binding.'
command_options keep_retval: true
banner <<-'BANNER'
Usage: exit [OPTIONS] [--help]
Aliases: quit
Pop the previous binding (does NOT exit program). It can be useful to exit a
context with a user-provided value. For instance an exit value can be used to
determine program flow.
exit "pry this"
exit
https://github.com/pry/pry/wiki/State-navigation#wiki-Exit_with_value
BANNER
def process
if pry_instance.binding_stack.one?
pry_instance.run_command "exit-all #{arg_string}"
else
# otherwise just pop a binding and return user supplied value
process_pop_and_return
end
end
def process_pop_and_return
popped_object = pry_instance.binding_stack.pop.eval('self')
# return a user-specified value if given otherwise return the object
return target.eval(arg_string) unless arg_string.empty?
popped_object
end
end
Pry::Commands.add_command(Pry::Command::Exit)
Pry::Commands.alias_command 'quit', 'exit'
end
end
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class Pry
class Command
class ExitAll < Pry::ClassCommand
match 'exit-all'
group 'Navigating Pry'
description 'End the current Pry session.'
banner <<-'BANNER'
Usage: exit-all [--help]
Aliases: !!@
End the current Pry session (popping all bindings and returning to caller).
Accepts optional return value.
BANNER
def process
# calculate user-given value
exit_value = target.eval(arg_string)
# clear the binding stack
pry_instance.binding_stack.clear
# break out of the repl loop
throw(:breakout, exit_value)
end
end
Pry::Commands.add_command(Pry::Command::ExitAll)
Pry::Commands.alias_command '!!@', 'exit-all'
end
end
@@ -0,0 +1,27 @@
# frozen_string_literal: true
class Pry
class Command
class ExitProgram < Pry::ClassCommand
match 'exit-program'
group 'Navigating Pry'
description 'End the current program.'
banner <<-'BANNER'
Usage: exit-program [--help]
Aliases: quit-program
!!!
End the current program.
BANNER
def process
Kernel.exit target.eval(arg_string).to_i
end
end
Pry::Commands.add_command(Pry::Command::ExitProgram)
Pry::Commands.alias_command 'quit-program', 'exit-program'
Pry::Commands.alias_command '!!!', 'exit-program'
end
end
@@ -0,0 +1,199 @@
# frozen_string_literal: true
class Pry
class Command
class FindMethod < Pry::ClassCommand
extend Pry::Helpers::BaseHelpers
match 'find-method'
group 'Context'
description 'Recursively search for a method within a Class/Module or ' \
'the current namespace.'
command_options shellwords: false
banner <<-'BANNER'
Usage: find-method [-n|-c] METHOD [NAMESPACE]
Recursively search for a method within a Class/Module or the current namespace.
Use the `-n` switch (the default) to search for methods whose name matches the
given regex. Use the `-c` switch to search for methods that contain the given
code.
# Find all methods whose name match /re/ inside
# the Pry namespace. Matches Pry#repl, etc.
find-method re Pry
# Find all methods that contain the code:
# output.puts inside the Pry namespace.
find-method -c 'output.puts' Pry
BANNER
def options(opt)
opt.on :n, :name, "Search for a method by name"
opt.on :c, :content, "Search for a method based on content in Regex form"
end
def process
return if args.empty?
klass = search_class
matches = opts.content? ? content_search(klass) : name_search(klass)
show_search_results(matches)
end
private
# @return [Regexp] The pattern to search for.
def pattern
@pattern ||= ::Regexp.new args[0]
end
# Output the result of the search.
#
# @param [Array] matches
def show_search_results(matches)
if matches.empty?
output.puts bold("No Methods Matched")
else
print_matches(matches)
end
end
# The class to search for methods.
# We only search classes, so if the search object is an
# instance, return its class. If no search object is given
# search `target_self`.
def search_class
klass = if args[1]
target.eval(args[1])
else
target_self
end
klass.is_a?(Module) ? klass : klass.class
end
# pretty-print a list of matching methods.
#
# @param [Array<Method>] matches
def print_matches(matches)
grouped = matches.group_by(&:owner)
order = grouped.keys.sort_by { |x| x.name || x.to_s }
order.each do |klass|
print_matches_for_class(klass, grouped)
end
end
# Print matched methods for a class
def print_matches_for_class(klass, grouped)
output.puts bold(klass.name)
grouped[klass].each do |method|
header = method.name_with_owner
output.puts header + additional_info(header, method)
end
end
# Return the matched lines of method source if `-c` is given or ""
# if `-c` was not given
def additional_info(header, method)
if opts.content?
': ' + colorize_code(matched_method_lines(header, method))
else
""
end
end
def matched_method_lines(header, method)
method.source.split(/\n/).select { |x| x =~ pattern }.join(
"\n#{' ' * header.length}"
)
end
# Run the given block against every constant in the provided namespace.
#
# @param [Module] klass The namespace in which to start the search.
# @param [Hash<Module,Boolean>] done The namespaces we've already visited (private)
# @yieldparam klass Each class/module in the namespace.
#
def recurse_namespace(klass, done = {}, &block)
return if !klass.is_a?(Module) || done[klass]
done[klass] = true
yield klass
klass.constants.each do |name|
next if klass.autoload?(name)
begin
const = Pry::Method.singleton_class_of(klass.const_get(name))
rescue RescuableException # rubocop:disable Lint/HandleExceptions
# constant loading is an inexact science at the best of times,
# this often happens when a constant was .autoload? but someone
# tried to load it. It's now not .autoload? but will still raise
# a NameError when you access it.
else
recurse_namespace(const, done, &block)
end
end
end
# Gather all the methods in a namespace that pass the given block.
#
# @param [Module] namespace The namespace in which to search.
# @yieldparam [Method] method The method to test
# @yieldreturn [Boolean]
# @return [Array<Method>]
#
def search_all_methods(namespace)
done = Hash.new { |h, k| h[k] = {} }
matches = []
recurse_namespace(namespace) do |klass|
methods = Pry::Method.all_from_class(klass) + Pry::Method.all_from_obj(klass)
methods.each do |method|
next if done[method.owner][method.name]
done[method.owner][method.name] = true
matches << method if yield method
end
end
matches
end
# Search for all methods with a name that matches the given regex
# within a namespace.
#
# @param [Module] namespace The namespace to search
# @return [Array<Method>]
#
def name_search(namespace)
search_all_methods(namespace) do |meth|
meth.name =~ pattern
end
end
# Search for all methods who's implementation matches the given regex
# within a namespace.
#
# @param [Module] namespace The namespace to search
# @return [Array<Method>]
#
def content_search(namespace)
search_all_methods(namespace) do |meth|
begin
meth.source =~ pattern
rescue RescuableException
false
end
end
end
end
Pry::Commands.add_command(Pry::Command::FindMethod)
end
end
@@ -0,0 +1,23 @@
# frozen_string_literal: true
class Pry
class Command
class FixIndent < Pry::ClassCommand
match 'fix-indent'
group 'Input and Output'
description "Correct the indentation for contents of the input buffer"
banner <<-USAGE
Usage: fix-indent
USAGE
def process
indented_str = Pry::Indent.indent(eval_string)
pry_instance.eval_string = indented_str
end
end
Pry::Commands.add_command(Pry::Command::FixIndent)
end
end
@@ -0,0 +1,171 @@
# frozen_string_literal: true
class Pry
class Command
class Help < Pry::ClassCommand
match 'help'
group 'Help'
description 'Show a list of commands or information about a specific command.'
banner <<-'BANNER'
Usage: help [COMMAND]
With no arguments, help lists all the available commands along with their
descriptions. When given a command name as an argument, shows the help
for that command.
BANNER
# We only want to show commands that have descriptions, so that the
# easter eggs don't show up.
def visible_commands
visible = {}
commands.each do |key, command|
visible[key] = command if command.description && !command.description.empty?
end
visible
end
# Get a hash of available commands grouped by the "group" name.
def command_groups
visible_commands.values.group_by(&:group)
end
def process
if args.empty?
display_index(command_groups)
else
display_search(args.first)
end
end
# Display the index view, with headings and short descriptions per command.
#
# @param [Hash<String, Array<Commands>>] groups
def display_index(groups)
help_text = []
sorted_group_names(groups).each do |group_name|
commands = sorted_commands(groups[group_name])
help_text << help_text_for_commands(group_name, commands) if commands.any?
end
pry_instance.pager.page help_text.join("\n\n")
end
# Given a group name and an array of commands,
# return the help string for those commands.
#
# @param [String] name The group name.
# @param [Array<Pry::Command>] commands
# @return [String] The generated help string.
def help_text_for_commands(name, commands)
"#{bold(name.capitalize)}\n" + commands.map do |command|
" #{command.options[:listing].to_s.ljust(18)} " \
"#{command.description.capitalize}"
end.join("\n")
end
# @param [Hash] groups
# @return [Array<String>] An array of sorted group names.
def sorted_group_names(groups)
groups.keys.sort_by(&method(:group_sort_key))
end
# Sort an array of commands by their `listing` name.
#
# @param [Array<Pry::Command>] commands The commands to sort
# @return [Array<Pry::Command>] commands sorted by listing name.
def sorted_commands(commands)
commands.sort_by { |command| command.options[:listing].to_s }
end
# Display help for an individual command or group.
#
# @param [String] search The string to search for.
def display_search(search)
if (command = command_set.find_command_for_help(search))
display_command(command)
else
display_filtered_search_results(search)
end
end
# Display help for a searched item, filtered first by group
# and if that fails, filtered by command name.
#
# @param [String] search The string to search for.
def display_filtered_search_results(search)
groups = search_hash(search, command_groups)
if !groups.empty?
display_index(groups)
else
display_filtered_commands(search)
end
end
# Display help for a searched item, filtered by group
#
# @param [String] search The string to search for.
def display_filtered_commands(search)
filtered = search_hash(search, visible_commands)
raise CommandError, "No help found for '#{args.first}'" if filtered.empty?
if filtered.size == 1
display_command(filtered.values.first)
else
display_index("'#{search}' commands" => filtered.values)
end
end
# Display help for an individual command.
#
# @param [Pry::Command] command
def display_command(command)
pry_instance.pager.page command.new.help
end
# Find a subset of a hash that matches the user's search term.
#
# If there's an exact match a Hash of one element will be returned,
# otherwise a sub-Hash with every key that matches the search will
# be returned.
#
# @param [String] search the search term
# @param [Hash] hash the hash to search
def search_hash(search, hash)
matching = {}
hash.each_pair do |key, value|
next unless key.is_a?(String)
return { key => value } if normalize(key) == normalize(search)
next unless normalize(key).start_with?(normalize(search))
matching[key] = value
end
matching
end
# Clean search terms to make it easier to search group names
#
# @param [String] key
# @return [String]
def normalize(key)
key.downcase.gsub(/pry\W+/, '')
end
def group_sort_key(group_name)
[
%w[
Help Context Editing Introspection Input_and_output Navigating_pry
Gems Basic Commands
].index(group_name.tr(' ', '_')) || 99, group_name
]
end
end
Pry::Commands.add_command(Pry::Command::Help)
end
end
@@ -0,0 +1,182 @@
# frozen_string_literal: true
class Pry
class Command
class Hist < Pry::ClassCommand
match 'hist'
group 'Editing'
description 'Show and replay Readline history.'
banner <<-'BANNER'
Usage: hist [--head|--tail]
hist --all
hist --head N
hist --tail N
hist --show START..END
hist --grep PATTERN
hist --clear
hist --replay START..END
hist --save [START..END] FILE
Aliases: history
Show and replay Readline history.
BANNER
def options(opt)
opt.on :a, :all, "Display all history"
opt.on :H, :head, "Display the first N items",
optional_argument: true, as: Integer
opt.on :T, :tail, "Display the last N items",
optional_argument: true, as: Integer
opt.on :s, :show, "Show the given range of lines",
optional_argument: true, as: Range
opt.on :G, :grep, "Show lines matching the given pattern",
argument: true, as: String
opt.on :c, :clear, "Clear the current session's history"
opt.on :r, :replay, "Replay a line or range of lines",
argument: true, as: Range
opt.on :save, "Save history to a file", argument: true, as: Range
opt.on :e, :'exclude-pry', "Exclude Pry commands from the history"
opt.on :n, :'no-numbers', "Omit line numbers"
end
def process
@history = find_history
@history = @history.between(opts[:show]) if opts.present?(:show)
@history = @history.grep(opts[:grep]) if opts.present?(:grep)
@history =
if opts.present?(:head)
@history.take_lines(1, opts[:head] || 10)
elsif opts.present?(:tail)
@history.take_lines(-(opts[:tail] || 10), opts[:tail] || 10)
else
@history
end
if opts.present?(:'exclude-pry')
@history = @history.reject do |loc|
command_set.valid_command?(loc.line)
end
end
if opts.present?(:save)
process_save
elsif opts.present?(:clear)
process_clear
elsif opts.present?(:replay)
process_replay
else
process_display
end
end
private
def process_display
@history = @history.with_line_numbers unless opts.present?(:'no-numbers')
pry_instance.pager.open do |pager|
@history.print_to_output(pager, true)
end
end
def process_save
case opts[:save]
when Range
@history = @history.between(opts[:save])
raise CommandError, "Must provide a file name." unless args.first
file_name = File.expand_path(args.first)
when String
file_name = File.expand_path(opts[:save])
end
output.puts "Saving history in #{file_name}..."
File.open(file_name, 'w') { |f| f.write(@history.raw) }
output.puts "History saved."
end
def process_clear
Pry.history.clear
output.puts "History cleared."
end
def process_replay
@history = @history.between(opts[:r])
replay_sequence = @history.raw
# If we met follow-up "hist" call, check for the "--replay" option
# presence. If "hist" command is called with other options, proceed
# further.
check_for_juxtaposed_replay(replay_sequence)
replay_sequence.lines.each do |line|
pry_instance.eval line, generated: true
end
end
# Checks +replay_sequence+ for the presence of neighboring replay calls.
# @example
# [1] pry(main)> hist --show 46894
# 46894: hist --replay 46675..46677
# [2] pry(main)> hist --show 46675..46677
# 46675: 1+1
# 46676: a = 100
# 46677: hist --tail
# [3] pry(main)> hist --replay 46894
# Error: Replay index 46894 points out to another replay call:
# `hist -r 46675..46677`
# [4] pry(main)>
#
# @raise [Pry::CommandError] If +replay_sequence+ contains another
# "hist --replay" call
# @param [String] replay_sequence The sequence of commands to be replayed
# (per saltum)
# @return [Boolean] `false` if +replay_sequence+ does not contain another
# "hist --replay" call
def check_for_juxtaposed_replay(replay_sequence)
if replay_sequence =~ /\Ahist(?:ory)?\b/
# Create *fresh* instance of Options for parsing of "hist" command.
slop_instance = slop
slop_instance.parse(replay_sequence.split(' ')[1..-1])
if slop_instance.present?(:r)
replay_sequence = replay_sequence.split("\n").join('; ')
index = opts[:r]
index = index.min if index.min == index.max || index.max.nil?
raise CommandError,
"Replay index #{index} points out to another replay call: " \
"`#{replay_sequence}`"
end
else
false
end
end
# Finds history depending on the given switch.
#
# @return [Pry::Code] if it finds `--all` (or `-a`) switch, returns all
# entries in history. Without the switch returns only the entries from the
# current Pry session.
def find_history
h = if opts.present?(:all)
Pry.history.to_a
else
Pry.history.to_a.last(Pry.history.session_line_count)
end
Pry::Code(Pry.history.filter(h[0..-2]))
end
end
Pry::Commands.add_command(Pry::Command::Hist)
Pry::Commands.alias_command 'history', 'hist'
end
end
@@ -0,0 +1,27 @@
# frozen_string_literal: true
class Pry
class Command
class ImportSet < Pry::ClassCommand
match 'import-set'
group 'Commands'
# TODO: Provide a better description with examples and a general conception
# of this command.
description 'Import a Pry command set.'
banner <<-'BANNER'
Import a Pry command set.
BANNER
# TODO: resolve unused parameter.
def process(_command_set_name)
raise CommandError, "Provide a command set name" if command_set.nil?
set = target.eval(arg_string)
pry_instance.commands.import set
end
end
Pry::Commands.add_command(Pry::Command::ImportSet)
end
end
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class Pry
class Command
class JumpTo < Pry::ClassCommand
match 'jump-to'
group 'Navigating Pry'
description 'Jump to a binding further up the stack.'
banner <<-'BANNER'
Jump to a binding further up the stack, popping all bindings below.
BANNER
def process(break_level)
break_level = break_level.to_i
nesting_level = pry_instance.binding_stack.size - 1
max_nest_level = nesting_level - 1
case break_level
when nesting_level
output.puts "Already at nesting level #{nesting_level}"
when 0..max_nest_level
pry_instance.binding_stack = pry_instance.binding_stack[0..break_level]
else
output.puts "Invalid nest level. Must be between 0 and " \
"#{max_nest_level}. Got #{break_level}."
end
end
end
Pry::Commands.add_command(Pry::Command::JumpTo)
end
end
@@ -0,0 +1,42 @@
# frozen_string_literal: true
class Pry
class Command
class ListInspectors < Pry::ClassCommand
match 'list-inspectors'
group 'Input and Output'
description 'List the inspector procs available for use.'
banner <<-BANNER
Usage: list-inspectors
List the inspector procs available to print return values. You can use
change-inspector to switch between them.
BANNER
def process
output.puts heading("Available inspectors") + "\n"
inspector_map.each do |name, inspector|
output.write "Name: #{bold(name)}"
output.puts selected_inspector?(inspector) ? selected_text : ""
output.puts inspector[:description]
output.puts
end
end
private
def inspector_map
Pry::Inspector::MAP
end
def selected_text
red " (selected) "
end
def selected_inspector?(inspector)
pry_instance.print == inspector[:value]
end
Pry::Commands.add_command(self)
end
end
end
+114
View File
@@ -0,0 +1,114 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
match 'ls'
group 'Context'
description 'Show the list of vars and methods in the current scope.'
command_options shellwords: false, interpolate: false
banner <<-'BANNER'
Usage: ls [-m|-M|-p|-pM] [-q|-v] [-c|-i] [Object]
ls [-g] [-l]
ls shows you which methods, constants and variables are accessible to Pry. By
default it shows you the local variables defined in the current shell, and any
public methods or instance variables defined on the current object.
The colours used are configurable using Pry.config.ls.*_color, and the separator
is Pry.config.ls.separator.
Pry.config.ls.ceiling is used to hide methods defined higher up in the
inheritance chain, this is by default set to [Object, Module, Class] so that
methods defined on all Objects are omitted. The -v flag can be used to ignore
this setting and show all methods, while the -q can be used to set the ceiling
much lower and show only methods defined on the object or its direct class.
Also check out `find-method` command (run `help find-method`).
BANNER
def options(opt)
opt.on :m, :methods, "Show public methods defined on the Object"
opt.on :M, "instance-methods", "Show public methods defined in a " \
"Module or Class"
opt.on :p, :ppp, "Show public, protected (in yellow) and private " \
"(in green) methods"
opt.on :q, :quiet, "Show only methods defined on object.singleton_class " \
"and object.class"
opt.on :v, :verbose, "Show methods and constants on all super-classes " \
"(ignores Pry.config.ls.ceiling)"
opt.on :g, :globals, "Show global variables, including those builtin to " \
"Ruby (in cyan)"
opt.on :l, :locals, "Show hash of local vars, sorted by descending size"
opt.on :c, :constants, "Show constants, highlighting classes (in blue), " \
"and exceptions (in purple).\n" \
"#{' ' * 32}Constants that are pending autoload? " \
"are also shown (in yellow)"
opt.on :i, :ivars, "Show instance variables (in blue) and class " \
"variables (in bright blue)"
opt.on :G, :grep, "Filter output by regular expression", argument: true
if Object.respond_to?(:deprecate_constant)
opt.on :d, :dconstants, "Show deprecated constants"
end
return unless Helpers::Platform.jruby?
opt.on :J, "all-java", "Show all the aliases for methods from java " \
"(default is to show only prettiest)"
end
# Exclude -q, -v and --grep because they,
# don't specify what the user wants to see.
def no_user_opts?
!(opts[:methods] || opts['instance-methods'] || opts[:ppp] ||
opts[:globals] || opts[:locals] || opts[:constants] || opts[:ivars])
end
def process
@interrogatee = args.empty? ? target_self : target.eval(args.join(' '))
raise_errors_if_arguments_are_weird
ls_entity = LsEntity.new(
interrogatee: @interrogatee,
no_user_opts: no_user_opts?,
opts: opts,
args: args,
pry_instance: pry_instance
)
pry_instance.pager.page ls_entity.entities_table
end
private
def error_list
any_args = args.any?
# rubocop:disable Style/CaseEquality
non_mod_interrogatee = !(Module === @interrogatee)
# rubocop:enable Style/CaseEquality
[
['-l does not make sense with a specified Object', :locals, any_args],
['-g does not make sense with a specified Object', :globals, any_args],
['-q does not make sense with -v', :quiet, opts.present?(:verbose)],
[
'-M only makes sense with a Module or a Class', 'instance-methods',
non_mod_interrogatee
],
[
'-c only makes sense with a Module or a Class', :constants,
any_args && non_mod_interrogatee
]
]
end
def raise_errors_if_arguments_are_weird
error_list.each do |message, option, invalid_expr|
raise Pry::CommandError, message if opts.present?(option) && invalid_expr
end
end
end
Pry::Commands.add_command(Pry::Command::Ls)
end
end
@@ -0,0 +1,54 @@
# frozen_string_literal: true
require 'forwardable'
class Pry
class Command
class Ls < Pry::ClassCommand
class Config
extend Forwardable
DEFAULT_OPTIONS = {
heading_color: :bright_blue,
public_method_color: :default,
private_method_color: :blue,
protected_method_color: :blue,
method_missing_color: :bright_red,
local_var_color: :yellow,
pry_var_color: :default, # e.g. _, pry_instance, _file_
instance_var_color: :blue, # e.g. @foo
class_var_color: :bright_blue, # e.g. @@foo
global_var_color: :default, # e.g. $CODERAY_DEBUG, $eventmachine_library
builtin_global_color: :cyan, # e.g. $stdin, $-w, $PID
pseudo_global_color: :cyan, # e.g. $~, $1..$9, $LAST_MATCH_INFO
constant_color: :default, # e.g. VERSION, ARGF
class_constant_color: :blue, # e.g. Object, Kernel
exception_constant_color: :magenta, # e.g. Exception, RuntimeError
unloaded_constant_color: :yellow, # Constant still in .autoload? state
separator: " ",
ceiling: [Object, Module, Class]
}.freeze
DEFAULT_OPTIONS.each_key do |key|
define_method key do
@config[key]
end
define_method "#{key}=" do |value|
@config[key] = value
end
end
def_delegators :@config, :[], :[]=, :each, :each_pair, :values, :keys, :to_a
def initialize(config)
@config = config
end
def self.default
new(DEFAULT_OPTIONS.dup)
end
end
end
end
end
@@ -0,0 +1,75 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
class Constants < Pry::Command::Ls::Formatter
DEPRECATED_CONSTANTS = %i[
Data Fixnum Bignum TimeoutError NIL FALSE TRUE
].tap do |constants|
constants << :JavaPackageModuleTemplate if Helpers::Platform.jruby?
end
include Pry::Command::Ls::Interrogatable
def initialize(interrogatee, no_user_opts, opts, pry_instance)
super(pry_instance)
@interrogatee = interrogatee
@no_user_opts = no_user_opts
@default_switch = opts[:constants]
@verbose_switch = opts[:verbose]
@dconstants = opts.dconstants?
end
def correct_opts?
super || (@no_user_opts && interrogating_a_module?)
end
def output_self
mod = interrogatee_mod
constants = WrappedModule.new(mod).constants(@verbose_switch)
output_section('constants', grep.regexp[format(mod, constants)])
end
private
def show_deprecated_constants?
@dconstants == true
end
def format(mod, constants)
constants.sort_by(&:downcase).map do |name|
if Object.respond_to?(:deprecate_constant) &&
DEPRECATED_CONSTANTS.include?(name) &&
!show_deprecated_constants?
next
end
if (const = (begin
!mod.autoload?(name) && (mod.const_get(name) || true)
rescue StandardError
nil
end))
if begin
const < Exception
rescue StandardError
false
end
color(:exception_constant, name)
elsif begin
mod.const_get(name).is_a?(Module)
rescue StandardError
false
end
color(:class_constant, name)
else
color(:constant, name)
end
else
color(:unloaded_constant, name)
end
end
end
end
end
end
end
@@ -0,0 +1,55 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
class Formatter
attr_writer :grep
attr_reader :pry_instance
def initialize(pry_instance)
@pry_instance = pry_instance
@target = pry_instance.current_context
@default_switch = nil
end
def write_out
return false unless correct_opts?
output_self
end
private
def color(type, str)
Pry::Helpers::Text.send pry_instance.config.ls.send("#{type}_color"), str
end
# Add a new section to the output.
# Outputs nothing if the section would be empty.
def output_section(heading, body)
return '' if body.compact.empty?
fancy_heading = Pry::Helpers::Text.bold(color(:heading, heading))
Pry::Helpers.tablify_or_one_line(fancy_heading, body, @pry_instance)
end
def format_value(value)
Pry::ColorPrinter.pp(value, ''.dup)
end
def correct_opts?
@default_switch
end
def output_self
raise NotImplementedError
end
def grep
@grep || proc { |x| x }
end
end
end
end
end
@@ -0,0 +1,50 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
class Globals < Pry::Command::Ls::Formatter
# Taken from "puts global_variables.inspect".
BUILTIN_GLOBALS =
%w[$" $$ $* $, $-0 $-F $-I $-K $-W $-a $-d $-i $-l $-p $-v $-w $. $/ $\\
$: $; $< $= $> $0 $ARGV $CONSOLE $DEBUG $DEFAULT_INPUT $DEFAULT_OUTPUT
$FIELD_SEPARATOR $FILENAME $FS $IGNORECASE $INPUT_LINE_NUMBER
$INPUT_RECORD_SEPARATOR $KCODE $LOADED_FEATURES $LOAD_PATH $NR $OFS
$ORS $OUTPUT_FIELD_SEPARATOR $OUTPUT_RECORD_SEPARATOR $PID $PROCESS_ID
$PROGRAM_NAME $RS $VERBOSE $deferr $defout $stderr $stdin $stdout].freeze
# `$SAFE` and `$?` are thread-local, the exception stuff only works in a
# rescue clause, everything else is basically a local variable with a `$`
# in its name.
PSEUDO_GLOBALS =
%w[$! $' $& $` $@ $? $+ $_ $~ $1 $2 $3 $4 $5 $6 $7 $8 $9
$CHILD_STATUS $SAFE $ERROR_INFO $ERROR_POSITION $LAST_MATCH_INFO
$LAST_PAREN_MATCH $LAST_READ_LINE $MATCH $POSTMATCH $PREMATCH].freeze
def initialize(opts, pry_instance)
super(pry_instance)
@default_switch = opts[:globals]
end
def output_self
variables = format(@target.eval('global_variables'))
output_section('global variables', grep.regexp[variables])
end
private
def format(globals)
globals.map(&:to_s).sort_by(&:downcase).map do |name|
if PSEUDO_GLOBALS.include?(name)
color(:pseudo_global, name)
elsif BUILTIN_GLOBALS.include?(name)
color(:builtin_global, name)
else
color(:global_var, name)
end
end
end
end
end
end
end
@@ -0,0 +1,23 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
class Grep
def initialize(grep_regexp)
@grep_regexp = grep_regexp
end
def regexp
proc { |x|
if x.instance_of?(Array)
x.grep(@grep_regexp)
else
x =~ @grep_regexp
end
}
end
end
end
end
end
@@ -0,0 +1,40 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
class InstanceVars < Pry::Command::Ls::Formatter
include Pry::Command::Ls::Interrogatable
def initialize(interrogatee, no_user_opts, opts, pry_instance)
super(pry_instance)
@interrogatee = interrogatee
@no_user_opts = no_user_opts
@default_switch = opts[:ivars]
end
def correct_opts?
super || @no_user_opts
end
def output_self
ivars = if Object === @interrogatee # rubocop:disable Style/CaseEquality
Pry::Method.safe_send(@interrogatee, :instance_variables)
else
[] # TODO: BasicObject support
end
kvars = Pry::Method.safe_send(interrogatee_mod, :class_variables)
ivars_out = output_section('instance variables', format(:instance_var, ivars))
kvars_out = output_section('class variables', format(:class_var, kvars))
ivars_out + kvars_out
end
private
def format(type, vars)
vars.sort_by { |var| var.to_s.downcase }.map { |var| color(type, var) }
end
end
end
end
end
@@ -0,0 +1,24 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
module Interrogatable
private
def interrogating_a_module?
Module === @interrogatee # rubocop:disable Style/CaseEquality
end
def interrogatee_mod
if interrogating_a_module?
@interrogatee
else
singleton = Pry::Method.singleton_class_of(@interrogatee)
singleton.ancestors.grep(::Class).reject { |c| c == singleton }.first
end
end
end
end
end
end
@@ -0,0 +1,55 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
module JRubyHacks
private
# JRuby creates lots of aliases for methods imported from java in an attempt
# to make life easier for ruby programmers. (e.g. getFooBar becomes
# get_foo_bar and foo_bar, and maybe foo_bar? if it returns a Boolean). The
# full transformations are in the assignAliases method of:
# https://github.com/jruby/jruby/blob/master/src/org/jruby/javasupport/JavaClass.java
#
# This has the unfortunate side-effect of making the output of ls even more
# incredibly verbose than it normally would be for these objects; and so we
# filter out all but the nicest of these aliases here.
#
# TODO: This is a little bit vague, better heuristics could be used.
# JRuby also has a lot of scala-specific logic, which we don't copy.
def trim_jruby_aliases(methods)
grouped = methods.group_by do |m|
m.name.sub(/\A(is|get|set)(?=[A-Z_])/, '').gsub(/[_?=]/, '').downcase
end
grouped.flat_map do |_key, values|
values = values.sort_by do |m|
rubbishness(m.name)
end
found = []
values.select do |x|
(found.none? { |y| x == y }) && found << x
end
end
end
# When removing jruby aliases, we want to keep the alias that is
# "least rubbish" according to this metric.
def rubbishness(name)
name.each_char.map do |x|
case x
when /[A-Z]/
1
when '?', '=', '!'
-2
else
0
end
end.inject(&:+) + (name.size / 100.0)
end
end
end
end
end
@@ -0,0 +1,37 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
class LocalNames < Pry::Command::Ls::Formatter
def initialize(no_user_opts, args, pry_instance)
super(pry_instance)
@no_user_opts = no_user_opts
@args = args
@sticky_locals = pry_instance.sticky_locals
end
def correct_opts?
super || (@no_user_opts && @args.empty?)
end
def output_self
local_vars = grep.regexp[@target.eval('local_variables')]
output_section('locals', format(local_vars))
end
private
def format(locals)
locals.sort_by(&:downcase).map do |name|
if @sticky_locals.include?(name.to_sym)
color(:pry_var, name)
else
color(:local_var, name)
end
end
end
end
end
end
end
@@ -0,0 +1,47 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
class LocalVars < Pry::Command::Ls::Formatter
def initialize(opts, pry_instance)
super(pry_instance)
@default_switch = opts[:locals]
@sticky_locals = pry_instance.sticky_locals
end
def output_self
locals = @target.eval('local_variables').reject do |e|
@sticky_locals.key?(e.to_sym)
end
name_value_pairs = locals.map do |name|
[name, @target.eval(name.to_s)]
end
format(name_value_pairs).join('')
end
private
def format(name_value_pairs)
sorted = name_value_pairs.sort_by do |_name, value|
value.to_s.size
end
sorted.reverse.map do |name, value|
colorized_assignment_style(name, format_value(value))
end
end
def colorized_assignment_style(lhs, rhs, desired_width = 7)
colorized_lhs = color(:local_var, lhs)
color_escape_padding = colorized_lhs.size - lhs.size
pad = desired_width + color_escape_padding
Kernel.format(
"%-#{pad}<name>s = %<value>s",
name: color(:local_var, colorized_lhs),
value: rhs
)
end
end
end
end
end
@@ -0,0 +1,65 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
class LsEntity
attr_reader :pry_instance
def initialize(opts)
@interrogatee = opts[:interrogatee]
@no_user_opts = opts[:no_user_opts]
@opts = opts[:opts]
@args = opts[:args]
@grep = Grep.new(Regexp.new(opts[:opts][:G] || '.'))
@pry_instance = opts.delete(:pry_instance)
end
def entities_table
entities.map(&:write_out).select { |o| o }.join('')
end
private
def grep(entity)
entity.tap { |o| o.grep = @grep }
end
def globals
grep Globals.new(@opts, pry_instance)
end
def constants
grep Constants.new(@interrogatee, @no_user_opts, @opts, pry_instance)
end
def methods
grep(Methods.new(@interrogatee, @no_user_opts, @opts, pry_instance))
end
def self_methods
grep SelfMethods.new(@interrogatee, @no_user_opts, @opts, pry_instance)
end
def instance_vars
grep InstanceVars.new(@interrogatee, @no_user_opts, @opts, pry_instance)
end
def local_names
grep LocalNames.new(@no_user_opts, @args, pry_instance)
end
def local_vars
LocalVars.new(@opts, pry_instance)
end
def entities
[
globals, constants, methods, self_methods, instance_vars, local_names,
local_vars
]
end
end
end
end
end
@@ -0,0 +1,55 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
class Methods < Pry::Command::Ls::Formatter
include Pry::Command::Ls::Interrogatable
include Pry::Command::Ls::MethodsHelper
def initialize(interrogatee, no_user_opts, opts, pry_instance)
super(pry_instance)
@interrogatee = interrogatee
@no_user_opts = no_user_opts
@default_switch = opts[:methods]
@instance_methods_switch = opts['instance-methods']
@ppp_switch = opts[:ppp]
@jruby_switch = opts['all-java']
@quiet_switch = opts[:quiet]
@verbose_switch = opts[:verbose]
end
def output_self
methods = all_methods.group_by(&:owner)
# Reverse the resolution order so that the most useful information
# appears right by the prompt.
resolution_order.take_while(&below_ceiling).reverse.map do |klass|
methods_here = (methods[klass] || []).select { |m| grep.regexp[m.name] }
heading = "#{Pry::WrappedModule.new(klass).method_prefix}methods"
output_section(heading, format(methods_here))
end.join('')
end
private
def correct_opts?
super || @instance_methods_switch || @ppp_switch || @no_user_opts
end
# Get a lambda that can be used with `take_while` to prevent over-eager
# traversal of the Object's ancestry graph.
def below_ceiling
ceiling = if @quiet_switch
[Pry::Method.safe_send(interrogatee_mod, :ancestors)[1]] +
pry_instance.config.ls.ceiling
elsif @verbose_switch
[]
else
pry_instance.config.ls.ceiling.dup
end
->(klass) { !ceiling.include?(klass) }
end
end
end
end
end
@@ -0,0 +1,50 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
module MethodsHelper
include Pry::Command::Ls::JRubyHacks
private
# Get all the methods that we'll want to output.
def all_methods(instance_methods = false)
methods = if instance_methods || @instance_methods_switch
Pry::Method.all_from_class(@interrogatee)
else
Pry::Method.all_from_obj(@interrogatee)
end
if Pry::Helpers::Platform.jruby? && !@jruby_switch
methods = trim_jruby_aliases(methods)
end
methods.select { |method| @ppp_switch || method.visibility == :public }
end
def resolution_order
if @instance_methods_switch
Pry::Method.instance_resolution_order(@interrogatee)
else
Pry::Method.resolution_order(@interrogatee)
end
end
def format(methods)
methods.sort_by(&:name).map do |method|
if method.name == 'method_missing'
color(:method_missing, 'method_missing')
elsif method.visibility == :private
color(:private_method, method.name)
elsif method.visibility == :protected
color(:protected_method, method.name)
else
color(:public_method, method.name)
end
end
end
end
end
end
end
@@ -0,0 +1,34 @@
# frozen_string_literal: true
class Pry
class Command
class Ls < Pry::ClassCommand
class SelfMethods < Pry::Command::Ls::Formatter
include Pry::Command::Ls::Interrogatable
include Pry::Command::Ls::MethodsHelper
def initialize(interrogatee, no_user_opts, opts, pry_instance)
super(pry_instance)
@interrogatee = interrogatee
@no_user_opts = no_user_opts
@ppp_switch = opts[:ppp]
@jruby_switch = opts['all-java']
end
def output_self
methods = all_methods(true).select do |m|
m.owner == @interrogatee && grep.regexp[m.name]
end
heading = "#{Pry::WrappedModule.new(@interrogatee).method_prefix}methods"
output_section(heading, format(methods))
end
private
def correct_opts?
@no_user_opts && interrogating_a_module?
end
end
end
end
end
@@ -0,0 +1,29 @@
# frozen_string_literal: true
class Pry
class Command
class Nesting < Pry::ClassCommand
match 'nesting'
group 'Navigating Pry'
description 'Show nesting information.'
banner <<-'BANNER'
Show nesting information.
BANNER
def process
output.puts 'Nesting status:'
output.puts '--'
pry_instance.binding_stack.each_with_index do |obj, level|
if level == 0
output.puts "#{level}. #{Pry.view_clip(obj.eval('self'))} (Pry top level)"
else
output.puts "#{level}. #{Pry.view_clip(obj.eval('self'))}"
end
end
end
end
Pry::Commands.add_command(Pry::Command::Nesting)
end
end
@@ -0,0 +1,113 @@
# frozen_string_literal: true
class Pry
class Command
class Play < Pry::ClassCommand
match 'play'
group 'Editing'
description 'Playback a string variable, method, line, or file as input.'
banner <<-'BANNER'
Usage: play [OPTIONS] [--help]
The play command enables you to replay code from files and methods as if they
were entered directly in the Pry REPL.
play --lines 149..153 # assumes current context
play -i 20 --lines 1..3 # assumes lines of the input expression at 20
play -o 4 # the output of an expression at 4
play Pry#repl -l 1..-1 # play the contents of Pry#repl method
play -e 2 # play from specified line until end of valid expression
play hello.rb # play a file
play Rakefile -l 5 # play line 5 of a file
play -d hi # play documentation of hi method
play hi --open # play hi method and leave it open
https://github.com/pry/pry/wiki/User-Input#wiki-Play
BANNER
def options(opt)
CodeCollector.inject_options(opt)
opt.on :open, 'Plays the selected content except the last line. Useful' \
' for replaying methods and leaving the method definition' \
' "open". `amend-line` can then be used to' \
' modify the method.'
opt.on :e, :expression=, 'Executes until end of valid expression', as: Integer
opt.on :p, :print, 'Prints executed code'
end
def process
@cc = CodeCollector.new(args, opts, pry_instance)
perform_play
show_input
end
def perform_play
eval_string << content_after_options
run "fix-indent"
end
def show_input
return unless opts.present?(:print)
return unless Pry::Code.complete_expression?(eval_string)
run 'show-input'
end
def content_after_options
if opts.present?(:open)
restrict_to_lines(content, (0..-2))
elsif opts.present?(:expression)
content_at_expression
else
content
end
end
def content_at_expression
code_object.expression_at(opts[:expression])
end
def code_object
Pry::Code.new(content)
end
def should_use_default_file?
!args.first && !opts.present?(:in) && !opts.present?(:out)
end
def content
if should_use_default_file?
file_content
else
@cc.content
end
end
# The file to play from when no code object is specified.
# e.g `play --lines 4..10`
def default_file
file =
if target.respond_to?(:source_location)
target.source_location.first
else
target.eval("__FILE__")
end
file && File.expand_path(file)
end
def file_content
if !default_file || !File.exist?(default_file)
raise CommandError, "File does not exist! File was: #{default_file.inspect}"
end
@cc.restrict_to_lines(File.read(default_file), @cc.line_range)
end
end
Pry::Commands.add_command(Pry::Command::Play)
end
end
@@ -0,0 +1,30 @@
# frozen_string_literal: true
class Pry
class Command
class PryBacktrace < Pry::ClassCommand
match 'pry-backtrace'
group 'Context'
description 'Show the backtrace for the Pry session.'
banner <<-BANNER
Usage: pry-backtrace [OPTIONS] [--help]
Show the backtrace for the position in the code where Pry was started. This can
be used to infer the behavior of the program immediately before it entered Pry,
just like the backtrace property of an exception.
NOTE: if you are looking for the backtrace of the most recent exception raised,
just type: `_ex_.backtrace` instead.
See: https://github.com/pry/pry/wiki/Special-Locals
BANNER
def process
text = "#{bold('Backtrace:')}\n--\n#{pry_instance.backtrace.join("\n")}"
pry_instance.pager.page(text)
end
end
Pry::Commands.add_command(Pry::Command::PryBacktrace)
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class Pry
class Command
class Version < Pry::ClassCommand
match 'pry-version'
group 'Misc'
description 'Show Pry version.'
banner <<-'BANNER'
Show Pry version.
BANNER
def process
output.puts "Pry version: #{Pry::VERSION} on Ruby #{RUBY_VERSION}."
end
end
Pry::Commands.add_command(Pry::Command::Version)
end
end
@@ -0,0 +1,38 @@
# frozen_string_literal: true
class Pry
# N.B. using a regular expression here so that "raise-up 'foo'" does the right thing.
class Command
class RaiseUp < Pry::ClassCommand
match(/raise-up(!?\b.*)/)
group 'Context'
description 'Raise an exception out of the current pry instance.'
command_options listing: 'raise-up'
banner <<-BANNER
Raise up, like exit, allows you to quit pry. Instead of returning a value
however, it raises an exception. If you don't provide the exception to be
raised, it will use the most recent exception (in pry `_ex_`).
When called as raise-up! (with an exclamation mark), this command raises the
exception through any nested prys you have created by "cd"ing into objects.
raise-up "get-me-out-of-here"
# This is equivalent to the command above.
raise "get-me-out-of-here"
raise-up
BANNER
def process
return _pry.pager.page help if captures[0] =~ /(-h|--help)\b/
# Handle 'raise-up', 'raise-up "foo"', 'raise-up RuntimeError, 'farble'
# in a rubyesque manner
target.eval("pry_instance.raise_up#{captures[0]}")
end
end
Pry::Commands.add_command(Pry::Command::RaiseUp)
end
end
@@ -0,0 +1,74 @@
# frozen_string_literal: true
class Pry
class Command
class ReloadCode < Pry::ClassCommand
match 'reload-code'
group 'Misc'
description 'Reload the source file that contains the specified code object.'
banner <<-'BANNER'
Reload the source file that contains the specified code object.
e.g reload-code MyClass#my_method #=> reload a method
reload-code MyClass #=> reload a class
reload-code my-command #=> reload a pry command
reload-code self #=> reload the current object
reload-code #=> reload the current file or object
BANNER
def process
if !args.empty?
reload_object(args.join(" "))
elsif internal_binding?(target)
reload_object("self")
else
reload_current_file
end
end
private
def current_file
file =
if target.respond_to?(:source_location)
target.source_location.first
else
target.eval("__FILE__")
end
File.expand_path file
end
def reload_current_file
unless File.exist?(current_file)
raise CommandError, "Current file: #{current_file} cannot be found on disk!"
end
load current_file
output.puts "The current file: #{current_file} was reloaded!"
end
def reload_object(identifier)
code_object = Pry::CodeObject.lookup(identifier, pry_instance)
check_for_reloadability(code_object, identifier)
load code_object.source_file
output.puts "#{identifier} was reloaded!"
end
def check_for_reloadability(code_object, identifier)
if !code_object || !code_object.source_file
raise CommandError, "Cannot locate #{identifier}!"
end
return if File.exist?(code_object.source_file)
raise CommandError,
"Cannot reload #{identifier} as it has no associated file on disk. " \
"File found was: #{code_object.source_file}"
end
end
Pry::Commands.add_command(Pry::Command::ReloadCode)
Pry::Commands.alias_command 'reload-method', 'reload-code'
end
end
@@ -0,0 +1,22 @@
# frozen_string_literal: true
class Pry
class Command
class Reset < Pry::ClassCommand
match 'reset'
group 'Context'
description 'Reset the REPL to a clean state.'
banner <<-'BANNER'
Reset the REPL to a clean state.
BANNER
def process
output.puts 'Pry reset.'
exec 'pry'
end
end
Pry::Commands.add_command(Pry::Command::Reset)
end
end
@@ -0,0 +1,75 @@
# frozen_string_literal: true
require 'stringio'
class Pry
class Command
class Ri < Pry::ClassCommand
match 'ri'
group 'Introspection'
description 'View ri documentation.'
banner <<-'BANNER'
Usage: ri [spec]
View ri documentation. Relies on the "rdoc" gem being installed.
See also "show-doc" command.
ri Array#each
BANNER
def process(spec)
unless spec
return output.puts(
"Please provide a class, module, or method name (e.g: ri Array#push)"
)
end
# Lazily load RI
require 'rdoc/ri/driver'
unless defined? RDoc::RI::PryDriver
# Subclass RI so that it formats its output nicely, and uses `lesspipe`.
subclass = Class.new(RDoc::RI::Driver) # the hard way.
subclass.class_eval do
def initialize(pager, opts)
@pager = pager
super opts
end
def page
paging_text = StringIO.new
yield paging_text
@pager.page(paging_text.string)
end
def formatter(_io)
if @formatter_klass
@formatter_klass.new
else
RDoc::Markup::ToAnsi.new
end
end
end
RDoc::RI.const_set :PryDriver, subclass # hook it up!
end
# Spin-up an RI instance.
ri = RDoc::RI::PryDriver.new(
pry_instance.pager, use_stdout: true, interactive: false
)
begin
ri.display_names [spec] # Get the documentation (finally!)
rescue RDoc::RI::Driver::NotFoundError => e
output.puts "error: '#{e.name}' not found"
end
end
end
Pry::Commands.add_command(Pry::Command::Ri)
end
end
@@ -0,0 +1,63 @@
# frozen_string_literal: true
class Pry
class Command
class SaveFile < Pry::ClassCommand
match 'save-file'
group 'Input and Output'
description 'Export to a file using content from the REPL.'
banner <<-'BANNER'
Usage: save-file [OPTIONS] --to [FILE]
Export to a file using content from the REPL.
save-file my_method --to hello.rb
save-file -i 1..10 --to hello.rb --append
save-file show-method --to my_command.rb
save-file sample_file.rb --lines 2..10 --to output_file.rb
BANNER
def options(opt)
CodeCollector.inject_options(opt)
opt.on :to=, "Specify the output file path"
opt.on :a, :append, "Append output to file"
end
def process
@cc = CodeCollector.new(args, opts, pry_instance)
raise CommandError, "Found no code to save." if @cc.content.empty?
if !file_name
display_content
else
save_file
end
end
def file_name
opts[:to] || nil
end
def save_file
File.open(file_name, mode) do |f|
f.puts @cc.content
end
output.puts "#{file_name} successfully saved"
end
def display_content
output.puts @cc.content
output.puts "\n\n--\nPlease use `--to FILE` to export to a file."
output.puts "No file saved!\n--"
end
def mode
opts.present?(:append) ? "a" : "w"
end
end
Pry::Commands.add_command(Pry::Command::SaveFile)
end
end
@@ -0,0 +1,75 @@
# frozen_string_literal: true
class Pry
class Command
class ShellCommand < Pry::ClassCommand
match(/\.(.*)/)
group 'Input and Output'
description "All text following a '.' is forwarded to the shell."
command_options listing: '.<shell command>', use_prefix: false,
takes_block: true, state: %i[old_pwd]
banner <<-'BANNER'
Usage: .COMMAND_NAME
All text following a "." is forwarded to the shell.
.ls -aF
.uname
BANNER
def process(cmd)
if cmd =~ /^cd\s*(.*)/i
process_cd parse_destination(Regexp.last_match(1))
else
pass_block(cmd)
if command_block
command_block.call `#{cmd}`
else
pry_instance.config.system.call(output, cmd, pry_instance)
end
end
end
private
def parse_destination(dest)
return "~" if dest.empty?
return dest unless dest == "-"
state.old_pwd || raise(CommandError, "No prior directory available")
end
def process_cd(dest)
state.old_pwd = Dir.pwd
Dir.chdir(File.expand_path(path_from_cd_path(dest) || dest))
rescue Errno::ENOENT
raise CommandError, "No such directory: #{dest}"
end
def cd_path_env
Pry::Env['CDPATH']
end
def cd_path_exists?
cd_path_env && cd_path_env.length.nonzero?
end
def path_from_cd_path(dest)
return if !(dest && cd_path_exists?) || special_case_path?(dest)
cd_path_env.split(File::PATH_SEPARATOR).each do |path|
return path if File.directory?(path) && path.split(File::SEPARATOR).last == dest
end
nil
end
def special_case_path?(dest)
['.', '..', '-'].include?(dest) || dest =~ /\A[#{File::PATH_SEPARATOR}~]/
end
end
Pry::Commands.add_command(Pry::Command::ShellCommand)
end
end
@@ -0,0 +1,30 @@
# frozen_string_literal: true
class Pry
class Command
class ShellMode < Pry::ClassCommand
match 'shell-mode'
group 'Input and Output'
description 'Toggle shell mode. Bring in pwd prompt and file completion.'
command_options state: %i[disabled prev_prompt]
banner <<-'BANNER'
Toggle shell mode. Bring in pwd prompt and file completion.
BANNER
def process
state.disabled ^= true
if state.disabled
state.prev_prompt = pry_instance.prompt
pry_instance.prompt = Pry::Prompt[:shell]
else
pry_instance.prompt = state.prev_prompt
end
end
end
Pry::Commands.add_command(Pry::Command::ShellMode)
Pry::Commands.alias_command 'file-mode', 'shell-mode'
end
end
@@ -0,0 +1,93 @@
# frozen_string_literal: true
class Pry
class Command
class ShowDoc < Command::ShowInfo
include Pry::Helpers::DocumentationHelpers
match 'show-doc'
group 'Introspection'
description 'Show the documentation for a method or class.'
banner <<-BANNER
Usage: show-doc [OPTIONS] [METH]
Aliases: ?
Show the documentation for a method or class. Tries instance methods first and
then methods by default.
show-doc hi_method # docs for hi_method
show-doc Pry # for Pry class
show-doc Pry -a # for all definitions of Pry class (all monkey patches)
BANNER
def process
super
output.puts(
"\nWARNING: the show-doc command is deprecated. It will be removed " \
"from future Pry versions.\nPlease use 'show-source' with the -d " \
"(or --doc) switch instead\nExample: show-source #{obj_name} -d"
)
end
# The docs for code_object prepared for display.
def content_for(code_object)
Code.new(
render_doc_markup_for(code_object),
start_line_for(code_object),
:text
).with_line_numbers(use_line_numbers?).to_s
end
# process the markup (if necessary) and apply colors
def render_doc_markup_for(code_object)
docs = docs_for(code_object)
if code_object.command?
# command '--help' shouldn't use markup highlighting
docs
else
if docs.empty?
raise CommandError, "No docs found for: #{obj_name || 'current context'}"
end
process_comment_markup(docs)
end
end
# Return docs for the code_object, adjusting for whether the code_object
# has yard docs available, in which case it returns those.
# (note we only have to check yard docs for modules since they can
# have multiple docs, but methods can only be doc'd once so we
# dont need to check them)
def docs_for(code_object)
if code_object.module_with_yard_docs?
# yard docs
code_object.yard_doc
else
# normal docs (i.e comments above method/module/command)
code_object.doc
end
end
# Which sections to include in the 'header', can toggle: :owner,
# :signature and visibility.
def header_options
super.merge signature: true
end
# figure out start line of docs by back-calculating based on
# number of lines in the comment and the start line of the code_object
# @return [Fixnum] start line of docs
def start_line_for(code_object)
return 1 if code_object.command? || opts.present?(:'base-one')
return 1 unless code_object.source_line
code_object.source_line - code_object.doc.lines.count
end
end
Pry::Commands.add_command(Pry::Command::ShowDoc)
end
end
@@ -0,0 +1,234 @@
# frozen_string_literal: true
class Pry
class Command
class ShowInfo < Pry::ClassCommand
extend Pry::Helpers::BaseHelpers
command_options shellwords: false, interpolate: false
def initialize(*)
super
@used_super = nil
end
def options(opt)
opt.on :s, :super, "Select the 'super' method. Can be repeated to " \
"traverse the ancestors", as: :count
opt.on :l, "line-numbers", "Show line numbers"
opt.on :b, "base-one", "Show line numbers but start numbering at 1 " \
"(useful for `amend-line` and `play` commands)"
opt.on :a, :all, "Show all definitions and monkeypatches of the " \
"module/class"
end
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
# This method checks whether the `code_object` is a WrappedModule, if it
# is, then it returns the first candidate (monkeypatch) with accessible
# source (or docs). If `code_object` is not a WrappedModule (i.e a method
# or a command) then the `code_object` itself is just returned.
#
# @return [Pry::WrappedModule, Pry::Method, Pry::Command]
def code_object_with_accessible_source(code_object)
return code_object unless code_object.is_a?(WrappedModule)
candidate = code_object.candidates.find(&:source)
return candidate if candidate
raise CommandError, no_definition_message unless valid_superclass?(code_object)
@used_super = true
code_object_with_accessible_source(code_object.super)
end
def valid_superclass?(code_object)
code_object.super && code_object.super.wrapped != Object
end
def content_and_header_for_code_object(code_object)
header(code_object) + content_for(code_object)
end
def content_and_headers_for_all_module_candidates(mod)
result = "Found #{mod.number_of_candidates} candidates for " \
"`#{mod.name}` definition:\n"
mod.number_of_candidates.times do |v|
candidate = mod.candidate(v)
begin
result += "\nCandidate #{v + 1}/#{mod.number_of_candidates}: " \
"#{candidate.source_file}:#{candidate.source_line}\n"
content = content_for(candidate)
result += "Number of lines: #{content.lines.count}\n\n" + content
rescue Pry::RescuableException
result += "\nNo content found.\n"
next
end
end
result
end
def no_definition_message
"Couldn't locate a definition for #{obj_name}"
end
# Generate a header (meta-data information) for all the code
# object types: methods, modules, commands, procs...
def header(code_object)
file_name, line_num = file_and_line_for(code_object)
content = content_for(code_object)
h = "\n#{bold('From:')} #{file_name}"
h += code_object_header(code_object, line_num)
h += "\n#{bold('Number of lines:')} " + "#{content.lines.count}\n\n"
if @used_super
h += bold('** Warning:')
h += " Cannot find code for #{@original_code_object.nonblank_name}. " \
"Showing superclass #{code_object.nonblank_name} instead. **\n\n"
end
if content.lines.none?
h += bold('** Warning:')
h += " Cannot find code for '#{code_object.name}' (source_location is nil)"
end
h
end
def code_object_header(code_object, line_num)
if code_object.real_method_object?
method_header(code_object, line_num)
# It sucks we have to test for both Pry::WrappedModule and
# WrappedModule::Candidate, probably indicates a deep refactor needs
# to happen in those classes.
elsif code_object.is_a?(Pry::WrappedModule) ||
code_object.is_a?(Pry::WrappedModule::Candidate)
module_header(code_object, line_num)
else
""
end
end
def method_header(code_object, line_num)
h = ""
h += (code_object.c_method? ? ' (C Method):' : ":#{line_num}:")
h += method_sections(code_object)[:owner]
h += method_sections(code_object)[:visibility]
h += method_sections(code_object)[:signature]
h
end
def module_header(code_object, line_num)
h = ""
h += ":#{line_num}\n"
h += bold(code_object.module? ? "Module" : "Class")
h += " #{bold('name:')} #{code_object.nonblank_name}"
if code_object.number_of_candidates > 1
h += bold("\nNumber of monkeypatches: ")
h += code_object.number_of_candidates.to_s
h += ". Use the `-a` option to display all available monkeypatches"
end
h
end
def method_sections(code_object)
{
owner: "\n#{bold('Owner:')} #{code_object.owner || 'N/A'}\n",
visibility: "#{bold('Visibility:')} #{code_object.visibility}",
signature: "\n#{bold('Signature:')} #{code_object.signature}"
}.merge(header_options) { |_key, old, new| (new && old).to_s }
end
def header_options
{
owner: true,
visibility: true,
signature: nil
}
end
def show_all_modules?(code_object)
code_object.is_a?(Pry::WrappedModule) && opts.present?(:all)
end
def obj_name
@obj_name ||= args.empty? ? nil : args.join(' ')
end
def use_line_numbers?
opts.present?(:b) || opts.present?(:l)
end
def start_line_for(code_object)
if opts.present?(:'base-one')
1
else
code_object.source_line || 1
end
end
# takes into account possible yard docs, and returns yard_file / yard_line
# Also adjusts for start line of comments (using start_line_for), which it
# has to infer by subtracting number of lines of comment from start line
# of code_object
def file_and_line_for(code_object)
if code_object.module_with_yard_docs?
[code_object.yard_file, code_object.yard_line]
else
[code_object.source_file, start_line_for(code_object)]
end
end
def complete(input)
if input =~ /([^ ]*)#([a-z0-9_]*)\z/
prefix = Regexp.last_match(1)
search = Regexp.last_match(2)
methods =
begin
# rubocop:disable Security/Eval
Pry::Method.all_from_class(binding.eval(prefix))
# rubocop:enable Security/Eval
rescue RescuableException
return super
end
methods.map do |method|
[prefix, method.name].join('#') if method.name.start_with?(search)
end.compact
else
super
end
end
end
end
end
@@ -0,0 +1,22 @@
# frozen_string_literal: true
class Pry
class Command
class ShowInput < Pry::ClassCommand
match 'show-input'
group 'Editing'
description 'Show the contents of the input buffer for the current ' \
'multi-line expression.'
banner <<-'BANNER'
Show the contents of the input buffer for the current multi-line expression.
BANNER
def process
output.puts Code.new(eval_string).with_line_numbers
end
end
Pry::Commands.add_command(Pry::Command::ShowInput)
end
end
@@ -0,0 +1,118 @@
# frozen_string_literal: true
class Pry
class Command
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 hi_method
show-source hi_method
show-source Pry#rep # source for Pry#rep method
show-source Pry # for Pry class
show-source Pry -a # for all Pry class definitions (all monkey patches)
show-source Pry.foo -e # for class of the return value of expression `Pry.foo`
show-source Pry --super # for superclass of Pry (Object class)
show-source Pry -d # include documentation
https://github.com/pry/pry/wiki/Source-browsing#wiki-Show_method
BANNER
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
# The source for code_object prepared for display.
def content_for(code_object)
content = ''
if opts.present?(:d)
code = Code.new(
render_doc_markup_for(code_object), start_line_for(code_object), :text
)
content += code.with_line_numbers(use_line_numbers?).to_s
content += "\n"
end
code = Code.new(
code_object.source || [], start_line_for(code_object)
)
content += code.with_line_numbers(use_line_numbers?).highlighted
content
end
# process the markup (if necessary) and apply colors
def render_doc_markup_for(code_object)
docs = docs_for(code_object)
if code_object.command?
# command '--help' shouldn't use markup highlighting
docs
else
if docs.empty?
raise CommandError, "No docs found for: #{obj_name || 'current context'}"
end
process_comment_markup(docs)
end
end
# Return docs for the code_object, adjusting for whether the code_object
# has yard docs available, in which case it returns those.
# (note we only have to check yard docs for modules since they can
# have multiple docs, but methods can only be doc'd once so we
# dont need to check them)
def docs_for(code_object)
if code_object.module_with_yard_docs?
# yard docs
code_object.yard_doc
else
# normal docs (i.e comments above method/module/command)
code_object.doc
end
end
# Which sections to include in the 'header', can toggle: :owner,
# :signature and visibility.
def header_options
super.merge signature: true
end
# figure out start line of docs by back-calculating based on
# number of lines in the comment and the start line of the code_object
# @return [Fixnum] start line of docs
def start_line_for(code_object)
return 1 if code_object.command? || opts.present?(:'base-one')
return 1 unless code_object.source_line
code_object.source_line - code_object.doc.lines.count
end
end
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'
end
end
@@ -0,0 +1,44 @@
# frozen_string_literal: true
class Pry
class Command
class Stat < Pry::ClassCommand
match 'stat'
group 'Introspection'
description 'View method information and set _file_ and _dir_ locals.'
command_options shellwords: false
banner <<-'BANNER'
Usage: stat [OPTIONS] [METH]
Show method information for method METH and set _file_ and _dir_ locals.
stat hello_method
BANNER
def options(opt)
method_options(opt)
end
def process
meth = method_object
aliases = meth.aliases
output.puts(unindent(<<-OUTPUT))
Method Information:
--
Name: #{meth.name}
Alias#{'es' if aliases.length > 1}: #{aliases.any? ? aliases.join(', ') : 'None.'}
Owner: #{meth.owner || 'Unknown'}
Visibility: #{meth.visibility}
Type: #{meth.is_a?(::Method) ? 'Bound' : 'Unbound'}
Arity: #{meth.arity}
Method Signature: #{meth.signature}
Source Location: #{meth.source_location ? meth.source_location.join(':') : 'Not found.'}
OUTPUT
end
end
Pry::Commands.add_command(Pry::Command::Stat)
end
end
@@ -0,0 +1,29 @@
# frozen_string_literal: true
class Pry
class Command
class SwitchTo < Pry::ClassCommand
match 'switch-to'
group 'Navigating Pry'
description 'Start a new subsession on a binding in the current stack.'
banner <<-'BANNER'
Start a new subsession on a binding in the current stack (numbered by nesting).
BANNER
def process(selection)
selection = selection.to_i
if selection < 0 || selection > pry_instance.binding_stack.size - 1
raise CommandError,
"Invalid binding index #{selection} - use `nesting` command " \
"to view valid indices."
else
Pry.start(pry_instance.binding_stack[selection])
end
end
end
Pry::Commands.add_command(Pry::Command::SwitchTo)
end
end
@@ -0,0 +1,28 @@
# frozen_string_literal: true
class Pry
class Command
class ToggleColor < Pry::ClassCommand
match 'toggle-color'
group 'Misc'
description 'Toggle syntax highlighting.'
banner <<-'BANNER'
Usage: toggle-color
Toggle syntax highlighting.
BANNER
def process
pry_instance.color = color_toggle
output.puts "Syntax highlighting #{pry_instance.color ? 'on' : 'off'}"
end
def color_toggle
!pry_instance.color
end
Pry::Commands.add_command(self)
end
end
end
@@ -0,0 +1,108 @@
# frozen_string_literal: true
class Pry
class Command
class WatchExpression < Pry::ClassCommand
match 'watch'
group 'Context'
description 'Watch the value of an expression and print a notification ' \
'whenever it changes.'
command_options use_prefix: false, state: %i[watch_expressions]
banner <<-'BANNER'
Usage: watch [EXPRESSION]
watch
watch --delete [INDEX]
watch [EXPRESSION] adds an expression to the list of those being watched.
It will be re-evaluated every time you hit enter in pry. If its value has
changed, the new value will be printed to the console.
This is useful if you are step-through debugging and want to see how
something changes over time. It's also useful if you're trying to write
a method inside pry and want to check that it gives the right answers
every time you redefine it.
watch on its own displays all the currently watched expressions and their
values, and watch --delete [INDEX] allows you to delete expressions from
the list being watched.
BANNER
def options(opt)
opt.on :d, :delete,
"Delete the watch expression with the given index. If no index " \
"is given; clear all watch expressions.",
optional_argument: true, as: Integer
opt.on :l, :list,
"Show all current watch expressions and their values. Calling " \
"watch with no expressions or options will also show the watch " \
"expressions."
end
def process
if opts.present?(:delete)
delete opts[:delete]
elsif opts.present?(:list) || args.empty?
list
else
add_hook
add_expression
end
end
private
def expressions
state.watch_expressions ||= []
end
def delete(index)
if index
output.puts "Deleting watch expression ##{index}: #{expressions[index - 1]}"
expressions.delete_at(index - 1)
else
output.puts "Deleting all watched expressions"
expressions.clear
end
end
def list
if expressions.empty?
output.puts "No watched expressions"
else
pry_instance.pager.open do |pager|
pager.puts "Listing all watched expressions:"
pager.puts ""
expressions.each_with_index do |expr, index|
pager.print with_line_numbers(expr.to_s, index + 1)
end
pager.puts ""
end
end
end
def eval_and_print_changed(output)
expressions.each do |expr|
expr.eval!
output.puts "#{blue 'watch'}: #{expr}" if expr.changed?
end
end
def add_expression
expressions << Expression.new(pry_instance, target, arg_string)
output.puts "Watching #{Code.new(arg_string).highlighted}"
end
def add_hook
hook = %i[after_eval watch_expression]
return if pry_instance.hooks.hook_exists?(*hook)
pry_instance.hooks.add_hook(*hook) do |_, pry_instance|
eval_and_print_changed pry_instance.output
end
end
end
Pry::Commands.add_command(Pry::Command::WatchExpression)
end
end
@@ -0,0 +1,43 @@
# frozen_string_literal: true
class Pry
class Command
class WatchExpression
class Expression
attr_reader :target, :source, :value, :previous_value, :pry_instance
def initialize(pry_instance, target, source)
@pry_instance = pry_instance
@target = target
@source = Code.new(source).strip
end
def eval!
@previous_value = value
@value = Pry::ColorPrinter.pp(target_eval(target, source), ''.dup)
end
def to_s
"#{Code.new(source).highlighted.strip} => #{value}"
end
# Has the value of the expression changed?
#
# We use the pretty-printed string representation to detect differences
# as this avoids problems with dup (causes too many differences) and ==
# (causes too few)
def changed?
(value != previous_value)
end
private
def target_eval(target, source)
target.eval(source)
rescue StandardError => e
e
end
end
end
end
end
@@ -0,0 +1,205 @@
# frozen_string_literal: true
require 'method_source'
class Pry
class Command
class Whereami < Pry::ClassCommand
def initialize(*)
super
@method_code = nil
end
class << self
attr_accessor :method_size_cutoff
end
@method_size_cutoff = 30
match 'whereami'
description 'Show code surrounding the current context.'
group 'Context'
banner <<-'BANNER'
Usage: whereami [-qn] [LINES]
Describe the current location. If you use `binding.pry` inside a method then
whereami will print out the source for that method.
If a number is passed, then LINES lines before and after the current line will be
shown instead of the method itself.
The `-q` flag can be used to suppress error messages in the case that there's
no code to show. This is used by pry in the default before_session hook to show
you when you arrive at a `binding.pry`.
The `-n` flag can be used to hide line numbers so that code can be copy/pasted
effectively.
When pry was started on an Object and there is no associated method, whereami
will instead output a brief description of the current object.
BANNER
def setup
if target.respond_to?(:source_location)
file, @line = target.source_location
@file = expand_path(file)
else
@file = expand_path(target.eval('__FILE__'))
@line = target.eval('__LINE__')
end
@method = Pry::Method.from_binding(target)
end
def options(opt)
opt.on :q, :quiet, "Don't display anything in case of an error"
opt.on :n, :"no-line-numbers", "Do not display line numbers"
opt.on :m, :method, "Show the complete source for the current method."
opt.on :c, :class, "Show the complete source for the current class or module."
opt.on :f, :file, "Show the complete source for the current file."
end
def code
@code ||= if opts.present?(:m)
method_code || raise(CommandError, "Cannot find method code.")
elsif opts.present?(:c)
class_code || raise(CommandError, "Cannot find class code.")
elsif opts.present?(:f)
Pry::Code.from_file(@file)
elsif args.any?
code_window
else
default_code
end
end
def code?
!!code
rescue MethodSource::SourceNotFoundError
false
end
def bad_option_combination?
[opts.present?(:m), opts.present?(:f),
opts.present?(:c), args.any?].count(true) > 1
end
def location
"#{@file}:#{@line} #{@method && @method.name_with_owner}"
end
def process
if bad_option_combination?
raise CommandError, "Only one of -m, -c, -f, and LINES may be specified."
end
return if nothing_to_do?
if internal_binding?(target)
handle_internal_binding
return
end
set_file_and_dir_locals(@file)
pretty_code = code.with_line_numbers(use_line_numbers?)
.with_marker(marker)
.highlighted
pry_instance.pager.page(
"\n#{bold('From:')} #{location}:\n\n" + pretty_code + "\n"
)
end
private
def nothing_to_do?
opts.quiet? && (internal_binding?(target) || !code?)
end
def use_line_numbers?
!opts.present?(:n)
end
def marker
!opts.present?(:n) && @line
end
def top_level?
target_self == Pry.main
end
def handle_internal_binding
if top_level?
output.puts "At the top level."
else
output.puts "Inside #{Pry.view_clip(target_self)}."
end
end
def small_method?
@method.source_range.count < self.class.method_size_cutoff
end
def default_code
if method_code && small_method?
method_code
else
code_window
end
end
def code_window
Pry::Code.from_file(@file).around(@line, window_size)
end
def method_code
return @method_code if @method_code
@method_code = Pry::Code.from_method(@method) if valid_method?
end
# This either returns the `target_self`
# or it returns the class of `target_self` if `target_self` is not a class.
# @return [Pry::WrappedModule]
def target_class
return Pry::WrappedModule(target_self) if target_self.is_a?(Module)
Pry::WrappedModule(target_self.class)
end
def class_code
@class_code ||=
begin
mod = @method ? Pry::WrappedModule(@method.owner) : target_class
idx = mod.candidates.find_index { |v| expand_path(v.source_file) == @file }
idx && Pry::Code.from_module(mod, idx)
end
end
def valid_method?
@method && @method.source? && expand_path(@method.source_file) == @file &&
@method.source_range.include?(@line)
end
def expand_path(filename)
return unless filename
return filename if Pry.eval_path == filename
File.expand_path(filename)
end
def window_size
if args.empty?
pry_instance.config.default_window_size
else
args.first.to_i
end
end
end
Pry::Commands.add_command(Pry::Command::Whereami)
Pry::Commands.alias_command '@', 'whereami'
Pry::Commands.alias_command(/whereami[!?]+/, 'whereami')
end
end
@@ -0,0 +1,95 @@
# frozen_string_literal: true
class Pry
class Command
class Wtf < Pry::ClassCommand
match(/wtf([?!]*)/)
group 'Context'
description 'Show the backtrace of the most recent exception.'
options listing: 'wtf?'
banner <<-'BANNER'
Usage: wtf[?|!]
Shows a few lines of the backtrace of the most recent exception (also available
as `_ex_.backtrace`). If you want to see more lines, add more question marks or
exclamation marks.
wtf?
wtf?!???!?!?
# To see the entire backtrace, pass the `-v` or `--verbose` flag.
wtf -v
BANNER
RUBY_FRAME_PATTERN = /\A(?<file>(.+)):(?<line>(\d+))/.freeze
def options(opt)
opt.on :v, :verbose, "Show the full backtrace"
opt.on :c, :code, "Show code corresponding to the backtrace frame"
end
def process
unless pry_instance.last_exception
raise Pry::CommandError, "No most-recent exception"
end
text = ''.dup
unwind_exceptions.each_with_index do |exception, i|
title = (i == 0 ? 'Exception' : 'Caused by')
text << format_header(title, exception)
text << format_backtrace(exception.backtrace)
end
output.puts(text)
end
private
def unwind_exceptions
exception_list = []
exception = pry_instance.last_exception
while exception
exception_list << exception
exception = (exception.cause if exception.respond_to?(:cause))
end
exception_list
end
def format_header(title, exception)
"#{bold(title + ':')} #{exception.class}: #{exception}\n--\n"
end
def format_backtrace(backtrace)
lines = trim_backtrace(backtrace).map do |frame|
next frame unless opts.code?
match = frame.match(RUBY_FRAME_PATTERN)
code = read_line(match[:file], match[:line].to_i)
[bold(frame), code].join("\n")
end
Pry::Code.new(lines.compact, 0, :text).with_line_numbers.to_s
end
def trim_backtrace(backtrace)
return backtrace if opts.verbose?
size_of_backtrace = [captures[0].size, 0.5].max * 10
backtrace.first(size_of_backtrace)
end
def read_line(file, line)
File.open(file, 'r') do |f|
(line - 1).times { f.gets }
f.gets
end
rescue Errno::ENOENT
nil
end
end
Pry::Commands.add_command(Pry::Command::Wtf)
end
end