healthy_options 0.0.0.3
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/README.md +109 -0
- data/VERSION +1 -0
- data/lib/healthy_options.rb +218 -0
- metadata +50 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1ed3f8e766787a98e9435461195c58252cdc4409
|
4
|
+
data.tar.gz: 3be9aff97d9a762fe3c1d5dea09e55c6748cc718
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: db01c61673ba876f03842e653d0ee89a3646de2fb9e77f01b91a3a83895caeb15b77995d1329bb41c0d25409c64acab9f98758799ac47f42c168cfd660e0e09a
|
7
|
+
data.tar.gz: 444c049bdb6a8d43e2d67a389e98b29a225510b27e8b458d1728d8c73b27b8ef745343e7595b7b324d51188e20984c080950c3e86b20ba119f625c7a2c853127
|
data/README.md
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# Notes
|
2
|
+
|
3
|
+
*subject to revision; just capturing some notes for now; pay no heed*
|
4
|
+
|
5
|
+
# arg styles
|
6
|
+
|
7
|
+
* flags always start with dash
|
8
|
+
* could be double-dash long-form e.g. --long-flag
|
9
|
+
* could be single-dash short-form e.g. -l
|
10
|
+
* could be single-dash long-form e.g. -lf
|
11
|
+
|
12
|
+
# Values
|
13
|
+
|
14
|
+
* space, e.g. --long-flag value
|
15
|
+
* equals, e.g. --long-flag=value
|
16
|
+
* smash, e.g. -lvalue
|
17
|
+
|
18
|
+
# Smash flags
|
19
|
+
|
20
|
+
* e.g. ps aux
|
21
|
+
|
22
|
+
|
23
|
+
cases:
|
24
|
+
|
25
|
+
`-lf -lr`
|
26
|
+
|
27
|
+
1. is this flag=l value=f or flag=lf
|
28
|
+
2. check flag=lf first, noting whether we have a value for it
|
29
|
+
3. we don't have a value for flag=lf: no equals, and the next arg is a flag
|
30
|
+
|
31
|
+
|
32
|
+
let's consider the following flags:
|
33
|
+
|
34
|
+
--name, -n, requires a value
|
35
|
+
--enable, -e, no value accepted
|
36
|
+
--net-read, -nr, requires a value
|
37
|
+
|
38
|
+
`-nr -e`
|
39
|
+
|
40
|
+
This can't be --net-read because we don't have a value. It must be name=r.
|
41
|
+
|
42
|
+
--name, -n, requires a value
|
43
|
+
--enable, -e, no value accepted
|
44
|
+
--net-read, -nr, no value accepted
|
45
|
+
|
46
|
+
`-nr -e`
|
47
|
+
|
48
|
+
This could be --net-read or name=r, but we'll take --net-read because it was
|
49
|
+
specific.
|
50
|
+
|
51
|
+
Recommendation:
|
52
|
+
|
53
|
+
1. don't support -nr (one dash for short options, two for long)
|
54
|
+
2. don't support smashing for long options, ever
|
55
|
+
3. support smashing for short options, both flags and any final value
|
56
|
+
4. always handle an = immediately after a recognized flag (which takes a value) as a value assignment
|
57
|
+
|
58
|
+
|
59
|
+
2 primary distinctions:
|
60
|
+
|
61
|
+
* short option or long option
|
62
|
+
* takes a value or not
|
63
|
+
|
64
|
+
if it's a long option, it's easy:
|
65
|
+
1. read 2 dashes
|
66
|
+
2. read until [space] or [equals]
|
67
|
+
3. match flag or fail
|
68
|
+
4. does match take an arg?
|
69
|
+
yes.1 if we have [equals] [value], done.
|
70
|
+
yes.2 if the next arg is not a flag, done.
|
71
|
+
yes.3 otherwise fail
|
72
|
+
no.1 if we have equals, fail
|
73
|
+
no.2 if the next arg is a flag, done
|
74
|
+
no.3 if there are no more args, done
|
75
|
+
no.4 if there are no more flags, done (leave non-flag args alone)
|
76
|
+
no.5 otherwise we have an arg, fail
|
77
|
+
|
78
|
+
|
79
|
+
if it's a short option, we have to consider smashing
|
80
|
+
1. read a single dash followed by alphanum
|
81
|
+
2. confirm the flag and whether it takes a value
|
82
|
+
3. if the next character is a space, look for value match
|
83
|
+
2.a if no value wanted, done
|
84
|
+
2.b if value wanted, fail if no args or next arg is a flag. otherwise done
|
85
|
+
3. if the next character is equals, look for a value match
|
86
|
+
3.a if no value wanted, fail
|
87
|
+
3.b if value wanted, take the right side of the equals, done
|
88
|
+
4. if the next character is an alphanum, then we have either a smashed flag or a smashed value, depending on #2.
|
89
|
+
3.a if no value wanted, then parse next char as a short flag
|
90
|
+
3.b if value wanted, read the rest of the word as a value, done
|
91
|
+
|
92
|
+
|
93
|
+
1. given a string of alphanum, punctuation, and whitespace
|
94
|
+
2. split on whitespace into args consisting of alphanum and punctuation
|
95
|
+
3. an arg is either an option-flag, an option-value,
|
96
|
+
a combination of these 2, or a non-option
|
97
|
+
4. the combinations consist of short-option smashing or flag=value
|
98
|
+
5. options come before non-options
|
99
|
+
6. args flatten to FLAG VALUE NONOPT [[DOUBLEDASH] ANY]
|
100
|
+
7. FLAG can be followed by FLAG or !FLAG
|
101
|
+
8. VALUE must be preceded by FLAG (otherwise it's a NONOPT)
|
102
|
+
9. every arg after a NONOPT must be a NONOPT
|
103
|
+
10. any NONOPT that looks like a flag is forbidden
|
104
|
+
11. unless it's the special DOUBLEDASH
|
105
|
+
|
106
|
+
|
107
|
+
So, split on whitespace. That's handled for us with ARGV.
|
108
|
+
|
109
|
+
Next, look for dashes in the first arg.
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0.3
|
@@ -0,0 +1,218 @@
|
|
1
|
+
class HealthyOptions
|
2
|
+
RGX = {
|
3
|
+
long: %r{
|
4
|
+
\A # starts with
|
5
|
+
-- # double dash
|
6
|
+
(?'flag' # named group
|
7
|
+
(?: # non-capturing group
|
8
|
+
\w+ # word chars
|
9
|
+
-? # possible dash
|
10
|
+
)* # some number of word-word- (possibly none)
|
11
|
+
\w+ # one last word (possibly first as well)
|
12
|
+
) # end flag group
|
13
|
+
}x, # end :long regex
|
14
|
+
|
15
|
+
short: %r{
|
16
|
+
\A # starts with
|
17
|
+
- # single dash
|
18
|
+
(?'flag' # named group
|
19
|
+
\w # word char
|
20
|
+
) # end flag group
|
21
|
+
}x, # end :short regex
|
22
|
+
}
|
23
|
+
|
24
|
+
SPECIALS = {
|
25
|
+
separator: '--',
|
26
|
+
}
|
27
|
+
|
28
|
+
def self.index(flags)
|
29
|
+
idx = { long: {}, short: {}, }
|
30
|
+
flags.each { |flag, cfg|
|
31
|
+
[:long, :short].each { |fld|
|
32
|
+
idx[fld][cfg[fld]] = flag if cfg[fld]
|
33
|
+
}
|
34
|
+
}
|
35
|
+
idx
|
36
|
+
end
|
37
|
+
|
38
|
+
# consider dropping this and inlining calls to it
|
39
|
+
def self.flag?(arg)
|
40
|
+
arg[0] == '-'
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(flags = {})
|
44
|
+
self.flags = flags
|
45
|
+
end
|
46
|
+
|
47
|
+
def flags=(hsh)
|
48
|
+
@flags = {}
|
49
|
+
hsh.each { |flag, cfg|
|
50
|
+
raise("symbol expected for #{flag}") unless flag.is_a?(Symbol)
|
51
|
+
my_cfg = {}
|
52
|
+
cfg.each { |sym, val|
|
53
|
+
raise("symbol expected for #{sym}") unless sym.is_a?(Symbol)
|
54
|
+
my_cfg[sym] = val
|
55
|
+
}
|
56
|
+
@flags[flag] = my_cfg
|
57
|
+
}
|
58
|
+
self.reindex
|
59
|
+
@flags
|
60
|
+
end
|
61
|
+
|
62
|
+
def reindex
|
63
|
+
@index = self.class.index(@flags)
|
64
|
+
end
|
65
|
+
|
66
|
+
def check_flag(arg)
|
67
|
+
# note, #parse has already confirmed self.class.flag?(arg)
|
68
|
+
SPECIALS.each { |sym, val| return [sym, val] if arg == val }
|
69
|
+
flag = nil
|
70
|
+
flag_type = nil
|
71
|
+
|
72
|
+
# check the purported flag against the regex
|
73
|
+
RGX.each { |ft, rgx|
|
74
|
+
if (m = rgx.match(arg))
|
75
|
+
flag = m['flag']
|
76
|
+
flag_type = ft
|
77
|
+
break
|
78
|
+
end
|
79
|
+
}
|
80
|
+
raise "strange flag: #{arg}" unless flag_type # arg doesn't match regex
|
81
|
+
|
82
|
+
# look up the flag in the index
|
83
|
+
sym = @index.dig(flag_type, flag)
|
84
|
+
return [:unknown_flag, flag] unless sym
|
85
|
+
spec = @flags.fetch(sym)
|
86
|
+
|
87
|
+
#
|
88
|
+
# perform validation based on long/short and value/no-value
|
89
|
+
# everything below here returns or raises
|
90
|
+
#
|
91
|
+
|
92
|
+
val = arg.dup
|
93
|
+
first_two = val.slice!(0, 2)
|
94
|
+
|
95
|
+
# consume and validate the flag portion of arg (now val)
|
96
|
+
if flag_type == :short
|
97
|
+
raise "expected #{arg} to lead with -#{flag}" if first_two != "-#{flag}"
|
98
|
+
elsif flag_type == :long
|
99
|
+
raise("expected #{arg} arg to lead with --") if first_two != "--"
|
100
|
+
flagcheck = val.slice!(0, flag.length)
|
101
|
+
if flagcheck != flag
|
102
|
+
raise("expected #{arg} (#{flagcheck}) to match #{flag}")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
if spec[:value]
|
107
|
+
# happy, common case -- the needed value is in the next arg to follow
|
108
|
+
return [:flag_need_val, flag] if val.empty?
|
109
|
+
|
110
|
+
# check for equals -- consume it and take the rest as value
|
111
|
+
if val[0] == '='
|
112
|
+
val.slice!(0, 1)
|
113
|
+
if val.empty?
|
114
|
+
raise("a value is required after = (#{arg})")
|
115
|
+
else
|
116
|
+
return [:flag_has_val, flag, val]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
if flag_type == :short
|
121
|
+
# allow smashed value; the rest of the arg must be the value
|
122
|
+
return [:flag_has_val, flag, val]
|
123
|
+
else
|
124
|
+
raise("could not determine value for #{flag} in #{arg}")
|
125
|
+
end
|
126
|
+
else
|
127
|
+
# no value required
|
128
|
+
return [:flag_no_val, flag] if val.empty?
|
129
|
+
|
130
|
+
if flag_type == :short
|
131
|
+
# next char must not be equals
|
132
|
+
if val[0] == '='
|
133
|
+
raise("#{flag} does not take a value: #{arg}")
|
134
|
+
else
|
135
|
+
return [:flag_no_val_more, flag, val]
|
136
|
+
end
|
137
|
+
else
|
138
|
+
raise("#{flag} does not take a value: #{arg}")
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.pop_value(args)
|
144
|
+
val = args.first
|
145
|
+
raise "args is empty" unless val
|
146
|
+
raise "#{val} is not a value" if self.flag?(val)
|
147
|
+
args.shift
|
148
|
+
end
|
149
|
+
|
150
|
+
# this is a recursive method
|
151
|
+
# opts tends to grow while args shrinks
|
152
|
+
def parse(args, opts = {})
|
153
|
+
return [args, opts] if args.empty?
|
154
|
+
return [args, opts] unless self.class.flag?(args.first)
|
155
|
+
|
156
|
+
res, flag, value = self.check_flag(args.first)
|
157
|
+
# puts "\n\nDEBUG: #{res} #{flag} #{value}"
|
158
|
+
|
159
|
+
return [args, opts] if res == :separator
|
160
|
+
raise("flag expected for #{res}") unless flag # sanity check
|
161
|
+
|
162
|
+
# TODO: we should know by now whether it's a long or short flag
|
163
|
+
sym = @index[:long][flag] || @index[:short][flag]
|
164
|
+
raise "unrecognized flag: #{flag}" unless sym
|
165
|
+
args.shift
|
166
|
+
|
167
|
+
case res
|
168
|
+
when :flag_has_val
|
169
|
+
raise("value expected") unless value
|
170
|
+
opts[sym] = value
|
171
|
+
when :flag_need_val
|
172
|
+
opts[sym] = self.class.pop_value(args)
|
173
|
+
when :flag_no_val
|
174
|
+
opts[sym] = true
|
175
|
+
when :flag_no_val_more
|
176
|
+
opts[sym] = true
|
177
|
+
raise("more exected for #{flag} parsed as #{res}") unless value
|
178
|
+
# look for smashed flags
|
179
|
+
self.parse_smashed(value).each { |smflag, smval|
|
180
|
+
# the last smashed flag may need a val from args
|
181
|
+
opts[smflag] = smval || self.class.pop_value(args)
|
182
|
+
}
|
183
|
+
else
|
184
|
+
raise "unknown result: #{res}"
|
185
|
+
end
|
186
|
+
self.parse(args, opts)
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
|
191
|
+
# original args: -af=5
|
192
|
+
# -af 5
|
193
|
+
def parse_smashed(arg)
|
194
|
+
opts = {}
|
195
|
+
# preceding dash and flag have been removed
|
196
|
+
val = arg.dup
|
197
|
+
loop {
|
198
|
+
break if val.empty?
|
199
|
+
char = val.slice!(0, 1)
|
200
|
+
sym = @index[:short][char]
|
201
|
+
raise "unknown flag smashed in: #{char} in #{arg}" unless sym
|
202
|
+
spec = @flags.fetch(sym)
|
203
|
+
# TODO: error handling (punctuation, -p5 -5p, etc)
|
204
|
+
if spec[:value]
|
205
|
+
val.slice!(0, 1) if val[0] == '='
|
206
|
+
if val.empty?
|
207
|
+
opts[sym] = nil # indicate to parse we need another arg; ugh, hack!
|
208
|
+
else
|
209
|
+
opts[sym] = val
|
210
|
+
end
|
211
|
+
break # a value always ends smashing
|
212
|
+
else
|
213
|
+
opts[sym] = true
|
214
|
+
end
|
215
|
+
}
|
216
|
+
opts
|
217
|
+
end
|
218
|
+
end
|
metadata
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: healthy_options
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rick Hull
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-01-29 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |
|
14
|
+
* long and short options
|
15
|
+
* value or not
|
16
|
+
* use equals for value or not
|
17
|
+
* short option smashing
|
18
|
+
email:
|
19
|
+
executables: []
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- README.md
|
24
|
+
- VERSION
|
25
|
+
- lib/healthy_options.rb
|
26
|
+
homepage: https://github.com/rickhull/healthy_options
|
27
|
+
licenses:
|
28
|
+
- GPL-3.0
|
29
|
+
metadata: {}
|
30
|
+
post_install_message:
|
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: '2'
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubyforge_project:
|
46
|
+
rubygems_version: 2.5.2
|
47
|
+
signing_key:
|
48
|
+
specification_version: 4
|
49
|
+
summary: Parse a wide but limited variety of command line option styles
|
50
|
+
test_files: []
|