diff --git a/bin/hirb.rb b/bin/hirb.rb index 5b502f4ca3d..7b1b8f172c2 100644 --- a/bin/hirb.rb +++ b/bin/hirb.rb @@ -174,16 +174,20 @@ def debug? nil end + # For backwards compatibility, this will export all the HBase shell commands, constants, and # instance variables (@hbase and @shell) onto Ruby's top-level receiver object known as "main". @shell.export_all(self) if top_level_definitions # If script2run, try running it. If we're in interactive mode, will go on to run the shell unless # script calls 'exit' or 'exit 0' or 'exit errcode'. -@shell.eval_io(File.new(script2run)) if script2run +require 'shell/hbase_loader' +if script2run + ::Shell::Shell.exception_handler(!$fullBackTrace) { @shell.eval_io(Hbase::Loader.file_for_load(script2run), filename = script2run) } +end # If we are not running interactively, evaluate standard input -@shell.eval_io(STDIN) unless interactive +::Shell::Shell.exception_handler(!$fullBackTrace) { @shell.eval_io(STDIN) } unless interactive if interactive # Output a banner message that tells users where to go for help diff --git a/hbase-shell/src/main/ruby/shell.rb b/hbase-shell/src/main/ruby/shell.rb index b6cc8b0f37a..96b7fe22995 100644 --- a/hbase-shell/src/main/ruby/shell.rb +++ b/hbase-shell/src/main/ruby/shell.rb @@ -297,11 +297,11 @@ For more on the HBase Shell, see http://hbase.apache.org/book.html return @irb_workspace unless @irb_workspace.nil? hbase_receiver = HBaseReceiver.new - # Install all the hbase commands, constants, and instance variables @shell and @hbase. This - # must be BEFORE the irb commands are installed so that our help command is not overwritten. - export_all(hbase_receiver) # install all the IRB commands onto our receiver IRB::ExtendCommandBundle.extend_object(hbase_receiver) + # Install all the hbase commands, constants, and instance variables @shell and @hbase. This + # will override names that conflict with IRB methods like "help". + export_all(hbase_receiver) ::IRB::WorkSpace.new(hbase_receiver.get_binding) end @@ -311,7 +311,10 @@ For more on the HBase Shell, see http://hbase.apache.org/book.html # Unlike Ruby's require or load, this method allows us to execute code with a custom binding. In # this case, we are using the binding constructed with all the HBase shell constants and # methods. - def eval_io(io) + # + # @param [IO] io instance of Ruby's IO (or subclass like File) to read script from + # @param [String] filename to print in tracebacks + def eval_io(io, filename = 'stdin') require 'irb/ruby-lex' # Mixing HBaseIOExtensions into IO allows us to pass IO objects to RubyLex. IO.include HBaseIOExtensions @@ -320,10 +323,20 @@ For more on the HBase Shell, see http://hbase.apache.org/book.html scanner = RubyLex.new scanner.set_input(io) + scanner.each_top_level_statement do |statement, linenum| + puts(workspace.evaluate(nil, statement, filename, linenum)) + end + nil + end + + ## + # Runs a block and logs exception from both Ruby and Java, optionally discarding the traceback + # + # @param [Boolean] hide_traceback if true, Exceptions will be converted to + # a SystemExit so that the traceback is not printed + def self.exception_handler(hide_traceback) begin - scanner.each_top_level_statement do |statement, linenum| - puts(workspace.evaluate(nil, statement, 'stdin', linenum)) - end + yield rescue Exception => e message = e.to_s # exception unwrapping in shell means we'll have to handle Java exceptions @@ -335,12 +348,9 @@ For more on the HBase Shell, see http://hbase.apache.org/book.html # Include the 'ERROR' string to try to make transition easier for scripts that # may have already been relying on grepping output. puts "ERROR #{e.class}: #{message}" - if $fullBacktrace - # re-raising the will include a backtrace and exit. - raise e - else - exit 1 - end + raise e unless hide_traceback + + exit 1 end nil end diff --git a/hbase-shell/src/main/ruby/shell/hbase_loader.rb b/hbase-shell/src/main/ruby/shell/hbase_loader.rb new file mode 100644 index 00000000000..2ad2ea9a6aa --- /dev/null +++ b/hbase-shell/src/main/ruby/shell/hbase_loader.rb @@ -0,0 +1,56 @@ +# +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +module Hbase + ## + # HBase::Loader serves a similar purpose to IRB::IrbLoader, but with a different separation of + # concerns. This loader allows us to directly get the path for a filename in ruby's load path, + # and then use that in combination with something like HBase::Shell#eval_io. + module Loader + ## + # Determine the loadable path for a given filename by searching through $LOAD_PATH + # + # This serves a similar purpose to IRB::IrbLoader#search_file_from_ruby_path, but uses JRuby's + # loader, which allows us to find special paths like "uri:classloader" inside of a Jar. + # + # @param [String] filename + # @return [String] path + def self.path_for_load(filename) + return File.absolute_path(filename) if File.exist? filename + + # Get JRuby's LoadService from the global (singleton) instance of the runtime + # (org.jruby.Ruby), which allows us to use JRuby's tools for searching the load path. + runtime = org.jruby.Ruby.getGlobalRuntime + loader = runtime.getLoadService + search_state = loader.findFileForLoad filename + raise LoadError, "no such file to load -- #{filename}" if search_state.library.nil? + + search_state.loadName + end + + ## + # Return a file handle for the given file found in the load path + # + # @param [String] filename + # @return [File] file handle + def self.file_for_load(filename) + File.new(path_for_load(filename)) + end + end +end diff --git a/hbase-shell/src/test/ruby/shell/shell_test.rb b/hbase-shell/src/test/ruby/shell/shell_test.rb index c8f68073b6d..5d3b07ac2fc 100644 --- a/hbase-shell/src/test/ruby/shell/shell_test.rb +++ b/hbase-shell/src/test/ruby/shell/shell_test.rb @@ -114,6 +114,7 @@ class ShellTest < Test::Unit::TestCase #----------------------------------------------------------------------------- define_test 'Shell::Shell#eval_io should evaluate IO' do + StringIO.include HBaseIOExtensions # check that at least one of the commands is present while evaluating io = StringIO.new <<~EOF puts (respond_to? :list) @@ -123,7 +124,7 @@ class ShellTest < Test::Unit::TestCase # check that at least one of the HBase constants is present while evaluating io = StringIO.new <<~EOF - ROWPREFIXFILTER + ROWPREFIXFILTER.dump EOF output = capture_stdout { @shell.eval_io(io) } assert_match(/"ROWPREFIXFILTER"/, output) @@ -131,6 +132,25 @@ class ShellTest < Test::Unit::TestCase #----------------------------------------------------------------------------- + define_test 'Shell::Shell#exception_handler should hide traceback' do + class TestException < RuntimeError; end + # When hide_traceback is true, exception_handler should replace exceptions + # with SystemExit so that the traceback is not printed. + assert_raises(SystemExit) do + ::Shell::Shell.exception_handler(true) { raise TestException, 'Custom Exception' } + end + end + + define_test 'Shell::Shell#exception_handler should show traceback' do + class TestException < RuntimeError; end + # When hide_traceback is false, exception_handler should re-raise Exceptions + assert_raises(TestException) do + ::Shell::Shell.exception_handler(false) { raise TestException, 'Custom Exception' } + end + end + + #----------------------------------------------------------------------------- + define_test 'Shell::Shell#print_banner should display Reference Guide link' do @shell.interactive = true output = capture_stdout { @shell.print_banner } @@ -141,7 +161,7 @@ class ShellTest < Test::Unit::TestCase #----------------------------------------------------------------------------- - define_test "Shell::Shell interactive mode should not throw" do + define_test 'Shell::Shell interactive mode should not throw' do # incorrect number of arguments @shell.command('create', 'nothrow_table') @shell.command('create', 'nothrow_table', 'family_1') diff --git a/hbase-shell/src/test/ruby/tests_runner.rb b/hbase-shell/src/test/ruby/tests_runner.rb index 73d4a6e7113..b0a0aaf1763 100644 --- a/hbase-shell/src/test/ruby/tests_runner.rb +++ b/hbase-shell/src/test/ruby/tests_runner.rb @@ -82,9 +82,14 @@ if java.lang.System.get_property('shell.test') puts "Only running tests that match #{shell_test_pattern}" runner_args << "--testcase=#{shell_test_pattern}" end -# first couple of args are to match the defaults, so we can pass options to limit the tests run -if !(Test::Unit::AutoRunner.run(false, nil, runner_args)) - raise "Shell unit tests failed. Check output file for details." +begin + # first couple of args are to match the defaults, so we can pass options to limit the tests run + unless Test::Unit::AutoRunner.run(false, nil, runner_args) + raise 'Shell unit tests failed. Check output file for details.' + end +rescue SystemExit => e + # Unit tests should not raise uncaught SystemExit exceptions. This could cause tests to be ignored. + raise 'Caught SystemExit during unit test execution! Check output file for details.' end puts "Done with tests! Shutting down the cluster..."