strop 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +150 -0
- data/lib/strop/version.rb +5 -0
- data/lib/strop.rb +205 -0
- metadata +48 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c7e4c1b959f079ed2beb07575918ae878b23051fa8fabd983e346aef8b340223
|
|
4
|
+
data.tar.gz: 88366bb7a1b7bf57328fd9a149e2b21c61b052508155a84d48e286e87d323550
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ea8491685f9edebf4364c96886c1ab18cca85d628306ed2fbd76827cc7b1db06b8021851db33e2b337d765805700c12e6bf80e4e4d5479886818654d35f6c566
|
|
7
|
+
data.tar.gz: c14c0ac725fd9530adc4eaf474c142218c1f8eb88001de14907b34d79ee29db15564f535eb6d41dadffc5a7cd3b3cfe42fbaa8a73d7d46d8fcdf7a3c5ab94bd0
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Caio Chassot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Strop: Command-line Option Parser
|
|
2
|
+
|
|
3
|
+
## Distinctive features
|
|
4
|
+
|
|
5
|
+
- Build options from parsing help text, instead of the other way around
|
|
6
|
+
- Pattern matching for result processing (with `case`/`in …`)
|
|
7
|
+
|
|
8
|
+
## Core workflow
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
opts = Optlist.from_help(help_text) # extract from help
|
|
12
|
+
result = Strop.parse(opts, ARGV) # parse argv -> Result
|
|
13
|
+
result = Strop.parse!(opts, ARGV) # same but on error prints message and exits
|
|
14
|
+
result = Strop.parse!(help_text) # Automatically parse help text, ARGV default
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Processing parsed results
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
Strop.parse!(help).each do |item|
|
|
21
|
+
case item
|
|
22
|
+
in Strop::Opt[label: "help"] then show_help
|
|
23
|
+
in Strop::Opt[label: "verbose", value:] then set_verbose(value)
|
|
24
|
+
in Strop::Opt[label: "output", value: nil] then output = :stdout
|
|
25
|
+
in Strop::Opt[label: "color"] then item.no? ? disable_color : enable_color
|
|
26
|
+
in Strop::Arg[value:] then files << value
|
|
27
|
+
in Strop::Sep then break
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or, more succinctly:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
Strop.parse!(help).each do |item|
|
|
36
|
+
case item
|
|
37
|
+
in label: "help" then show_help # only Opt has .label
|
|
38
|
+
in arg: then files << arg # `value:` might match an Opt, so Arg offters alias .arg
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
You can generate the case expression above with:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
puts Strop::Optlist.from_help(help_text).to_s(:case)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Result members (Array of Opt, Arg, Sep)
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
res.opts # all Opt objects
|
|
54
|
+
res.args # all Arg objects
|
|
55
|
+
res.rest # args after -- separator
|
|
56
|
+
res["flag"] # find opt by name
|
|
57
|
+
|
|
58
|
+
opt = res.opts.first
|
|
59
|
+
arg = res.args.first
|
|
60
|
+
opt.decl # Optdecl matched for this Opt instance
|
|
61
|
+
opt.name # name used in invocation (could differ from label)
|
|
62
|
+
opt.value # argument passed to this option
|
|
63
|
+
opt.label # primary name (first long name or first name), used for pattern matching
|
|
64
|
+
opt.no? # true if --no-foo variant used
|
|
65
|
+
opt.yes? # opposite of `no?`
|
|
66
|
+
arg.value # positional argument
|
|
67
|
+
arg.arg # same as .value, useful for pattern matching
|
|
68
|
+
Sep # -- end of options marker; Const, not instantiated
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Notice that parsing `--[no-]flag` from help results in a single `Optdecl[:flag, :"no-flag"]`, and you can use `yes?`/`no?` to check which was passed.
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## Help text format for parsing
|
|
75
|
+
|
|
76
|
+
Auto-extracts indented option lines from help:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Options:
|
|
80
|
+
-f, --flag Flag
|
|
81
|
+
-v, --verbose LEVEL Required arg
|
|
82
|
+
-o, --output [FILE] Optional arg
|
|
83
|
+
--color=MODE Optional with =
|
|
84
|
+
--debug[=LEVEL] Required/optional with =
|
|
85
|
+
--[no-]quiet --quiet/--no-quiet pair
|
|
86
|
+
--[no]force --force/--noforce pair
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
By default `parse_help` expects options to be indented by 2 or 4 spaces. Override with:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
Strop.parse_help(text, pad: / {6}/) # 6 spaces exactly
|
|
93
|
+
Strop.parse_help(text, pad: /\t/) # tabs ????
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Use at least two spaces before description, and only a single space before args.
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
--file PATH # !! PATH seen as description and ignored, --file considered a flag (no arg)
|
|
100
|
+
--quiet Supresses output # !! interpreted as --quiet=Supresses
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The latter case is detected and a warning is printed, but best to avoid this situation altogether.
|
|
104
|
+
|
|
105
|
+
## Command-line parsing features
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
cmd -abc # short option clumping (-a -b -c)
|
|
109
|
+
cmd -fVAL, --foo=VAL # attached values
|
|
110
|
+
cmd -f VAL, --foo VAL # separate values
|
|
111
|
+
cmd --foo val -- --bar # --bar becomes positional after --
|
|
112
|
+
cmd --intermixed args and --options # flexible ordering
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Manual option declaration building
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
Optdecl[:f] # flag only: -f
|
|
119
|
+
Optdecl[:f?] # optional arg: -f [X]
|
|
120
|
+
Optdecl[:f!] # required arg: -f x
|
|
121
|
+
Optdecl[:f, :foo] # multiple names: -f or --foo
|
|
122
|
+
Optdecl[:f?, :foo] # multiple + arg modifier: use ?/! only on first
|
|
123
|
+
Optdecl[:f?, :foo?] # so this will result in -f, --foo?
|
|
124
|
+
Optdecl[:f, :foo, arg: :may] # explicit arg form: --foo [ARG]
|
|
125
|
+
Optdecl[:?, arg: :shant] # explicit form allows using ?/! in option name: -?
|
|
126
|
+
Optdecl[:foo_bar] # --foo-bar: Underscores in symbol names get replaced with `-`
|
|
127
|
+
Optdecl["foo_bar"] # --foo_bar: but not in strings.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Option lists:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
optlist = Optlist[optdecl1, optdecl2] # combine decls into optlist
|
|
134
|
+
optlist["f"] # lookup by name
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Argument requirements
|
|
138
|
+
|
|
139
|
+
- `:shant` - no argument allowed
|
|
140
|
+
- `:may` - optional argument (takes next token if not option-like)
|
|
141
|
+
- `:must` - required argument (error if missing)
|
|
142
|
+
|
|
143
|
+
## Adding hidden options
|
|
144
|
+
|
|
145
|
+
If you want to use `parse_help` mainly, but need secret options:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
optlist = Strop.parse_help HELP
|
|
149
|
+
optlist << Optdecl[:D, :debug]
|
|
150
|
+
```
|
data/lib/strop.rb
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# require "debug"; DEBUGGER__.add_catch_breakpoint "Exception"
|
|
4
|
+
require_relative "strop/version"
|
|
5
|
+
|
|
6
|
+
module Strop
|
|
7
|
+
|
|
8
|
+
Optdecl = Data.define(:names, :arg, :label) do
|
|
9
|
+
def self.[](*names, arg: nil) = new(names:, arg:)
|
|
10
|
+
def initialize(names:, arg: nil)
|
|
11
|
+
names = [*names].map{ Symbol === it ? it.to_s.gsub(?_, ?-) : it } # :foo_bar to "foo-bar" for symbols
|
|
12
|
+
names[0] = names[0].sub(/[!?]$/, "") unless arg # opt? / opt! to opt, and... (unless arg given)
|
|
13
|
+
arg ||= { ?? => :may, ?! => :must }[$&] || :shant # use ?/! to determine arg (unless arg given)
|
|
14
|
+
label = names.find{ it.size > 1 } || names.first # the canonical name used to search for it
|
|
15
|
+
%i[must may shant].member? arg or raise "invalid arg" # validate arg
|
|
16
|
+
super names:, arg:, label:
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def no? = names.each_cons(2).any?{|a,b| b =~ /\Ano-?#{Regexp.escape a}\z/ } # is a flag like --[no-]foo, --[no]bar
|
|
20
|
+
def arg? = self.arg != :shant # accepts arg
|
|
21
|
+
def arg! = self.arg == :must # requires arg
|
|
22
|
+
def to_s = names.map{ (it[1] ? "--" : "-")+it }.join(", ") + { must: " X", may: " [X]", shant: "" }[arg]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Optlist < Array # a list of Optdecls
|
|
26
|
+
def self.from_help(doc) = Strop.parse_help(doc)
|
|
27
|
+
def [](k, ...) = [String, Symbol].any?{ it === k } ? self.find{ it.names.member? k.to_s } : super(k, ...)
|
|
28
|
+
def to_s(as=:plain)
|
|
29
|
+
case as
|
|
30
|
+
when :plain then join("\n")
|
|
31
|
+
when :case
|
|
32
|
+
caseins = map{|os| "in label: #{os.label.inspect}".tap{ it << ", value:" if os.arg? }}
|
|
33
|
+
len = caseins.map(&:size).max
|
|
34
|
+
caseins = caseins.zip(self).map{ |s,o| s.ljust(len) + " then#{' opt.no?' if o.no?} # #{o}" }
|
|
35
|
+
<<~RUBY
|
|
36
|
+
for item in Strop.parse!(optlist)
|
|
37
|
+
case item
|
|
38
|
+
#{caseins.map{ " #{it}" }.join("\n").lstrip}
|
|
39
|
+
case Strop::Arg[value:] then
|
|
40
|
+
case Strop::Sep then break # if you want to handle result.rest separately
|
|
41
|
+
else raise "Unhandled result \#{item}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
RUBY
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
Arg = Data.define :value, :arg do
|
|
51
|
+
def initialize(value:) = super(value:, arg: value)
|
|
52
|
+
alias to_s value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Opt = Data.define :decl, :name, :value, :label, :no do
|
|
56
|
+
def initialize(decl:, name:, value: nil)
|
|
57
|
+
label = decl.label # repeated here so can be pattern-matched against in case/in
|
|
58
|
+
no = name =~ /\Ano-?/ && decl.names.member?($') # flag given in negated version: (given --no-foo and also accepts --foo)
|
|
59
|
+
super(decl:, name:, value:, label:, no: !!no)
|
|
60
|
+
end
|
|
61
|
+
alias no? no
|
|
62
|
+
def yes? = !no?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
Sep = :end_marker
|
|
66
|
+
|
|
67
|
+
# for debugging only, TODO remove later probably
|
|
68
|
+
class Arg
|
|
69
|
+
def encode_with(coder) = (coder.scalar = self.value; coder.tag = nil)
|
|
70
|
+
end
|
|
71
|
+
class Opt
|
|
72
|
+
def encode_with(coder) = (coder.map = { self.name => self.value }; coder.tag = nil)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
module Exports
|
|
77
|
+
Optlist = Strop::Optlist
|
|
78
|
+
Optdecl = Strop::Optdecl
|
|
79
|
+
Opt = Strop::Opt
|
|
80
|
+
Arg = Strop::Arg
|
|
81
|
+
Sep = Strop::Sep
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class Result < Array # of Opt, Arg, Sep
|
|
85
|
+
def rest = drop_while{ it != Sep }.drop(1) # args after sep
|
|
86
|
+
def args = Result.new(select { Arg === it })
|
|
87
|
+
def opts = Result.new(select { Opt === it })
|
|
88
|
+
def [](k, ...)
|
|
89
|
+
case k
|
|
90
|
+
when String, Symbol then find{ Opt === it && it.decl.names.member?(k.to_s) }
|
|
91
|
+
else super(k, ...)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class Unreachable < RuntimeError; end
|
|
97
|
+
class OptionError < ArgumentError; end
|
|
98
|
+
|
|
99
|
+
def self.parse(optlist, argv=ARGV)
|
|
100
|
+
Array === argv && argv.all?{ String === it } or raise "argv must be an array of strings (given #{argv.class})"
|
|
101
|
+
optlist = case optlist
|
|
102
|
+
when IO then parse_help(optlist.read)
|
|
103
|
+
when String then parse_help(optlist)
|
|
104
|
+
when Optlist then optlist
|
|
105
|
+
else raise "optlist must be an Optlist or help text (given #{optlist.class})"
|
|
106
|
+
end
|
|
107
|
+
tokens = argv.dup
|
|
108
|
+
res = Result.new
|
|
109
|
+
ctx = :top
|
|
110
|
+
name, token, opt = nil
|
|
111
|
+
rx_value = /\A[^-]|\A\z/
|
|
112
|
+
loop do
|
|
113
|
+
case ctx
|
|
114
|
+
when :end then return res.concat tokens.map{ Arg[it] } # opt parsing ended, rest is positional args
|
|
115
|
+
when :value then ctx = :top; res << Arg[token] # interspersed positional arg amidst opts
|
|
116
|
+
|
|
117
|
+
when :top
|
|
118
|
+
token = tokens.shift or next ctx = :end # next token or done
|
|
119
|
+
case token
|
|
120
|
+
when "--" then ctx = :end; res << Sep # end of options
|
|
121
|
+
when /\A--(.+)\z/m then token, ctx = $1, :long # long (--foo, --foo xxx), long with attached value (--foo=xxx)
|
|
122
|
+
when /\A-(.+)\z/m then token, ctx = $1, :short # short or clump (-a, -abc)
|
|
123
|
+
when rx_value then ctx = :value # value
|
|
124
|
+
else raise Unreachable
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
when :long
|
|
128
|
+
name, value = token =~ /\A(.*?)=/m ? [$1, $'] : [token, nil]
|
|
129
|
+
opt = optlist[name] or raise OptionError, "Unknown option: --#{name}"
|
|
130
|
+
case
|
|
131
|
+
when opt.arg? && value then ctx = :top; res << Opt[opt, name, value] # --foo=XXX
|
|
132
|
+
when !opt.arg? && !value then ctx = :top; res << Opt[opt, name] # --foo
|
|
133
|
+
when opt.arg? && !value then ctx = :arg # --foo XXX
|
|
134
|
+
when !opt.arg? && value then raise OptionError, "Option --#{name} takes no argument"
|
|
135
|
+
else raise Unreachable
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
when :short
|
|
139
|
+
name, token = token[0], token[1..].then{ it != "" ? it : nil } # -abc -> a, bc
|
|
140
|
+
opt = optlist[name] or raise OptionError, "Unknown option: -#{name}"
|
|
141
|
+
case
|
|
142
|
+
when opt.arg? && token then ctx = :top; res << Opt[opt, name, token] # -aXXX
|
|
143
|
+
when !opt.arg? && !token then ctx = :top; res << Opt[opt, name] # end of -abc
|
|
144
|
+
when opt.arg? && !token then ctx = :arg # -a XXX
|
|
145
|
+
when !opt.arg? && token then res << Opt[opt, name] # -abc -> took -a, will parse -bc
|
|
146
|
+
else raise Unreachable
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
when :arg
|
|
150
|
+
token = tokens[0]&.=~(rx_value) ? tokens.shift : nil
|
|
151
|
+
case
|
|
152
|
+
when opt.arg! && !token then raise OptionError, "Expected argument for option -#{?- if name[1]}#{name}" # --req missing value
|
|
153
|
+
when opt.arg! && token then ctx = :top; res << Opt[opt, name, token] # --req val
|
|
154
|
+
when opt.arg? && token then ctx = :top; res << Opt[opt, name, token] # --opt val
|
|
155
|
+
when opt.arg? && !token then ctx = :top; res << Opt[opt, name] # --opt followed by --foo, --opt as last token
|
|
156
|
+
else raise Unreachable
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
else raise Unreachable
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.parse!(...) # same but catches errors, print msg, exit
|
|
165
|
+
parse(...)
|
|
166
|
+
rescue OptionError => e
|
|
167
|
+
$stderr.puts e.message
|
|
168
|
+
exit 1
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
RX_SOARG = /\[\S+?\]/ # short opt optional arg
|
|
173
|
+
RX_SARG = /[^\s,]+/ # short opt required arg
|
|
174
|
+
RX_LOARG = /\[=\S+?\]| #{RX_SOARG}/ # long opt optional arg: --foo[=bar] or --foo [bar]
|
|
175
|
+
RX_LARG = /[ =]#{RX_SARG}/ # long opt required arg: --foo=bar or --foo bar
|
|
176
|
+
RX_NO = /\[no-?\]/ # prefix for --[no-]flags
|
|
177
|
+
RX_SOPT = /-[^-\s,](?: (?:#{RX_SOARG}|#{RX_SARG}))?/ # full short opt
|
|
178
|
+
RX_LOPT = /--(?=[^-=,\s])#{RX_NO}?[^\s=,\[]+(?:#{RX_LOARG}|#{RX_LARG})?/ # full long opt
|
|
179
|
+
RX_OPT = /#{RX_SOPT}|#{RX_LOPT}/ # either opt
|
|
180
|
+
RX_OPTS = /#{RX_OPT}(?:, {0,2}#{RX_OPT})*/ # list of opts, comma separated
|
|
181
|
+
|
|
182
|
+
def self.parse_help(help, pad: /(?: ){1,2}/)
|
|
183
|
+
help.scan(/^#{pad}(#{RX_OPTS})(.*)/).map do |line, rest| # get all opts lines
|
|
184
|
+
# Ambiguous: --opt Desc with only one space before will interpret "Desc" as arg.
|
|
185
|
+
if rest =~ /^ \S/ && line =~ / (#{RX_SARG})$/ # desc preceeded by sringle space && last arg is " "+word. Capture arg name
|
|
186
|
+
$stderr.puts "#{$1.inspect} was interpreted as argument, In #{(line+rest).inspect}. Use at least two spaces before description to avoid this warning."
|
|
187
|
+
end
|
|
188
|
+
line.scan(RX_OPT).map do |opt| # take options from each line
|
|
189
|
+
opt.split(/(?=\[=)|=| +/, 2) # separate name from arg
|
|
190
|
+
end.map do |name, arg| # remove opt markers -/--, transform arg str into requirement
|
|
191
|
+
[name.sub(/^--?/, ''), arg.nil? ? :shant : arg[0] == "[" ? :may : :must]
|
|
192
|
+
end.transpose # [[name,arg], ...] -> [names, args]
|
|
193
|
+
.then do |names, args| # handle -f,--foo=x style (without arg on short opt); expand --[no]flag into --flag and --noflag (also --[no-])
|
|
194
|
+
args = args.uniq.tap{ it.delete :shant if it.size > 1 } # delete excess :shant (from -f in -f,--foo=x)
|
|
195
|
+
raise "Option #{names} has conflicting arg requirements: #{args}" if args.size > 1 # raise if still conflict, like -f X, --ff [X]
|
|
196
|
+
[(names.flat_map{|f| f.start_with?(RX_NO) ? [$', $&[1...-1] + $'] : f }).uniq, args[0]] # [flags and noflags, resolved single arg]
|
|
197
|
+
end
|
|
198
|
+
end.uniq.tap do |list| # [[[name, name, ...], arg, more opts ...]
|
|
199
|
+
dupes = list.flat_map(&:first).tally.reject{|k,v|v==1}
|
|
200
|
+
raise "Options #{dupes.keys.inspect} seen more than once in distinct definitions" if dupes.any?
|
|
201
|
+
end.map{ |names, arg| Optdecl[*names, arg:] }.then{ Optlist[*it] }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: strop
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Caio Chassot
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Build options from parsing help text, with pattern matching for result
|
|
13
|
+
processing
|
|
14
|
+
email: dev@caiochassot.com
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- MIT-LICENSE
|
|
20
|
+
- README.md
|
|
21
|
+
- lib/strop.rb
|
|
22
|
+
- lib/strop/version.rb
|
|
23
|
+
homepage: http://github.com/kch/strop
|
|
24
|
+
licenses:
|
|
25
|
+
- MIT
|
|
26
|
+
metadata:
|
|
27
|
+
homepage_uri: http://github.com/kch/strop
|
|
28
|
+
source_code_uri: http://github.com/kch/strop
|
|
29
|
+
bug_tracker_uri: http://github.com/kch/strop/issues
|
|
30
|
+
documentation_uri: https://rubydoc.info/gems/strop
|
|
31
|
+
rdoc_options: []
|
|
32
|
+
require_paths:
|
|
33
|
+
- lib
|
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '3.3'
|
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
requirements: []
|
|
45
|
+
rubygems_version: 3.7.2
|
|
46
|
+
specification_version: 4
|
|
47
|
+
summary: Command-line option parser
|
|
48
|
+
test_files: []
|