healthy_options 0.0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +109 -0
  3. data/VERSION +1 -0
  4. data/lib/healthy_options.rb +218 -0
  5. metadata +50 -0
@@ -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
@@ -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: []