opt_parse_builder 0.1.0

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