Skip to content

Commit

Permalink
Parse incrementally, enabling subcommands:
Browse files Browse the repository at this point in the history
By rewriting parse to accumulate arguments as we go, we can easily add a
condition to stop parsing when a subcommand is detected.
  • Loading branch information
burke committed Nov 14, 2023
1 parent 76cea9d commit 6c0c676
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 48 deletions.
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,4 +311,23 @@ end
Commands
--------

Slop no longer has built in support for git-style subcommands.
You can implement git-style subcommands by passing `subcommands: true` to
`parse`:

```ruby
argv = ["-n", "my-ns", "run", "-q"]
global_result = Slop.parse(argv, subcommands: true) do |o|
o.string "-n", "--namespace", "a namespace"
end

argv = global_result.arguments
subcommand = argv.shift
if subcommand == "run"
result = Slop.parse(argv) do |o|
o.bool "-q", "--quiet", "suppress output"
end

puts global_result[:namespace] #=> "my-ns"
puts result[:quiet] #=> true
end
```
71 changes: 24 additions & 47 deletions lib/slop/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,50 +37,28 @@ def reset
# Returns a Slop::Result.
def parse(strings)
reset # reset before every parse
strings = strings.dup

# ignore everything after "--"
strings, ignored_args = partition(strings)
while (arg = strings.shift)
possible_value = strings.first unless strings.first == '--'
break if arg == "--"

pairs = strings.each_cons(2).to_a
# this ensures we still support the last string being a flag,
# otherwise it'll only be used as an argument.
pairs << [strings.last, nil]
opt_name, explicit_value = arg.split("=", 2)

@arguments = strings.dup

pairs.each_with_index do |pair, idx|
flag, arg = pair
break if !flag

# support `foo=bar`
orig_flag = flag.dup
if match = flag.match(/([^=]+)=(.*)/)
flag, arg = match.captures
end

if opt = try_process(flag, arg)
# since the option was parsed, we remove it from our
# arguments (plus the arg if necessary)
# delete argument first while we can find its index.
if opt.expects_argument?

# if we consumed the argument, remove the next pair
if consume_next_argument?(orig_flag)
pairs.delete_at(idx + 1)
end

arguments.each_with_index do |argument, i|
if argument == orig_flag && !orig_flag.include?("=")
arguments.delete_at(i + 1)
end
end
if (opt = try_process(opt_name, explicit_value || possible_value))
# Skip the next argument if we consumed it as the value for this arg.
if opt.expects_argument? && consume_next_argument?(arg)
strings.shift
end
arguments.delete(orig_flag)
else
# If it wasn't used as an arg, add it to the arguments.
add_argument(arg)
# If we're expecting subcommands, this argument was the subcommand,
# and any subsequent flags/opts are _its_ property, not ours.
break if subcommands?
end
end

@arguments += ignored_args

if !suppress_errors?
unused_options.each do |o|
if o.config[:required]
Expand All @@ -89,12 +67,17 @@ def parse(strings)
end
end
end
arguments.concat(strings)

Result.new(self).tap do |result|
used_options.each { |o| o.finish(result) }
end
end

def add_argument(string)
arguments << string
end

# Returns an Array of Option instances that were used.
def used_options
options.select { |o| o.count > 0 }
Expand Down Expand Up @@ -154,22 +137,16 @@ def try_process_grouped_flags(flag, arg)
try_process(last, arg) # send the argument to the last flag
end

def subcommands?
config[:subcommands]
end

def suppress_errors?
config[:suppress_errors]
end

def matching_option(flag)
options.find { |o| o.flags.include?(flag) }
end

def partition(strings)
if strings.include?("--")
partition_idx = strings.index("--")
return [[], strings[1..-1]] if partition_idx.zero?
[strings[0..partition_idx-1], strings[partition_idx+1..-1]]
else
[strings, []]
end
end
end
end
12 changes: 12 additions & 0 deletions test/parser_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@
@result = @parser.parse %w(foo -v --name lee argument)
end

describe "in subcommands mode" do
before do
@parser = Slop::Parser.new(@options, subcommands: true)
end

it "stops parsing after the first argument" do
@parser.parse %w(-n name cmd -v)
assert_equal [@name], @parser.used_options
assert_equal ["cmd", "-v"], @parser.arguments
end
end

it "ignores everything after --" do
@parser.parse %w(-v -- -v --name lee)
assert_equal [@verbose], @parser.used_options
Expand Down

0 comments on commit 6c0c676

Please sign in to comment.