opt_parse_builder 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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rspec +1 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +35 -0
  8. data/LICENSE +23 -0
  9. data/README.md +434 -0
  10. data/Rakefile +13 -0
  11. data/examples/hello_world.rb +20 -0
  12. data/lib/opt_parse_builder.rb +156 -0
  13. data/lib/opt_parse_builder/argument.rb +62 -0
  14. data/lib/opt_parse_builder/argument_builder.rb +193 -0
  15. data/lib/opt_parse_builder/argument_bundle.rb +30 -0
  16. data/lib/opt_parse_builder/argument_bundle_builder.rb +34 -0
  17. data/lib/opt_parse_builder/argument_values.rb +60 -0
  18. data/lib/opt_parse_builder/banner_argument.rb +11 -0
  19. data/lib/opt_parse_builder/constant_argument.rb +16 -0
  20. data/lib/opt_parse_builder/errors.rb +10 -0
  21. data/lib/opt_parse_builder/formats_operand_name.rb +9 -0
  22. data/lib/opt_parse_builder/has_value.rb +21 -0
  23. data/lib/opt_parse_builder/null_argument.rb +4 -0
  24. data/lib/opt_parse_builder/option_argument.rb +33 -0
  25. data/lib/opt_parse_builder/optional_operand_argument.rb +29 -0
  26. data/lib/opt_parse_builder/parser.rb +345 -0
  27. data/lib/opt_parse_builder/parser_builder.rb +17 -0
  28. data/lib/opt_parse_builder/required_operand_argument.rb +32 -0
  29. data/lib/opt_parse_builder/separator_argument.rb +11 -0
  30. data/lib/opt_parse_builder/splat_operand_argument.rb +22 -0
  31. data/lib/opt_parse_builder/stable_sort.rb +13 -0
  32. data/lib/opt_parse_builder/version.rb +6 -0
  33. data/opt_parse_builder.gemspec +35 -0
  34. data/rake/bundler.rake +1 -0
  35. data/rake/default.rake +1 -0
  36. data/rake/rdoc.rake +7 -0
  37. data/rake/spec.rake +3 -0
  38. data/rake/test.rake +2 -0
  39. metadata +126 -0
