diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 64aeb1d4f..5b7917e45 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,7 +4,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: 3.1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 736a7b7c8..8b18e3d5c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,12 +5,13 @@ jobs: strategy: fail-fast: false matrix: - ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', 'head'] + ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3', 'head'] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} + bundler: ${{ (matrix.ruby_version < '3' && '2.4.21') || 'latest' }} bundler-cache: true # runs 'bundle install' and caches installed gems automatically - run: bundle exec thor spec diff --git a/lib/thor/actions/file_manipulation.rb b/lib/thor/actions/file_manipulation.rb index e4aa8e890..8eec045e1 100644 --- a/lib/thor/actions/file_manipulation.rb +++ b/lib/thor/actions/file_manipulation.rb @@ -10,7 +10,6 @@ module Actions # destination:: the relative path to the destination root. # config:: give :verbose => false to not log the status, and # :mode => :preserve, to preserve the file mode from the source. - # # ==== Examples # @@ -275,9 +274,8 @@ def gsub_file(path, flag, *args, &block) end end - # Uncomment all lines matching a given regex. It will leave the space - # which existed before the comment hash in tact but will remove any spacing - # between the comment hash and the beginning of the line. + # Uncomment all lines matching a given regex. Preserves indentation before + # the comment hash and removes the hash and any immediate following space. # # ==== Parameters # path:: path of the file to be changed @@ -291,7 +289,7 @@ def gsub_file(path, flag, *args, &block) def uncomment_lines(path, flag, *args) flag = flag.respond_to?(:source) ? flag.source : flag - gsub_file(path, /^(\s*)#[[:blank:]]*(.*#{flag})/, '\1\2', *args) + gsub_file(path, /^(\s*)#[[:blank:]]?(.*#{flag})/, '\1\2', *args) end # Comment all lines matching a given regex. It will leave the space diff --git a/lib/thor/parser/argument.rb b/lib/thor/parser/argument.rb index 01820111f..0cd1f7bfb 100644 --- a/lib/thor/parser/argument.rb +++ b/lib/thor/parser/argument.rb @@ -26,10 +26,7 @@ def initialize(name, options = {}) def print_default if @type == :array and @default.is_a?(Array) - @default.map { |x| - p = x.gsub('"','\\"') - "\"#{p}\"" - }.join(" ") + @default.map(&:dump).join(" ") else @default end diff --git a/lib/thor/parser/option.rb b/lib/thor/parser/option.rb index b1a994313..a369d8f11 100644 --- a/lib/thor/parser/option.rb +++ b/lib/thor/parser/option.rb @@ -89,8 +89,8 @@ def usage(padding = 0) sample = "[#{sample}]".dup unless required? - if boolean? - sample << ", [#{dasherize('no-' + human_name)}]" unless (name == "force") || name.match(/\Ano[\-_]/) + if boolean? && name != "force" && !name.match(/\A(no|skip)[\-_]/) + sample << ", [#{dasherize('no-' + human_name)}], [#{dasherize('skip-' + human_name)}]" end aliases_for_usage.ljust(padding) + sample diff --git a/lib/thor/parser/options.rb b/lib/thor/parser/options.rb index 984a8ab27..497bcbb36 100644 --- a/lib/thor/parser/options.rb +++ b/lib/thor/parser/options.rb @@ -250,7 +250,8 @@ def parsing_options? @parsing_options end - # Parse boolean values which can be given as --foo=true, --foo or --no-foo. + # Parse boolean values which can be given as --foo=true or --foo for true values, or + # --foo=false, --no-foo or --skip-foo for false values. # def parse_boolean(switch) if current_is_value? diff --git a/lib/thor/shell/basic.rb b/lib/thor/shell/basic.rb index e20f132c0..71ce40710 100644 --- a/lib/thor/shell/basic.rb +++ b/lib/thor/shell/basic.rb @@ -67,15 +67,15 @@ def indent(count = 1) # Readline. # # ==== Example - # ask("What is your name?") + # ask("What is your name?") # - # ask("What is the planet furthest from the sun?", :default => "Pluto") + # ask("What is the planet furthest from the sun?", :default => "Neptune") # - # ask("What is your favorite Neopolitan flavor?", :limited_to => ["strawberry", "chocolate", "vanilla"]) + # ask("What is your favorite Neopolitan flavor?", :limited_to => ["strawberry", "chocolate", "vanilla"]) # - # ask("What is your password?", :echo => false) + # ask("What is your password?", :echo => false) # - # ask("Where should the file be saved?", :path => true) + # ask("Where should the file be saved?", :path => true) # def ask(statement, *args) options = args.last.is_a?(Hash) ? args.pop : {} @@ -93,7 +93,7 @@ def ask(statement, *args) # are passed straight to puts (behavior got from Highline). # # ==== Example - # say("I know you knew that.") + # say("I know you knew that.") # def say(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/)) return if quiet? @@ -110,7 +110,7 @@ def say(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/) # are passed straight to puts (behavior got from Highline). # # ==== Example - # say_error("error: something went wrong") + # say_error("error: something went wrong") # def say_error(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/)) return if quiet? @@ -143,14 +143,14 @@ def say_status(status, message, log_status = true) stdout.flush end - # Make a question the to user and returns true if the user replies "y" or + # Asks the user a question and returns true if the user replies "y" or # "yes". # def yes?(statement, color = nil) !!(ask(statement, color, add_to_history: false) =~ is?(:yes)) end - # Make a question the to user and returns true if the user replies "n" or + # Asks the user a question and returns true if the user replies "n" or # "no". # def no?(statement, color = nil) diff --git a/lib/thor/shell/html.rb b/lib/thor/shell/html.rb index 547a9da59..bb3ef6661 100644 --- a/lib/thor/shell/html.rb +++ b/lib/thor/shell/html.rb @@ -67,7 +67,7 @@ def set_color(string, *colors) # Ask something to the user and receives a response. # # ==== Example - # ask("What is your name?") + # ask("What is your name?") # # TODO: Implement #ask for Thor::Shell::HTML def ask(statement, color = nil) diff --git a/lib/thor/shell/table_printer.rb b/lib/thor/shell/table_printer.rb index 888289541..6ca57b2b5 100644 --- a/lib/thor/shell/table_printer.rb +++ b/lib/thor/shell/table_printer.rb @@ -102,33 +102,17 @@ def print_border_separator def truncate(string) return string unless @truncate - as_unicode do - chars = string.chars.to_a - if chars.length <= @truncate - chars.join - else - chars[0, @truncate - 3 - @indent].join + "..." - end + chars = string.chars.to_a + if chars.length <= @truncate + chars.join + else + chars[0, @truncate - 3 - @indent].join + "..." end end def indentation " " * @indent end - - if "".respond_to?(:encode) - def as_unicode - yield - end - else - def as_unicode - old = $KCODE # rubocop:disable Style/GlobalVars - $KCODE = "U" # rubocop:disable Style/GlobalVars - yield - ensure - $KCODE = old # rubocop:disable Style/GlobalVars - end - end end end end diff --git a/lib/thor/version.rb b/lib/thor/version.rb index 2002eee59..9ddec0f19 100644 --- a/lib/thor/version.rb +++ b/lib/thor/version.rb @@ -1,3 +1,3 @@ class Thor - VERSION = "1.3.0" + VERSION = "1.3.1" end diff --git a/spec/actions/file_manipulation_spec.rb b/spec/actions/file_manipulation_spec.rb index a1b902af3..72919e09b 100644 --- a/spec/actions/file_manipulation_spec.rb +++ b/spec/actions/file_manipulation_spec.rb @@ -475,20 +475,26 @@ def file File.join(destination_root, "doc", "COMMENTER") end - unmodified_comments_file = /__start__\n # greenblue\n#\n# yellowblue\n#yellowred\n #greenred\norange\n purple\n ind#igo\n # ind#igo\n__end__/ + unmodified_comments_file = /__start__\n # greenblue\n#\n# yellowblue\n#yellowred\n #greenred\norange\n purple\n ind#igo\n # ind#igo\n # spaces_between\n__end__/ describe "#uncomment_lines" do it "uncomments all matching lines in the file" do action :uncomment_lines, "doc/COMMENTER", "green" - expect(File.binread(file)).to match(/__start__\n greenblue\n#\n# yellowblue\n#yellowred\n greenred\norange\n purple\n ind#igo\n # ind#igo\n__end__/) + expect(File.binread(file)).to match(/__start__\n greenblue\n#\n# yellowblue\n#yellowred\n greenred\norange\n purple\n ind#igo\n # ind#igo\n # spaces_between\n__end__/) action :uncomment_lines, "doc/COMMENTER", "red" - expect(File.binread(file)).to match(/__start__\n greenblue\n#\n# yellowblue\nyellowred\n greenred\norange\n purple\n ind#igo\n # ind#igo\n__end__/) + expect(File.binread(file)).to match(/__start__\n greenblue\n#\n# yellowblue\nyellowred\n greenred\norange\n purple\n ind#igo\n # ind#igo\n # spaces_between\n__end__/) end it "correctly uncomments lines with hashes in them" do action :uncomment_lines, "doc/COMMENTER", "ind#igo" - expect(File.binread(file)).to match(/__start__\n # greenblue\n#\n# yellowblue\n#yellowred\n #greenred\norange\n purple\n ind#igo\n ind#igo\n__end__/) + expect(File.binread(file)).to match(/__start__\n # greenblue\n#\n# yellowblue\n#yellowred\n #greenred\norange\n purple\n ind#igo\n ind#igo\n # spaces_between\n__end__/) + end + + it "will leave the space which existed before the comment hash in tact" do + action :uncomment_lines, "doc/COMMENTER", "ind#igo" + action :uncomment_lines, "doc/COMMENTER", "spaces_between" + expect(File.binread(file)).to match(/__start__\n # greenblue\n#\n# yellowblue\n#yellowred\n #greenred\norange\n purple\n ind#igo\n ind#igo\n spaces_between\n__end__/) end it "does not modify already uncommented lines in the file" do @@ -499,22 +505,22 @@ def file it "does not uncomment the wrong line when uncommenting lines preceded by blank commented line" do action :uncomment_lines, "doc/COMMENTER", "yellow" - expect(File.binread(file)).to match(/__start__\n # greenblue\n#\nyellowblue\nyellowred\n #greenred\norange\n purple\n ind#igo\n # ind#igo\n__end__/) + expect(File.binread(file)).to match(/__start__\n # greenblue\n#\nyellowblue\nyellowred\n #greenred\norange\n purple\n ind#igo\n # ind#igo\n # spaces_between\n__end__/) end end describe "#comment_lines" do it "comments lines which are not commented" do action :comment_lines, "doc/COMMENTER", "orange" - expect(File.binread(file)).to match(/__start__\n # greenblue\n#\n# yellowblue\n#yellowred\n #greenred\n# orange\n purple\n ind#igo\n # ind#igo\n__end__/) + expect(File.binread(file)).to match(/__start__\n # greenblue\n#\n# yellowblue\n#yellowred\n #greenred\n# orange\n purple\n ind#igo\n # ind#igo\n # spaces_between\n__end__/) action :comment_lines, "doc/COMMENTER", "purple" - expect(File.binread(file)).to match(/__start__\n # greenblue\n#\n# yellowblue\n#yellowred\n #greenred\n# orange\n # purple\n ind#igo\n # ind#igo\n__end__/) + expect(File.binread(file)).to match(/__start__\n # greenblue\n#\n# yellowblue\n#yellowred\n #greenred\n# orange\n # purple\n ind#igo\n # ind#igo\n # spaces_between\n__end__/) end it "correctly comments lines with hashes in them" do action :comment_lines, "doc/COMMENTER", "ind#igo" - expect(File.binread(file)).to match(/__start__\n # greenblue\n#\n# yellowblue\n#yellowred\n #greenred\norange\n purple\n # ind#igo\n # ind#igo\n__end__/) + expect(File.binread(file)).to match(/__start__\n # greenblue\n#\n# yellowblue\n#yellowred\n #greenred\norange\n purple\n # ind#igo\n # ind#igo\n # spaces_between\n__end__/) end it "does not modify already commented lines" do diff --git a/spec/fixtures/doc/COMMENTER b/spec/fixtures/doc/COMMENTER index 384cb3a50..84e62144a 100644 --- a/spec/fixtures/doc/COMMENTER +++ b/spec/fixtures/doc/COMMENTER @@ -8,4 +8,5 @@ orange purple ind#igo # ind#igo + # spaces_between __end__ diff --git a/spec/group_spec.rb b/spec/group_spec.rb index 660a40d55..6d5a65ee5 100644 --- a/spec/group_spec.rb +++ b/spec/group_spec.rb @@ -28,7 +28,11 @@ end it "raises when an exception happens within the command call" do - expect { BrokenCounter.start(%w(1 2 --fail)) }.to raise_error(NameError, /undefined local variable or method `this_method_does_not_exist'/) + if RUBY_VERSION < "3.4.0" + expect { BrokenCounter.start(%w(1 2 --fail)) }.to raise_error(NameError, /undefined local variable or method `this_method_does_not_exist'/) + else + expect { BrokenCounter.start(%w(1 2 --fail)) }.to raise_error(NameError, /undefined local variable or method 'this_method_does_not_exist'/) + end end it "raises an error when a Thor group command expects arguments" do diff --git a/spec/parser/option_spec.rb b/spec/parser/option_spec.rb index 54aba1bba..4cc9b360f 100644 --- a/spec/parser/option_spec.rb +++ b/spec/parser/option_spec.rb @@ -218,11 +218,11 @@ def option(name, options = {}) end it "returns usage for boolean types" do - expect(parse(:foo, :boolean).usage).to eq("[--foo], [--no-foo]") + expect(parse(:foo, :boolean).usage).to eq("[--foo], [--no-foo], [--skip-foo]") end it "does not use padding when no aliases are given" do - expect(parse(:foo, :boolean).usage).to eq("[--foo], [--no-foo]") + expect(parse(:foo, :boolean).usage).to eq("[--foo], [--no-foo], [--skip-foo]") end it "documents a negative option when boolean" do @@ -231,6 +231,9 @@ def option(name, options = {}) it "does not document a negative option for a negative boolean" do expect(parse(:'no-foo', :boolean).usage).not_to include("[--no-no-foo]") + expect(parse(:'no-foo', :boolean).usage).not_to include("[--skip-no-foo]") + expect(parse(:'skip-foo', :boolean).usage).not_to include("[--no-skip-foo]") + expect(parse(:'skip-foo', :boolean).usage).not_to include("[--skip-skip-foo]") end it "does not document a negative option for an underscored negative boolean" do @@ -261,7 +264,7 @@ def option(name, options = {}) end it "does not negate the aliases" do - expect(parse([:foo, "-f", "-b"], :boolean).usage).to eq("-f, -b, [--foo], [--no-foo]") + expect(parse([:foo, "-f", "-b"], :boolean).usage).to eq("-f, -b, [--foo], [--no-foo], [--skip-foo]") end it "normalizes the aliases" do