diff --git a/lib/thor.rb b/lib/thor.rb index 1409623b3..7884711fe 100644 --- a/lib/thor.rb +++ b/lib/thor.rb @@ -507,3 +507,5 @@ def help(command = nil, subcommand = false) end end end + +require "thor/thor2" diff --git a/lib/thor/base.rb b/lib/thor/base.rb index c5f004e3e..fbb838f12 100644 --- a/lib/thor/base.rb +++ b/lib/thor/base.rb @@ -138,7 +138,8 @@ def attr_accessor(*) #:nodoc: end # If you want to raise an error for unknown options, call check_unknown_options! - # This is disabled by default to allow dynamic invocations. + # This is disabled by default in the Thor class to allow dynamic invocations. + # This is enabled by default in the Thor2 class def check_unknown_options! @check_unknown_options = true end @@ -153,7 +154,8 @@ def check_unknown_options?(config) #:nodoc: # If you want to raise an error when the default value of an option does not match # the type call check_default_type! - # This is disabled by default for compatibility. + # This is disabled by default in the Thor class for compatibility. + # This is enabled by default in the Thor2 class def check_default_type! @check_default_type = true end diff --git a/lib/thor/thor2.rb b/lib/thor/thor2.rb new file mode 100644 index 000000000..34b1ad45e --- /dev/null +++ b/lib/thor/thor2.rb @@ -0,0 +1,32 @@ +class Thor + class Thor2 < Thor + # This is a class to use instead of Thor when declaring your CLI + # This alternative works the same way as Thor, but has more common defaults: + # * If there is a failure in the argument parsing and other Thor-side + # things, the exit code will be non-zero + # * Things that look like options but are not valid options will + # will show an error of being unknown option instead of being + # used as arguments. + # * Make sure the default value of options is of the correct type + # For backward compatibility reasons, these cannot be made default in + # the regular `Thor` class + # + # This class is available in the top-level as Thor2, so you can do + # class MyCli < Thor2 + # ... + # end + + # Fail on unknown options instead of treating them as argument + check_unknown_options! + + # Make sure the default value of options is of the correct type + check_default_type! + + # All failures should result in non-zero error code + def self.exit_on_failure? + true + end + end +end + +::Thor2 = Thor::Thor2 diff --git a/spec/fixtures/thor2.thor b/spec/fixtures/thor2.thor new file mode 100644 index 000000000..b94d049d3 --- /dev/null +++ b/spec/fixtures/thor2.thor @@ -0,0 +1,15 @@ +require "thor" + +class MySimpleThor2 < Thor2 + class_option "verbose", :type => :boolean + class_option "mode", :type => :string + + desc "checked", "a command with checked" + def checked(*args) + puts [options, args].inspect + [options, args] + end +end + +MySimpleThor2.start(ARGV) + diff --git a/spec/helper.rb b/spec/helper.rb index fa3bd0080..c4da8f455 100644 --- a/spec/helper.rb +++ b/spec/helper.rb @@ -80,4 +80,24 @@ def silence_warnings end alias silence capture + + # Runs the fixture in a different process. + # Useful to deal with exit_on_failure?, which interrupts the tests when it calls `exit` + # This doesn't run on ruby 1.8.7 + def run_thor_fixture_standalone(fixture, command) + gem_dir = File.expand_path("#{File.dirname(__FILE__)}/..") + lib_path = "#{gem_dir}/lib" + script_path = "#{gem_dir}/spec/fixtures/#{fixture}.thor" + ruby_lib = ENV['RUBYLIB'].nil? ? lib_path : "#{lib_path}:#{ENV['RUBYLIB']}" + + if command.is_a?(String) + full_command = "ruby #{script_path} #{command}" + elsif command.is_a?(Array) + full_command = ['ruby', script_path] + command + end + + require 'open3' + stdout, stderr, status = Open3.capture3({'RUBYLIB' => ruby_lib}, *full_command) + [stdout, stderr, status] + end end diff --git a/spec/script_exit_status_spec.rb b/spec/script_exit_status_spec.rb index 45e04148b..409d4d0b6 100644 --- a/spec/script_exit_status_spec.rb +++ b/spec/script_exit_status_spec.rb @@ -20,10 +20,12 @@ def thor_command(command) end it "a command that raises a Thor::Error exits with a status of 1" do - expect(thor_command("error")).to eq(1) + _stdout, _stderr, status = run_thor_fixture_standalone('exit_status', ['error']) + expect(status.exitstatus).to eq(1) end it "a command that does not raise a Thor::Error exits with a status of 0" do - expect(thor_command("ok")).to eq(0) + _stdout, _stderr, status = run_thor_fixture_standalone('exit_status', ['ok']) + expect(status.exitstatus).to eq(0) end end if RUBY_VERSION > "1.8.7" diff --git a/spec/thor2_spec.rb b/spec/thor2_spec.rb new file mode 100644 index 000000000..c1fa3741d --- /dev/null +++ b/spec/thor2_spec.rb @@ -0,0 +1,26 @@ +require "helper" + +describe Thor2 do + describe "#check_unknown_options!" do + it "still accept options and arguments" do + stdout, _, status = run_thor_fixture_standalone('thor2', %w(checked command --verbose)) + + expect(stdout.strip).to eq [{"verbose" => true}, %w[command]].inspect + expect(status.exitstatus).to eq(0) + end + + it "does not accept if non-option that looks like an option is after an argument and exits with code 1" do + _stdout, stderr, status = run_thor_fixture_standalone('thor2', %w(checked command --foo --bar)) + expect(stderr.strip).to eq("Unknown switches '--foo, --bar'") + expect(status.exitstatus).to eq(1) + end + end if RUBY_VERSION > "1.8.7" + + it "checks the default type" do + expect do + Class.new(Thor2) do + option "bar", :type => :numeric, :default => "foo" + end + end.to raise_error(ArgumentError, "Expected numeric default value for '--bar'; got \"foo\" (string)") + end +end