healthy_options 0.0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|