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.
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: []