From de10e38b5659818059dd0b74d954e371d03eedb7 Mon Sep 17 00:00:00 2001 From: Maxime Lapointe Date: Sun, 16 Sep 2018 15:48:42 -0400 Subject: [PATCH 1/2] Extract thor_command into helper run_thor_fixture_standalone --- spec/helper.rb | 20 ++++++++++++++++++++ spec/script_exit_status_spec.rb | 6 ++++-- 2 files changed, 24 insertions(+), 2 deletions(-) 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" From 21d1aaafd9cc0548e882c2d35fb18fbf27bf9609 Mon Sep 17 00:00:00 2001 From: Maxime Lapointe Date: Sat, 15 Sep 2018 13:30:40 -0400 Subject: [PATCH 2/2] Introduce Thor2, a class with less unexpected defaults This new class allows having more expected defaults without breaking backward compatibility. Documentation can then recommend using Thor2 for new CLIs which will be more intuitive for users. --- lib/thor.rb | 2 ++ lib/thor/base.rb | 6 ++++-- lib/thor/thor2.rb | 32 ++++++++++++++++++++++++++++++++ spec/fixtures/thor2.thor | 15 +++++++++++++++ spec/thor2_spec.rb | 26 ++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 lib/thor/thor2.rb create mode 100644 spec/fixtures/thor2.thor create mode 100644 spec/thor2_spec.rb 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/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