diff --git a/lib/irb.rb b/lib/irb.rb index a08aa874c9..a41d4e13be 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -10,6 +10,7 @@ # # require "e2mmap" +require "ripper" require "irb/init" require "irb/context" @@ -410,6 +411,35 @@ module IRB end class Irb + ASSIGNMENT_NODE_TYPES = [ + # Local, instance, global, class, constant, instance, and index assignment: + # "foo = bar", + # "@foo = bar", + # "$foo = bar", + # "@@foo = bar", + # "::Foo = bar", + # "a::Foo = bar", + # "Foo = bar" + # "foo.bar = 1" + # "foo[1] = bar" + :assign, + + # Operation assignment: + # "foo += bar" + # "foo -= bar" + # "foo ||= bar" + # "foo &&= bar" + :opassign, + + # Multiple assignment: + # "foo, bar = 1, 2 + :massign, + ] + # Note: instance and index assignment expressions could also be written like: + # "foo.bar=(1)" and "foo.[]=(1, bar)", when expressed that way, the former + # be parsed as :assign and echo will be suppressed, but the latter is + # parsed as a :method_add_arg and the output won't be suppressed + # Creates a new irb session def initialize(workspace = nil, input_method = nil, output_method = nil) @context = Context.new(self, workspace, input_method, output_method) @@ -498,7 +528,7 @@ module IRB begin line.untaint @context.evaluate(line, line_no, exception: exc) - output_value if @context.echo? + output_value if @context.echo? && (@context.echo_on_assignment? || !assignment_expression?(line)) rescue Interrupt => exc rescue SystemExit, SignalException raise @@ -717,6 +747,18 @@ module IRB format("#<%s: %s>", self.class, ary.join(", ")) end + def assignment_expression?(line) + # Try to parse the line and check if the last of possibly multiple + # expressions is an assignment type. + + # If the expression is invalid, Ripper.sexp should return nil which will + # result in false being returned. Any valid expression should return an + # s-expression where the second selement of the top level array is an + # array of parsed expressions. The first element of each expression is the + # expression's type. + ASSIGNMENT_NODE_TYPES.include?(Ripper.sexp(line)&.dig(1,-1,0)) + end + ATTR_TTY = "\e[%sm" def ATTR_TTY.[](*a) self % a.join(";"); end ATTR_PLAIN = "" diff --git a/lib/irb/context.rb b/lib/irb/context.rb index 9544a8aa1a..5d2336008f 100644 --- a/lib/irb/context.rb +++ b/lib/irb/context.rb @@ -121,6 +121,11 @@ module IRB if @echo.nil? @echo = true end + + @echo_on_assignment = IRB.conf[:ECHO_ON_ASSIGNMENT] + if @echo_on_assignment.nil? + @echo_on_assignment = false + end end # The top-level workspace, see WorkSpace#main @@ -236,6 +241,15 @@ module IRB # puts "omg" # # omg attr_accessor :echo + # Whether to echo for assignment expressions + # + # Uses IRB.conf[:ECHO_ON_ASSIGNMENT] if available, or defaults to +false+. + # + # a = "omg" + # IRB.CurrentContext.echo_on_assignment = true + # a = "omg" + # #=> omg + attr_accessor :echo_on_assignment # Whether verbose messages are displayed or not. # # A copy of the default IRB.conf[:VERBOSE] @@ -261,6 +275,7 @@ module IRB alias ignore_sigint? ignore_sigint alias ignore_eof? ignore_eof alias echo? echo + alias echo_on_assignment? echo_on_assignment # Returns whether messages are displayed or not. def verbose? diff --git a/lib/irb/init.rb b/lib/irb/init.rb index d7ee885665..5dd0c12c32 100644 --- a/lib/irb/init.rb +++ b/lib/irb/init.rb @@ -51,6 +51,7 @@ module IRB # :nodoc: @CONF[:IGNORE_SIGINT] = true @CONF[:IGNORE_EOF] = false @CONF[:ECHO] = nil + @CONF[:ECHO_ON_ASSIGNMENT] = nil @CONF[:VERBOSE] = nil @CONF[:EVAL_HISTORY] = nil @@ -172,6 +173,10 @@ module IRB # :nodoc: @CONF[:ECHO] = true when "--noecho" @CONF[:ECHO] = false + when "--echo-on-assignment" + @CONF[:ECHO_ON_ASSIGNMENT] = true + when "--noecho-on-assignment" + @CONF[:ECHO_ON_ASSIGNMENT] = false when "--verbose" @CONF[:VERBOSE] = true when "--noverbose" diff --git a/test/irb/test_context.rb b/test/irb/test_context.rb index df9edcc63f..c55de4f84e 100644 --- a/test/irb/test_context.rb +++ b/test/irb/test_context.rb @@ -26,6 +26,10 @@ module TestIRB def encoding Encoding.default_external end + + def reset + @line_no = 0 + end end def setup @@ -84,5 +88,126 @@ module TestIRB def test_default_config assert_equal(true, @context.use_colorize?) end + + def test_assignment_expression + input = TestInputMethod.new + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) + [ + "foo = bar", + "@foo = bar", + "$foo = bar", + "@@foo = bar", + "::Foo = bar", + "a::Foo = bar", + "Foo = bar", + "foo.bar = 1", + "foo[1] = bar", + "foo += bar", + "foo -= bar", + "foo ||= bar", + "foo &&= bar", + "foo, bar = 1, 2", + "foo.bar=(1)", + "foo; foo = bar", + "foo; foo = bar; ;\n ;", + "foo\nfoo = bar", + ].each do |exp| + assert( + irb.assignment_expression?(exp), + "#{exp.inspect}: should be an assignment expression" + ) + end + + [ + "foo", + "foo.bar", + "foo[0]", + "foo = bar; foo", + "foo = bar\nfoo", + ].each do |exp| + refute( + irb.assignment_expression?(exp), + "#{exp.inspect}: should not be an assignment expression" + ) + end + end + + def test_echo_on_assignment + input = TestInputMethod.new([ + "a = 1\n", + "a\n", + "a, b = 2, 3\n", + "a\n", + "b\n", + "b = 4\n", + "_\n" + ]) + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) + + # The default + irb.context.echo = true + irb.context.echo_on_assignment = false + out, err = capture_io do + irb.eval_input + end + assert_empty err + assert_equal("=> 1\n=> 2\n=> 3\n=> 4\n", out) + + # Everything is output, like before echo_on_assignment was introduced + input.reset + irb.context.echo = true + irb.context.echo_on_assignment = true + out, err = capture_io do + irb.eval_input + end + assert_empty err + assert_equal("=> 1\n=> 1\n=> [2, 3]\n=> 2\n=> 3\n=> 4\n=> 4\n", out) + + # Nothing is output when echo is false + input.reset + irb.context.echo = false + irb.context.echo_on_assignment = false + out, err = capture_io do + irb.eval_input + end + assert_empty err + assert_equal("", out) + + # Nothing is output when echo is false even if echo_on_assignment is true + input.reset + irb.context.echo = false + irb.context.echo_on_assignment = true + out, err = capture_io do + irb.eval_input + end + assert_empty err + assert_equal("", out) + end + + def test_echo_on_assignment_conf + # Default + IRB.conf[:ECHO] = nil + IRB.conf[:ECHO_ON_ASSIGNMENT] = nil + input = TestInputMethod.new() + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) + + assert(irb.context.echo?, "echo? should be true by default") + refute(irb.context.echo_on_assignment?, "echo_on_assignment? should be false by default") + + # Explicitly set :ECHO to false + IRB.conf[:ECHO] = false + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) + + refute(irb.context.echo?, "echo? should be false when IRB.conf[:ECHO] is set to false") + refute(irb.context.echo_on_assignment?, "echo_on_assignment? should be false by default") + + # Explicitly set :ECHO_ON_ASSIGNMENT to true + IRB.conf[:ECHO] = nil + IRB.conf[:ECHO_ON_ASSIGNMENT] = true + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) + + assert(irb.context.echo?, "echo? should be true by default") + assert(irb.context.echo_on_assignment?, "echo_on_assignment? should be true when IRB.conf[:ECHO_ON_ASSIGNMENT] is set to true") + end end end diff --git a/test/irb/test_raise_no_backtrace_exception.rb b/test/irb/test_raise_no_backtrace_exception.rb index e92d8dc970..2174600082 100644 --- a/test/irb/test_raise_no_backtrace_exception.rb +++ b/test/irb/test_raise_no_backtrace_exception.rb @@ -7,6 +7,7 @@ module TestIRB bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] assert_in_out_err(bundle_exec + %w[-rirb -W0 -e IRB.start(__FILE__) -- -f --], <<-IRB, /Exception: foo/, []) e = Exception.new("foo") + puts e.inspect def e.backtrace; nil; end raise e IRB