@@ -0,0 +1,34 @@
1
+ module OptParseBuilder
2
+
3
+ # Yielded by OptParseBuilder.bundle_arguments to create an
4
+ # ArgumentBundle, a collection of arguments that can be treated as
5
+ # through it is one argument.
6
+ class ArgumentBundleBuilder
7
+
8
+ def initialize # :nodoc:
9
+ @argument_bundle = ArgumentBundle.new
10
+ end
11
+
12
+ # Add an argument to the bundle. Takes either the argument to
13
+ # add, or yields an ArgumentBuilder which builds a new argument
14
+ # and adds it.
15
+ #
16
+ # If adding an existing argument, that argument may itself be an
17
+ # ArgumentBundle.
18
+ def add(argument = nil, &block)
19
+ unless argument.nil? ^ block.nil?
20
+ raise BuildError, "Need exactly 1 of arg and block"
21
+ end
22
+ if argument
23
+ @argument_bundle << argument
24
+ else
25
+ @argument_bundle << OptParseBuilder.build_argument(&block)
26
+ end
27
+ end
28
+
29
+ def argument # :nodoc:
30
+ @argument_bundle.simplify
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,60 @@
1
+ module OptParseBuilder
2
+
3
+ # Like OpenStruct, in that it allows access as through either a Hash
4
+ # or a Struct, but raises an error if you try to read a value that
5
+ # has not been set.
6
+ #
7
+ # Strings and symbols may be interchanged freely for hash access.
8
+ #
9
+ # A value may only by set using hash syntax:
10
+ #
11
+ # arg_values = ArgumentValues.new
12
+ # arg_values[:one] = 1
13
+ # arg_values["two"] = 2
14
+ #
15
+ # But may be retrieved using hash syntax:
16
+ #
17
+ # arg_values["one"] # => 1
18
+ # arg_values[:two] # => 2
19
+ #
20
+ # or struct syntax:
21
+ #
22
+ # arg_values.one # => 1
23
+ # arg_values.two # => 2
24
+ class ArgumentValues
25
+
26
+ # Create an empty instance.
27
+ def initialize
28
+ @h = {}
29
+ end
30
+
31
+ # Return true if the collection is empty.
32
+ def empty?
33
+ @h.empty?
34
+ end
35
+
36
+ # Return true if the collection contains the key, which may be
37
+ # either a symbol or a string.
38
+ def has_key?(key)
39
+ @h.has_key?(key.to_sym)
40
+ end
41
+
42
+ # Set a key to a value. The key may be either a string or a
43
+ # symbol.
44
+ def []=(key, value)
45
+ @h[key.to_sym] = value
46
+ end
47
+
48
+ # Get a value. The key may be either a string or a symbol.
49
+ # Raises KeyError if the collection does not have that key.
50
+ def [](key)
51
+ @h.fetch(key.to_sym)
52
+ end
53
+
54
+ def method_missing(method, *args) # :nodoc:
55
+ return super unless has_key?(method)
56
+ self[method]
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,11 @@
1
+ module OptParseBuilder
2
+ class BannerArgument < Argument # :nodoc:
3
+
4
+ attr_reader :banner_lines
5
+
6
+ def initialize(banner_lines)
7
+ @banner_lines = banner_lines
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module OptParseBuilder
2
+ class ConstantArgument < Argument # :nodoc:
3
+
4
+ attr_reader :key
5
+ attr_reader :value
6
+
7
+ def initialize(key, value)
8
+ unless key
9
+ raise BuildError, "default requires a key"
10
+ end
11
+ @key = key
12
+ @value = value
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ module OptParseBuilder
2
+
3
+ # The base class for all exceptions directly raised by this library.
4
+ class Error < StandardError ; end
5
+
6
+ # Exception raised for an error when building a parser, argument or
7
+ # argument bundle.
8
+ class BuildError < Error ; end
9
+
10
+ end
@@ -0,0 +1,9 @@
1
+ module OptParseBuilder
2
+ module FormatsOperandName # :nodoc:
3
+
4
+ def format_operand_name(s)
5
+ s.to_s.gsub(/_/, " ")
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ module OptParseBuilder
2
+ module HasValue # :nodoc:
3
+
4
+ attr_reader :key
5
+ attr_accessor :value
6
+
7
+ def init_value(key, default)
8
+ unless key
9
+ raise BuildError, "argument with value requires a key"
10
+ end
11
+ @key = key
12
+ @default = default
13
+ reset
14
+ end
15
+
16
+ def reset
17
+ @value = @default
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,4 @@
1
+ module OptParseBuilder
2
+ class NullArgument < Argument # :nodoc:
3
+ end
4
+ end
@@ -0,0 +1,33 @@
1
+ module OptParseBuilder
2
+ class OptionArgument < Argument # :nodoc:
3
+
4
+ include HasValue
5
+
6
+ DEFAULT_HANDLER = ->(argument, value) { argument.value = value }
7
+
8
+ def initialize(key, default, on, handler)
9
+ init_value(key, default)
10
+ @on = on
11
+ @handler = handler || DEFAULT_HANDLER
12
+ end
13
+
14
+ def apply_option(op)
15
+ op.on(*edited_on) do |value|
16
+ @handler.call(self, value)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def edited_on
23
+ @on.map do |s|
24
+ if s.respond_to?(:gsub!)
25
+ s.gsub(/_DEFAULT_/, @default.to_s)
26
+ else
27
+ s
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ module OptParseBuilder
2
+ class OptionalOperandArgument < Argument # :nodoc:
3
+
4
+ include FormatsOperandName
5
+ include HasValue
6
+
7
+ def initialize(key, default, help_name)
8
+ init_value(key, default)
9
+ @help_name = help_name || key
10
+ end
11
+
12
+ def operand_notation
13
+ "[<#{format_operand_name(@help_name)}>]"
14
+ end
15
+
16
+ def shift_operand(argv)
17
+ @value = argv.shift
18
+ end
19
+
20
+ def optional
21
+ self
22
+ end
23
+
24
+ def required
25
+ RequiredOperandArgument.new(@key, @default, @help_name)
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,345 @@
1
+ require "optparse"
2
+
3
+ require_relative "stable_sort"
4
+
5
+ module OptParseBuilder
6
+
7
+ # A command-line parser. Create an instance of this by calling
8
+ # OptParseBuilder.build_parser.
9
+ #
10
+ # Note: The constructor for this class is not part of the public
11
+ # API.
12
+ class Parser
13
+
14
+ include StableSort
15
+
16
+ # Controls whether unparsed arguments are an error.
17
+ #
18
+ # If `false` (the default), then unparsed arguments cause an
19
+ # error:
20
+ #
21
+ # arg_parser = OptParseBuilder.build_parser do |args|
22
+ # args.allow_unparsed_operands = false
23
+ # args.add do |arg|
24
+ # arg.key :quiet
25
+ # arg.on "-q", "--quiet", "Suppress normal output"
26
+ # end
27
+ # end
28
+ #
29
+ # ARGV = ["-q", "/tmp/file1", "/tmp/file2"]
30
+ # arg_values = arg_parser.parse!
31
+ # # aborts with "needless argument: /tmp/file1"
32
+ #
33
+ # If `true`, then unparsed operands are not considered an error, and
34
+ # they remain unconsumed. Use this setting when you want unparsed
35
+ # operands to remain in `ARGV` so that they can be used by, for
36
+ # example, `ARGF`:
37
+ #
38
+ # arg_parser = OptParseBuilder.build_parser do |args|
39
+ # args.allow_unparsed_operands = true
40
+ # args.add do |arg|
41
+ # arg.key :quiet
42
+ # arg.on "-q", "--quiet", "Suppress normal output"
43
+ # end
44
+ # end
45
+ #
46
+ # ARGV = ["-q", "/tmp/file1", "/tmp/file2"]
47
+ # arg_values = arg_parser.parse!
48
+ # # ARGV now equals ["/tmp/file1", "/tmp/file2"]
49
+ # ARGF.each_line do |line|
50
+ # puts line unless arg_values.quiet
51
+ # end
52
+ attr_accessor :allow_unparsed_operands
53
+
54
+ def initialize # :nodoc:
55
+ @arguments = []
56
+ @allow_unparsed_operands = false
57
+ end
58
+
59
+ # Reset to the state after construction, before #parse! was called.
60
+ # Each argument is set to its default value. An argument with no
61
+ # explicit default is set to `nil`.
62
+ #
63
+ # This is called implicitly when you call #parse!, so there's seldom
64
+ # any need for it to be called explicitly.
65
+ def reset
66
+ @arguments.each(&:reset)
67
+ sort_arguments
68
+ end
69
+
70
+ # Parse arguments, consuming them from the array.
71
+ #
72
+ # After parsing, there are numerous ways to access the value of the arguments:
73
+ #
74
+ # arg_parser = OptParseBuilder.build_parser do |args|
75
+ # args.add do |arg|
76
+ # arg.key :num
77
+ # arg.on "--num=N", Integer, "A number"
78
+ # end
79
+ # end
80
+ # arg_values = arg_parser.parse!(["--num=123"])
81
+ # p arg_parser[:num] # => 123
82
+ # p arg_parser["num"] # => 123
83
+ # p arg_values[:num] # => 123
84
+ # p arg_values["num"] # => 123
85
+ # p arg_values.num # => 123
86
+ #
87
+ # If there are operands (positional arguments) in the array that are
88
+ # not consumed, an error normally results. This behavior can be
89
+ # changed using #allow_unparsed_operands.
90
+ #
91
+ # The #parse! method defaults to parsing `ARGV`, which is what is
92
+ # usually wanted, but you can pass in any array of strings, as the
93
+ # above example does.
94
+ #
95
+ # Design note: A method that modifies its argument _and_ modifies
96
+ # its object _and_ returns a value is not the best design, violating
97
+ # the good principle of command-query separation. However, that
98
+ # violation is more useful in this case than it is sinful, and it's
99
+ # the only place in this library that violates that principle.
100
+ def parse!(argv = ARGV)
101
+ reset
102
+ begin
103
+ op = optparse
104
+ op.parse!(argv)
105
+ @arguments.each do |arg|
106
+ arg.shift_operand(argv)
107
+ end
108
+ unless @allow_unparsed_operands || argv.empty?
109
+ raise OptionParser::NeedlessArgument, argv.first
110
+ end
111
+ values
112
+ rescue OptionParser::ParseError => e
113
+ abort e.message
114
+ end
115
+ end
116
+
117
+ # Add a line to the banner. The banner is text that appears at the
118
+ # top of the help text.
119
+ #
120
+ # A new-line will automatically be added to the end of the line.
121
+ # Although it's called a "line," you can embed new-lines in it so
122
+ # that it is actually more than one line.
123
+ #
124
+ # This example:
125
+ #
126
+ # arg_parser = OptParseBuilder.build_parser do |args|
127
+ # args.banner "This is my program"
128
+ # args.banner <<~BANNER
129
+ # There are many programs like it,
130
+ # but this program is mine.
131
+ # BANNER
132
+ # end
133
+ # arg_parser.parse!(["--help"])
134
+ #
135
+ # Results in `--help` output like this:
136
+ #
137
+ # This is my program
138
+ # There are many programs like it,
139
+ # but this program is mine.
140
+ # Usage: example [options] <path>
141
+ def banner(line)
142
+ add do |arg|
143
+ arg.banner(line)
144
+ end
145
+ end
146
+
147
+ # Add a line to the separator. The separator is text that appears
148
+ # at the bottom of the help text.
149
+ #
150
+ # A new-line will automatically be added to the end of the line.
151
+ # Although it's called a "line," you can embed new-lines in it so
152
+ # that it is actually more than one line.
153
+ #
154
+ # This example:
155
+ #
156
+ # arg_parser = OptParseBuilder.build_parser do |args|
157
+ # args.separator "Here I explain more about my program"
158
+ # args.separator <<~SEPARATOR
159
+ # For such a small program,
160
+ # it has a lot of text at the end.
161
+ # SEPARATOR
162
+ # end
163
+ # arg_parser.parse!(["--help"])
164
+ #
165
+ # Results in `--help` output like this:
166
+ #
167
+ # Usage: example [options] <path>
168
+ # Here I explain more about my program
169
+ # For such a small program,
170
+ # it has a lot of text at the end.
171
+ def separator(line)
172
+ add do |arg|
173
+ arg.separator(line)
174
+ end
175
+ end
176
+
177
+ # Add an argument. Must be passed either the argument to add, or
178
+ # given a block. If given a block, yields an ArgumentBuilder.
179
+ #
180
+ # Example using a pre-built argument:
181
+ #
182
+ # DRY_RUN = OptParseBuilder.build_argument do |arg|
183
+ # arg.key :dry_run
184
+ # arg.on "-d", "--dry-run", "Make no changes"
185
+ # end
186
+ # arg_parser = OptParseBuilder.build_parser do |args|
187
+ # args.add DRY_RUN
188
+ # end
189
+ #
190
+ # Example using a block to build the argument in-place:
191
+ #
192
+ # arg_parser = OptParseBuilder.build_parser do |args|
193
+ # args.add do |arg|
194
+ # arg.key :dry_run
195
+ # arg.on "-d", "--dry-run", "Make no changes"
196
+ # end
197
+ # end
198
+ #
199
+ # This is equivalent to:
200
+ #
201
+ # arg_parser = OptParseBuilder.build_parser do |args|
202
+ # args.add OptParseBuilder.build_argument do |arg|
203
+ # arg.key :dry_run
204
+ # arg.on "-d", "--dry-run", "Make no changes"
205
+ # end
206
+ # end
207
+ #
208
+ # See the README for details of the different options available
209
+ # for an argument.
210
+ #
211
+ # Raises BuildError if the argument cannot be built or added.
212
+ def add(argument = nil, &block)
213
+ unless argument.nil? ^ block.nil?
214
+ raise BuildError, "Need exactly 1 of arg and block"
215
+ end
216
+ if argument
217
+ add_argument(argument)
218
+ else
219
+ add_argument(OptParseBuilder.build_argument(&block))
220
+ end
221
+ end
222
+
223
+ # Returns the value of an argument, given either a symbol or a
224
+ # string with its name. If the key does not exist, raises KeyError.
225
+ #
226
+ # arg_parser = OptParseBuilder.build_parser do |args|
227
+ # args.add do |arg|
228
+ # arg.key :x
229
+ # arg.default 123
230
+ # end
231
+ # end
232
+ # arg_parser[:x] # => 123
233
+ # arg_parser["x"] # => 123
234
+ # arg_parser[:y] # KeyError (key not found :y)
235
+ #
236
+ # See also:
237
+ #
238
+ # * method #values - returns a collection of all argument values
239
+ # * method #has_key? - find out if the parser knows about a key
240
+ def [](key)
241
+ find_argument!(key).value
242
+ end
243
+
244
+ # Return a collection with all of the argument values. The
245
+ # collection can be accessed in several ways:
246
+ #
247
+ # arg_parser = OptParseBuilder.build_parser do |args|
248
+ # args.add do |arg|
249
+ # arg.key :num
250
+ # arg.on "--num=N", Integer, "A number"
251
+ # end
252
+ # end
253
+ # arg_parser.parse!(["--num=123"])
254
+ # arg_values = arg_parser.values
255
+ # p arg_values[:num] # => 123
256
+ # p arg_values["num"] # => 123
257
+ # p arg_values.num # => 123
258
+ def values
259
+ av = ArgumentValues.new
260
+ @arguments.each do |arg|
261
+ av[arg.key] = arg.value if arg.key
262
+ end
263
+ av
264
+ end
265
+
266
+ # Return true if the parser has the named key, which may be either a
267
+ # string or a symbol.
268
+ #
269
+ # arg_parser = OptParseBuilder.build_parser do |args|
270
+ # args.add do |arg|
271
+ # arg.key :quiet
272
+ # end
273
+ # end
274
+ # arg_parser.has_key?(:quiet) # => true
275
+ # arg_parser.has_key?("quiet") # => true
276
+ # arg_parser.has_key?(:verbose) # => false
277
+ def has_key?(key)
278
+ !!find_argument(key)
279
+ end
280
+
281
+ private
282
+
283
+ def sort_arguments
284
+ stable_sort_by!(@arguments) do |arg|
285
+ case arg
286
+ when RequiredOperandArgument
287
+ 1
288
+ when OptionalOperandArgument
289
+ 2
290
+ when SplatOperandArgument
291
+ 3
292
+ else
293
+ 0
294
+ end
295
+ end
296
+ end
297
+
298
+ def find_argument!(key)
299
+ argument = find_argument(key)
300
+ unless argument
301
+ raise Key, "key not found #{key.inspect}"
302
+ end
303
+ argument
304
+ end
305
+
306
+ def find_argument(key)
307
+ key = key.to_sym
308
+ @arguments.find do |arg|
309
+ arg.key == key
310
+ end
311
+ end
312
+
313
+ def optparse
314
+ op = OptParse.new
315
+ op.banner = banner_prefix + op.banner + banner_suffix
316
+ @arguments.each { |arg| arg.apply_option(op) }
317
+ @arguments.each do |argument|
318
+ argument.separator_lines.each do |line|
319
+ op.separator(line)
320
+ end
321
+ end
322
+ op
323
+ end
324
+
325
+ def add_argument(argument)
326
+ if argument.key && has_key?(argument.key)
327
+ raise BuildError, "duplicate key #{argument.key}"
328
+ end
329
+ @arguments.concat(argument.to_a.map(&:dup))
330
+ end
331
+
332
+ def banner_prefix
333
+ @arguments.flat_map(&:banner_lines).map do |line|
334
+ line + "\n"
335
+ end.join
336
+ end
337
+
338
+ def banner_suffix
339
+ suffix = @arguments.map(&:operand_notation).compact.join(" ")
340
+ suffix = " " + suffix unless suffix.empty?
341
+ suffix
342
+ end
343
+
344
+ end
345
+ end