diff --git a/lib/irb/cmd/exit_program.rb b/lib/irb/cmd/exit_program.rb new file mode 100644 index 000000000..aaf0c9fd6 --- /dev/null +++ b/lib/irb/cmd/exit_program.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "nop" + +module IRB + module ExtendCommand + class ExitProgram < Nop + category "Misc" + description "End the current program, optionally with a status to give to `Kernel.exit`" + + def execute(arg = true) + Kernel.exit(arg) + end + end + end +end diff --git a/lib/irb/extend-command.rb b/lib/irb/extend-command.rb index d5f216bc9..1589fec4b 100644 --- a/lib/irb/extend-command.rb +++ b/lib/irb/extend-command.rb @@ -197,6 +197,12 @@ def irb_context :irb_history, :History, "cmd/history", [:history, NO_OVERRIDE], [:hist, NO_OVERRIDE], + ], + + [ + :irb_exit_program, :ExitProgram, 'cmd/exit_program', + [:exit_program, NO_OVERRIDE], + [:quit_program, NO_OVERRIDE], ] ] diff --git a/lib/irb/init.rb b/lib/irb/init.rb index 66e7b6146..419dc2bbd 100644 --- a/lib/irb/init.rb +++ b/lib/irb/init.rb @@ -187,8 +187,9 @@ def IRB.init_config(ap_path) @CONF[:COMMAND_ALIASES] = { # Symbol aliases - :'$' => :show_source, - :'@' => :whereami, + :'$' => :show_source, + :'@' => :whereami, + :'!!!' => :exit_program, } end diff --git a/test/irb/cmd/test_exit_program.rb b/test/irb/cmd/test_exit_program.rb new file mode 100644 index 000000000..a7e719019 --- /dev/null +++ b/test/irb/cmd/test_exit_program.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true +require 'irb' + +require_relative "../helper" + +module TestIRB + class ExitProgramTest < IntegrationTestCase + def test_irb_exit_program + assert_exits_program(with_status: 0) do + type "irb_exit_program" + end + end + + def test_exit_program + assert_exits_program(with_status: 0) do + type "exit_program" + end + end + + def test_quit_program + assert_exits_program(with_status: 0) do + type "quit_program" + end + end + + def test_triple_bang + assert_exits_program(with_status: 0) do + type "!!!" + end + end + + def test_exit_code_zero + assert_exits_program(with_status: 0) do + type "!!! 0" + end + end + + def test_exit_code_one + assert_exits_program(with_status: 1) do + type "!!! 1" + end + end + + def test_exit_code_expression + assert_exits_program(with_status: 2) do + type "n = 1" + type "!!! n + 1" + end + end + + def test_exit_code_expression + assert_exits_program(with_status: 2) do + type "n = 1" + type "!!! n + 1" + end + end + + private + + def assert_exits_program(with_status:, &block) + write_ruby <<~'RUBY' + begin + binding.irb + puts "Did not raise #{SystemExit}!" # Interpolate so we don't match whereami context + rescue SystemExit => e + puts "Raised SystemExit with status #{e.status.inspect}" + end + RUBY + + output = run_ruby_file(&block) + + refute_includes(output, "Did not raise SystemExit!", "AN ERROR MESSAGE") + matching_status = output[/(?<=Raised SystemExit with status )(\d+)/] + refute_nil matching_status, "Did not find exit status in output: \n#{output}" + assert_equal with_status, matching_status.to_i, "Exited with wrong status code" + end + end +end