Skip to content

Commit 4eb2300

Browse files
committed
Fix check_unknown_options! when parsing gets stopped
If the parsing gets stopped by either a -- or by #stop_on_unknown_option!, then check_unknown_options! would still try to do validation on the unparsed parts. We now track where the stop happened and only check the extras that were before that point.
1 parent dd45079 commit 4eb2300

File tree

3 files changed

+119
-1
lines changed

3 files changed

+119
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
* Fix `check_unknown_options!` to not check the content that was not parsed, i.e. after a `--` or after the first unknown with `stop_on_unknown_option!`
2+
13
## 0.20.0
24
* Add `check_default_type!` to check if the default value of an option matches the defined type.
35
It removes the warning on usage and gives the command authors the possibility to check for programming errors.

lib/thor/parser/options.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false, disabl
4444
@shorts = {}
4545
@switches = {}
4646
@extra = []
47+
@stopped_parsing_after_extra_index = nil
4748

4849
options.each do |option|
4950
@switches[option.switch_name] = option
@@ -66,6 +67,7 @@ def peek
6667
if result == OPTS_END
6768
shift
6869
@parsing_options = false
70+
@stopped_parsing_after_extra_index ||= @extra.size
6971
super
7072
else
7173
result
@@ -99,6 +101,7 @@ def parse(args) # rubocop:disable MethodLength
99101
elsif @stop_on_unknown
100102
@parsing_options = false
101103
@extra << shifted
104+
@stopped_parsing_after_extra_index ||= @extra.size
102105
@extra << shift while peek
103106
break
104107
elsif match
@@ -120,8 +123,10 @@ def parse(args) # rubocop:disable MethodLength
120123
end
121124

122125
def check_unknown!
126+
to_check = @stopped_parsing_after_extra_index ? @extra[0...@stopped_parsing_after_extra_index] : @extra
127+
123128
# an unknown option starts with - or -- and has no more --'s afterward.
124-
unknown = @extra.select { |str| str =~ /^--?(?:(?!--).)*$/ }
129+
unknown = to_check.select { |str| str =~ /^--?(?:(?!--).)*$/ }
125130
raise UnknownArgumentError, "Unknown switches '#{unknown.join(', ')}'" unless unknown.empty?
126131
end
127132

spec/thor_spec.rb

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ def boring(*args)
127127
expect(my_script.start(%w(exec -- --verbose))).to eq [{}, %w(--verbose)]
128128
end
129129

130+
it "still passes everything after -- to command, complex" do
131+
expect(my_script.start(%w[exec command --mode z again -- --verbose more])).to eq [{}, %w[command --mode z again -- --verbose more]]
132+
end
133+
130134
it "does not affect ordinary commands" do
131135
expect(my_script.start(%w(boring command --verbose))).to eq [{"verbose" => true}, %w(command)]
132136
end
@@ -157,6 +161,113 @@ def boring(*args)
157161
it "doesn't break new" do
158162
expect(my_script.new).to be_a(Thor)
159163
end
164+
165+
context "along with check_unknown_options!" do
166+
my_script2 = Class.new(Thor) do
167+
class_option "verbose", :type => :boolean
168+
class_option "mode", :type => :string
169+
check_unknown_options!
170+
stop_on_unknown_option! :exec
171+
172+
desc "exec", "Run a command"
173+
def exec(*args)
174+
[options, args]
175+
end
176+
end
177+
178+
it "passes remaining args to command when it encounters a non-option" do
179+
expect(my_script2.start(%w[exec command --verbose])).to eq [{}, %w[command --verbose]]
180+
end
181+
182+
it "does not accept if first non-option looks like an option, but only refuses that invalid option" do
183+
expect(capture(:stderr) do
184+
my_script2.start(%w[exec --foo command --bar])
185+
end.strip).to eq("Unknown switches '--foo'")
186+
end
187+
188+
it "still accepts options that are given before non-options" do
189+
expect(my_script2.start(%w[exec --verbose command])).to eq [{"verbose" => true}, %w[command]]
190+
end
191+
192+
it "still accepts when non-options are given after real options and argument" do
193+
expect(my_script2.start(%w[exec --verbose command --foo])).to eq [{"verbose" => true}, %w[command --foo]]
194+
end
195+
196+
it "does not accept when non-option looks like an option and is after real options" do
197+
expect(capture(:stderr) do
198+
my_script2.start(%w[exec --verbose --foo])
199+
end.strip).to eq("Unknown switches '--foo'")
200+
end
201+
202+
it "still accepts options that require a value" do
203+
expect(my_script2.start(%w[exec --mode rashly command])).to eq [{"mode" => "rashly"}, %w[command]]
204+
end
205+
206+
it "still passes everything after -- to command" do
207+
expect(my_script2.start(%w[exec -- --verbose])).to eq [{}, %w[--verbose]]
208+
end
209+
210+
it "still passes everything after -- to command, complex" do
211+
expect(my_script2.start(%w[exec command --mode z again -- --verbose more])).to eq [{}, %w[command --mode z again -- --verbose more]]
212+
end
213+
end
214+
end
215+
216+
describe "#check_unknown_options!" do
217+
my_script = Class.new(Thor) do
218+
class_option "verbose", :type => :boolean
219+
class_option "mode", :type => :string
220+
check_unknown_options!
221+
222+
desc "checked", "a command with checked"
223+
def checked(*args)
224+
[options, args]
225+
end
226+
end
227+
228+
it "still accept options and arguments" do
229+
expect(my_script.start(%w[checked command --verbose])).to eq [{"verbose" => true}, %w[command]]
230+
end
231+
232+
it "still accepts options that are given before arguments" do
233+
expect(my_script.start(%w[checked --verbose command])).to eq [{"verbose" => true}, %w[command]]
234+
end
235+
236+
it "does not accept if non-option that looks like an option is before the arguments" do
237+
expect(capture(:stderr) do
238+
my_script.start(%w[checked --foo command --bar])
239+
end.strip).to eq("Unknown switches '--foo, --bar'")
240+
end
241+
242+
it "does not accept if non-option that looks like an option is after an argument" do
243+
expect(capture(:stderr) do
244+
my_script.start(%w[checked command --foo --bar])
245+
end.strip).to eq("Unknown switches '--foo, --bar'")
246+
end
247+
248+
it "does not accept when non-option that looks like an option is after real options" do
249+
expect(capture(:stderr) do
250+
my_script.start(%w[checked --verbose --foo])
251+
end.strip).to eq("Unknown switches '--foo'")
252+
end
253+
254+
it "does not accept when non-option that looks like an option is before real options" do
255+
expect(capture(:stderr) do
256+
my_script.start(%w[checked --foo --verbose])
257+
end.strip).to eq("Unknown switches '--foo'")
258+
end
259+
260+
it "still accepts options that require a value" do
261+
expect(my_script.start(%w[checked --mode rashly command])).to eq [{"mode" => "rashly"}, %w[command]]
262+
end
263+
264+
it "still passes everything after -- to command" do
265+
expect(my_script.start(%w[checked -- --verbose])).to eq [{}, %w[--verbose]]
266+
end
267+
268+
it "still passes everything after -- to command, complex" do
269+
expect(my_script.start(%w[checked command --mode z again -- --verbose more])).to eq [{"mode" => "z"}, %w[command again --verbose more]]
270+
end
160271
end
161272

162273
describe "#disable_required_check!" do

0 commit comments

Comments
 (0)