Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse incrementally, enabling subcommands #281

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Parse incrementally, enabling subcommands:
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
commit 6c0c676e6921cfa415fc6d73e17501b342ec11e8
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