boson 0.4.0 → 1.0.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 (64) hide show
  1. data/.gemspec +6 -7
  2. data/.rspec +2 -0
  3. data/.travis.yml +7 -0
  4. data/CHANGELOG.rdoc +1 -1
  5. data/README.md +144 -0
  6. data/README.rdoc +2 -2
  7. data/Upgrading.md +23 -0
  8. data/bin/boson +2 -2
  9. data/lib/boson.rb +44 -52
  10. data/lib/boson/bare_runner.rb +83 -0
  11. data/lib/boson/bin_runner.rb +114 -0
  12. data/lib/boson/command.rb +92 -132
  13. data/lib/boson/inspector.rb +49 -48
  14. data/lib/boson/library.rb +71 -120
  15. data/lib/boson/loader.rb +73 -84
  16. data/lib/boson/manager.rb +131 -135
  17. data/lib/boson/method_inspector.rb +112 -0
  18. data/lib/boson/option_command.rb +71 -154
  19. data/lib/boson/option_parser.rb +178 -173
  20. data/lib/boson/options.rb +46 -32
  21. data/lib/boson/runner.rb +58 -66
  22. data/lib/boson/runner_library.rb +31 -0
  23. data/lib/boson/scientist.rb +48 -81
  24. data/lib/boson/util.rb +46 -61
  25. data/lib/boson/version.rb +1 -1
  26. data/test/bin_runner_test.rb +53 -191
  27. data/test/command_test.rb +5 -9
  28. data/test/deps.rip +2 -2
  29. data/test/loader_test.rb +18 -216
  30. data/test/manager_test.rb +69 -79
  31. data/test/method_inspector_test.rb +12 -36
  32. data/test/option_parser_test.rb +45 -32
  33. data/test/runner_library_test.rb +10 -0
  34. data/test/runner_test.rb +158 -28
  35. data/test/scientist_test.rb +9 -147
  36. data/test/test_helper.rb +87 -52
  37. metadata +30 -72
  38. data/deps.rip +0 -2
  39. data/lib/boson/commands.rb +0 -7
  40. data/lib/boson/commands/core.rb +0 -77
  41. data/lib/boson/commands/web_core.rb +0 -153
  42. data/lib/boson/index.rb +0 -48
  43. data/lib/boson/inspectors/argument_inspector.rb +0 -97
  44. data/lib/boson/inspectors/comment_inspector.rb +0 -100
  45. data/lib/boson/inspectors/method_inspector.rb +0 -98
  46. data/lib/boson/libraries/file_library.rb +0 -144
  47. data/lib/boson/libraries/gem_library.rb +0 -30
  48. data/lib/boson/libraries/local_file_library.rb +0 -30
  49. data/lib/boson/libraries/module_library.rb +0 -37
  50. data/lib/boson/libraries/require_library.rb +0 -23
  51. data/lib/boson/namespace.rb +0 -31
  52. data/lib/boson/pipe.rb +0 -147
  53. data/lib/boson/pipes.rb +0 -75
  54. data/lib/boson/repo.rb +0 -107
  55. data/lib/boson/runners/bin_runner.rb +0 -208
  56. data/lib/boson/runners/console_runner.rb +0 -58
  57. data/lib/boson/view.rb +0 -95
  58. data/test/argument_inspector_test.rb +0 -62
  59. data/test/commands_test.rb +0 -22
  60. data/test/comment_inspector_test.rb +0 -126
  61. data/test/file_library_test.rb +0 -42
  62. data/test/pipes_test.rb +0 -65
  63. data/test/repo_index_test.rb +0 -122
  64. data/test/repo_test.rb +0 -23
@@ -0,0 +1,112 @@
1
+ module Boson
2
+ # Gathers method attributes by redefining method_added and capturing method
3
+ # calls before a method.
4
+ class MethodInspector
5
+ METHODS = [:config, :desc, :options]
6
+ SCRAPEABLE_METHODS = [:options]
7
+ METHOD_CLASSES = {:config=>Hash, :desc=>String, :options=>Hash}
8
+ ALL_METHODS = METHODS + [:option]
9
+
10
+ def self.safe_new_method_added(mod, meth)
11
+ return unless mod.to_s[/^Boson::Commands::/]
12
+ new_method_added(mod, meth)
13
+ end
14
+
15
+ def self.new_method_added(mod, meth)
16
+ instance.new_method_added(mod, meth)
17
+ end
18
+
19
+ class << self; attr_accessor :instance end
20
+
21
+ def self.instance
22
+ @instance ||= new
23
+ end
24
+
25
+ (METHODS + [:option, :mod_store]).each do |meth|
26
+ define_singleton_method(meth) do |*args|
27
+ instance.send(meth, *args)
28
+ end
29
+ end
30
+
31
+ attr_accessor :current_module, :mod_store
32
+ def initialize
33
+ @mod_store = {}
34
+ end
35
+
36
+ # The method_added used while scraping method attributes.
37
+ def new_method_added(mod, meth)
38
+ self.current_module = mod
39
+
40
+ store[:temp] ||= {}
41
+ METHODS.each do |e|
42
+ store[e][meth.to_s] = store[:temp][e] if store[:temp][e]
43
+ end
44
+ if store[:temp][:option]
45
+ (store[:options][meth.to_s] ||= {}).merge! store[:temp][:option]
46
+ end
47
+ during_new_method_added mod, meth
48
+ store[:temp] = {}
49
+
50
+ if SCRAPEABLE_METHODS.any? {|m| has_inspector_method?(meth, m) }
51
+ set_arguments(mod, meth)
52
+ end
53
+ end
54
+
55
+ METHODS.each do |e|
56
+ define_method(e) do |mod, val|
57
+ (@mod_store[mod] ||= {})[e] ||= {}
58
+ (store(mod)[:temp] ||= {})[e] = val
59
+ end
60
+ end
61
+
62
+ # Scrapes option
63
+ def option(mod, name, value)
64
+ (@mod_store[mod] ||= {})[:options] ||= {}
65
+ (store(mod)[:temp] ||= {})[:option] ||= {}
66
+ (store(mod)[:temp] ||= {})[:option][name] = value
67
+ end
68
+
69
+ # Hash of a module's method attributes i.e. descriptions, options by method
70
+ # and then attribute
71
+ def store(mod=@current_module)
72
+ @mod_store[mod]
73
+ end
74
+
75
+ # Renames store key from old to new name
76
+ def rename_store_key(old, new)
77
+ mod_store[new] = mod_store.delete old
78
+ end
79
+
80
+ # Sets current module
81
+ def current_module=(mod)
82
+ @current_module = mod
83
+ @mod_store[mod] ||= {}
84
+ end
85
+
86
+ module API
87
+ # Method hook called during new_method_added
88
+ def during_new_method_added(mod, meth); end
89
+
90
+ def set_arguments(mod, meth)
91
+ store[:args] ||= {}
92
+
93
+ args = mod.instance_method(meth).parameters.map do|(type, name)|
94
+ case type
95
+ when :rest then ["*#{name}"]
96
+ when :req then [name.to_s]
97
+ when :opt then [name.to_s, '']
98
+ else nil
99
+ end
100
+ end.compact
101
+
102
+ store[:args][meth.to_s] = args
103
+ end
104
+
105
+ # Determines if method's arguments should be scraped
106
+ def has_inspector_method?(meth, inspector)
107
+ true
108
+ end
109
+ end
110
+ include API
111
+ end
112
+ end
@@ -1,105 +1,36 @@
1
1
  require 'shellwords'
2
2
  module Boson
3
- # A class used by Scientist to wrap around Command objects. It's main purpose is to parse
4
- # a command's global options (basic, render and pipe types) and local options.
5
- # As the names imply, global options are available to all commands while local options are specific to a command.
6
- # When passing options to commands, global ones _must_ be passed first, then local ones.
7
- # Also, options _must_ all be passed either before or after arguments.
8
- # For more about pipe and render options see Pipe and View respectively.
3
+ # A class used by Scientist to wrap around Command objects. It's main purpose
4
+ # is to parse a command's global and local options. As the names imply,
5
+ # global options are available to all commands while local options are
6
+ # specific to a command. When passing options to commands, global ones _must_
7
+ # be passed first, then local ones. Also, options _must_ all be passed either
8
+ # before or after arguments.
9
9
  #
10
10
  # === Basic Global Options
11
- # Any command with options comes with basic global options. For example '-hv' on an option command
12
- # prints a help summarizing global and local options. Another basic global option is --pretend. This
13
- # option displays what global options have been parsed and the actual arguments to be passed to a
14
- # command if executed. For example:
15
- #
16
- # # Define this command in a library
17
- # options :level=>:numeric, :verbose=>:boolean
18
- # def foo(*args)
19
- # args
20
- # end
21
- #
22
- # irb>> foo 'testin -p -l=1'
23
- # Arguments: ["testin", {:level=>1}]
24
- # Global options: {:pretend=>true}
25
- #
26
- # If a global option conflicts with a local option, the local option takes precedence. You can get around
27
- # this by passing global options after a '-'. For example, if the global option -f (--fields) conflicts with
28
- # a local -f (--force):
29
- # foo 'arg1 -v -f - -f=f1,f2'
30
- # # is the same as
31
- # foo 'arg1 -v --fields=f1,f2 -f'
32
- #
33
- # === Toggling Views With the Basic Global Option --render
34
- # One of the more important global options is --render. This option toggles the rendering of a command's
35
- # output done with View and Hirb[http://github.com/cldwalker/hirb].
36
- #
37
- # Here's a simple example of toggling Hirb's table view:
38
- # # Defined in a library file:
39
- # #@options {}
40
- # def list(options={})
41
- # [1,2,3]
42
- # end
43
- #
44
- # Using it in irb:
45
- # >> list
46
- # => [1,2,3]
47
- # >> list '-r' # or list --render
48
- # +-------+
49
- # | value |
50
- # +-------+
51
- # | 1 |
52
- # | 2 |
53
- # | 3 |
54
- # +-------+
55
- # 3 rows in set
56
- # => true
11
+ # Any command with options comes with basic global options. For example '-h'
12
+ # on an option command prints help. If a global option conflicts with a local
13
+ # option, the local option takes precedence.
57
14
  class OptionCommand
58
15
  # ArgumentError specific to @command's arguments
59
16
  class CommandArgumentError < ::ArgumentError; end
60
17
 
18
+ # default global options
61
19
  BASIC_OPTIONS = {
62
20
  :help=>{:type=>:boolean, :desc=>"Display a command's help"},
63
- :render=>{:type=>:boolean, :desc=>"Toggle a command's default rendering behavior"},
64
- :verbose=>{:type=>:boolean, :desc=>"Increase verbosity for help, errors, etc."},
65
- :usage_options=>{:type=>:string, :desc=>"Render options to pass to usage/help"},
66
- :pretend=>{:type=>:boolean, :desc=>"Display what a command would execute without executing it"},
67
- :delete_options=>{:type=>:array, :desc=>'Deletes global options starting with given strings' }
68
- } #:nodoc:
69
-
70
- RENDER_OPTIONS = {
71
- :fields=>{:type=>:array, :desc=>"Displays fields in the order given"},
72
- :class=>{:type=>:string, :desc=>"Hirb helper class which renders"},
73
- :max_width=>{:type=>:numeric, :desc=>"Max width of a table"},
74
- :vertical=>{:type=>:boolean, :desc=>"Display a vertical table"},
75
- } #:nodoc:
76
-
77
- PIPE_OPTIONS = {
78
- :sort=>{:type=>:string, :desc=>"Sort by given field"},
79
- :reverse_sort=>{:type=>:boolean, :desc=>"Reverse a given sort"},
80
- :query=>{:type=>:hash, :desc=>"Queries fields given field:search pairs"},
81
- :pipes=>{:alias=>'P', :type=>:array, :desc=>"Pipe to commands sequentially"}
82
- } #:nodoc:
21
+ }
83
22
 
84
23
  class <<self
85
- #:stopdoc:
86
24
  def default_option_parser
87
- @default_option_parser ||= OptionParser.new default_pipe_options.
88
- merge(default_render_options.merge(BASIC_OPTIONS))
25
+ @default_option_parser ||= OptionParser.new default_options
89
26
  end
90
27
 
91
- def default_pipe_options
92
- @default_pipe_options ||= PIPE_OPTIONS.merge Pipe.pipe_options
93
- end
94
-
95
- def default_render_options
96
- @default_render_options ||= RENDER_OPTIONS.merge Boson.repo.config[:render_options] || {}
97
- end
98
-
99
- def delete_non_render_options(opt)
100
- opt.delete_if {|k,v| BASIC_OPTIONS.keys.include?(k) }
28
+ module API
29
+ def default_options
30
+ BASIC_OPTIONS
31
+ end
101
32
  end
102
- #:startdoc:
33
+ include API
103
34
  end
104
35
 
105
36
  attr_accessor :command
@@ -107,102 +38,63 @@ module Boson
107
38
  @command = cmd
108
39
  end
109
40
 
110
- # Parses arguments and returns global options, local options and leftover arguments.
41
+ # Parses arguments and returns global options, local options and leftover
42
+ # arguments.
111
43
  def parse(args)
112
44
  if args.size == 1 && args[0].is_a?(String)
113
45
  global_opt, parsed_options, args = parse_options Shellwords.shellwords(args[0])
114
46
  # last string argument interpreted as args + options
115
47
  elsif args.size > 1 && args[-1].is_a?(String)
116
- temp_args = Runner.in_shell? ? args : Shellwords.shellwords(args.pop)
48
+ temp_args = Boson.in_shell ? args : Shellwords.shellwords(args.pop)
117
49
  global_opt, parsed_options, new_args = parse_options temp_args
118
- Runner.in_shell? ? args = new_args : args += new_args
50
+ Boson.in_shell ? args = new_args : args += new_args
119
51
  # add default options
120
- elsif @command.options.nil? || @command.options.empty? || (!@command.has_splat_args? &&
121
- args.size <= (@command.arg_size - 1).abs) || (@command.has_splat_args? && !args[-1].is_a?(Hash))
52
+ elsif @command.options.nil? || @command.options.empty? ||
53
+ (!@command.has_splat_args? && args.size <= (@command.arg_size - 1).abs) ||
54
+ (@command.has_splat_args? && !args[-1].is_a?(Hash))
122
55
  global_opt, parsed_options = parse_options([])[0,2]
123
56
  # merge default options with given hash of options
124
- elsif (@command.has_splat_args? || (args.size == @command.arg_size)) && args[-1].is_a?(Hash)
125
- global_opt, parsed_options = parse_options([])[0,2]
126
- parsed_options.merge!(args.pop)
57
+ elsif (@command.has_splat_args? || (args.size == @command.arg_size)) &&
58
+ args[-1].is_a?(Hash)
59
+ global_opt, parsed_options = parse_options([])[0,2]
60
+ parsed_options.merge!(args.pop)
127
61
  end
128
62
  [global_opt || {}, parsed_options, args]
129
63
  end
130
64
 
131
- #:stopdoc:
132
- def parse_options(args)
133
- parsed_options = @command.option_parser.parse(args, :delete_invalid_opts=>true)
134
- trailing, unparseable = split_trailing
135
- global_options = parse_global_options @command.option_parser.leading_non_opts + trailing
136
- new_args = option_parser.non_opts.dup + unparseable
137
- [global_options, parsed_options, new_args]
138
- rescue OptionParser::Error
139
- global_options = parse_global_options @command.option_parser.leading_non_opts + split_trailing[0]
140
- global_options[:help] ? [global_options, nil, []] : raise
141
- end
142
-
143
- def split_trailing
144
- trailing = @command.option_parser.trailing_non_opts
145
- if trailing[0] == '--'
146
- trailing.shift
147
- [ [], trailing ]
148
- else
149
- trailing.shift if trailing[0] == '-'
150
- [ trailing, [] ]
151
- end
152
- end
153
-
154
65
  def parse_global_options(args)
155
66
  option_parser.parse args
156
67
  end
157
68
 
158
- def option_parser
159
- @option_parser ||= @command.render_options ? OptionParser.new(all_global_options) :
160
- self.class.default_option_parser
161
- end
162
-
163
- def all_global_options
164
- OptionParser.make_mergeable! @command.render_options
165
- render_opts = Util.recursive_hash_merge(@command.render_options, Util.deep_copy(self.class.default_render_options))
166
- merged_opts = Util.recursive_hash_merge Util.deep_copy(self.class.default_pipe_options), render_opts
167
- opts = Util.recursive_hash_merge merged_opts, Util.deep_copy(BASIC_OPTIONS)
168
- set_global_option_defaults opts
169
- end
170
-
171
- def set_global_option_defaults(opts)
172
- if !opts[:fields].key?(:values)
173
- if opts[:fields][:default]
174
- opts[:fields][:values] = opts[:fields][:default]
175
- else
176
- if opts[:change_fields] && (changed = opts[:change_fields][:default])
177
- opts[:fields][:values] = changed.is_a?(Array) ? changed : changed.values
178
- end
179
- opts[:fields][:values] ||= opts[:headers][:default].keys if opts[:headers] && opts[:headers][:default]
180
- end
181
- opts[:fields][:enum] = false if opts[:fields][:values] && !opts[:fields].key?(:enum)
182
- end
183
- if opts[:fields][:values]
184
- opts[:sort][:values] ||= opts[:fields][:values]
185
- opts[:query][:keys] ||= opts[:fields][:values]
186
- opts[:query][:default_keys] ||= "*"
69
+ module API
70
+ # option parser just for option command
71
+ def option_parser
72
+ @option_parser ||= self.class.default_option_parser
187
73
  end
188
- opts
189
74
  end
75
+ include API
190
76
 
77
+ # modifies args for edge cases
191
78
  def modify_args(args)
192
- if @command.default_option && @command.arg_size <= 1 && !@command.has_splat_args? &&
79
+ if @command.default_option && @command.arg_size <= 1 &&
80
+ !@command.has_splat_args? &&
193
81
  !args[0].is_a?(Hash) && args[0].to_s[/./] != '-' && !args.join.empty?
194
- args[0] = "--#{@command.default_option}=#{args[0]}"
82
+ args[0] = "--#{@command.default_option}=#{args[0]}"
195
83
  end
196
84
  end
197
85
 
86
+ # raises CommandArgumentError if argument size is incorrect for given args
198
87
  def check_argument_size(args)
199
88
  if args.size != @command.arg_size && !@command.has_splat_args?
200
- command_size, args_size = args.size > @command.arg_size ? [@command.arg_size, args.size] :
89
+ command_size, args_size = args.size > @command.arg_size ?
90
+ [@command.arg_size, args.size] :
201
91
  [@command.arg_size - 1, args.size - 1]
202
- raise CommandArgumentError, "wrong number of arguments (#{args_size} for #{command_size})"
92
+ raise CommandArgumentError,
93
+ "wrong number of arguments (#{args_size} for #{command_size})"
203
94
  end
204
95
  end
205
96
 
97
+ # Adds default args as original method would
206
98
  def add_default_args(args, obj)
207
99
  if @command.args && args.size < @command.args.size - 1
208
100
  # leave off last arg since its an option
@@ -210,13 +102,38 @@ module Boson
210
102
  next if args.size >= i + 1 # only fill in once args run out
211
103
  break if arr.size != 2 # a default arg value must exist
212
104
  begin
213
- args[i] = @command.file_parsed_args? ? obj.instance_eval(arr[1]) : arr[1]
105
+ args[i] = @command.file_parsed_args ? obj.instance_eval(arr[1]) : arr[1]
214
106
  rescue Exception
215
- raise Scientist::Error, "Unable to set default argument at position #{i+1}.\nReason: #{$!.message}"
107
+ raise Scientist::Error, "Unable to set default argument at " +
108
+ "position #{i+1}.\nReason: #{$!.message}"
216
109
  end
217
110
  }
218
111
  end
219
112
  end
220
- #:startdoc:
113
+
114
+ private
115
+ def parse_options(args)
116
+ parsed_options = @command.option_parser.parse(args, delete_invalid_opts: true)
117
+ trailing, unparseable = split_trailing
118
+ global_options = parse_global_options @command.option_parser.leading_non_opts +
119
+ trailing
120
+ new_args = option_parser.non_opts.dup + unparseable
121
+ [global_options, parsed_options, new_args]
122
+ rescue OptionParser::Error
123
+ global_options = parse_global_options @command.option_parser.leading_non_opts +
124
+ split_trailing[0]
125
+ global_options[:help] ? [global_options, nil, []] : raise
126
+ end
127
+
128
+ def split_trailing
129
+ trailing = @command.option_parser.trailing_non_opts
130
+ if trailing[0] == '--'
131
+ trailing.shift
132
+ [ [], trailing ]
133
+ else
134
+ trailing.shift if trailing[0] == '-'
135
+ [ trailing, [] ]
136
+ end
137
+ end
221
138
  end
222
139
  end
@@ -1,47 +1,24 @@
1
1
  module Boson
2
- # Simple Hash with indifferent fetching and storing using symbol or string keys. Other actions such as
3
- # merging should assume symbolic keys. Used by OptionParser.
4
- class IndifferentAccessHash < ::Hash
5
- #:stopdoc:
6
- def initialize(hash={})
7
- super()
8
- hash.each {|k,v| self[k] = v }
9
- end
10
-
11
- def [](key)
12
- super convert_key(key)
13
- end
14
-
15
- def []=(key, value)
16
- super convert_key(key), value
17
- end
18
-
19
- def values_at(*indices)
20
- indices.collect { |key| self[convert_key(key)] }
21
- end
22
-
23
- protected
24
- def convert_key(key)
25
- key.kind_of?(String) ? key.to_sym : key
26
- end
27
- #:startdoc:
28
- end
29
-
30
- # This class concisely defines commandline options that when parsed produce a Hash of option keys and values.
2
+ # This class concisely defines commandline options that when parsed produce a
3
+ # Hash of option keys and values.
31
4
  # Additional points:
32
- # * Setting option values should follow conventions in *nix environments. See examples below.
33
- # * By default, there are 5 option types, each which produce different objects for option values.
34
- # * The default option types can produce objects for one or more of the following Ruby classes:
35
- # String, Integer, Float, Array, Hash, FalseClass, TrueClass.
36
- # * Users can define their own option types which create objects for _any_ Ruby class. See Options.
37
- # * Each option type can have attributes to enable more features (see OptionParser.new).
38
- # * When options are parsed by parse(), an IndifferentAccessHash hash is returned.
5
+ # * Setting option values should follow conventions in *nix environments.
6
+ # See examples below.
7
+ # * By default, there are 5 option types, each which produce different
8
+ # objects for option values.
9
+ # * The default option types can produce objects for one or more of the following
10
+ # Ruby classes: String, Integer, Float, Array, Hash, FalseClass, TrueClass.
11
+ # * Users can define their own option types which create objects for _any_
12
+ # Ruby class. See Options.
13
+ # * Each option type can have attributes to enable more features (see
14
+ # OptionParser.new).
15
+ # * When options are parsed by parse(), an indifferent access hash is returned.
39
16
  # * Options are also called switches, parameters, flags etc.
40
17
  # * Option parsing stops when it comes across a '--'.
41
18
  #
42
19
  # Default option types:
43
- # [*:boolean*] This option has no passed value. To toogle a boolean, prepend with '--no-'.
44
- # Multiple booleans can be joined together.
20
+ # [*:boolean*] This option has no passed value. To toogle a boolean, prepend
21
+ # with '--no-'. Multiple booleans can be joined together.
45
22
  # '--debug' -> {:debug=>true}
46
23
  # '--no-debug' -> {:debug=>false}
47
24
  # '--no-d' -> {:debug=>false}
@@ -50,23 +27,25 @@ module Boson
50
27
  # '--color red' -> {:color=>'red'}
51
28
  # '--color=red' -> {:color=>'red'}
52
29
  # '--color "gotta love spaces"' -> {:color=>'gotta love spaces'}
53
- # [*:numeric*] Sets values as :string does or by appending number right after aliased name. Shortened form
54
- # can be appended to joined booleans.
30
+ # [*:numeric*] Sets values as :string does or by appending number right after
31
+ # aliased name. Shortened form can be appended to joined booleans.
55
32
  # '-n3' -> {:num=>3}
56
33
  # '-dn3' -> {:debug=>true, :num=>3}
57
- # [*:array*] Sets values as :string does. Multiple values are split by a configurable character
58
- # Default is ',' (see OptionParser.new). Passing '*' refers to all known :values.
59
- # '--fields 1,2,3' -> {:fields=>['1','2','3']}
60
- # '--fields *' -> {:fields=>['1','2','3']}
61
- # [*:hash*] Sets values as :string does. Key-value pairs are split by ':' and pairs are split by
62
- # a configurable character (default ','). Multiple keys can be joined to one value. Passing '*'
63
- # as a key refers to all known :keys.
34
+ # [*:array*] Sets values as :string does. Multiple values are split by a
35
+ # configurable character Default is ',' (see OptionParser.new).
36
+ # Passing '*' refers to all known :values.
37
+ # '--fields 1,2,3' -> {:fields=>['1','2','3']}
38
+ # '--fields *' -> {:fields=>['1','2','3']}
39
+ # [*:hash*] Sets values as :string does. Key-value pairs are split by ':' and
40
+ # pairs are split by a configurable character (default ',').
41
+ # Multiple keys can be joined to one value. Passing '*' as a key
42
+ # refers to all known :keys.
64
43
  # '--fields a:b,c:d' -> {:fields=>{'a'=>'b', 'c'=>'d'} }
65
44
  # '--fields a,b:d' -> {:fields=>{'a'=>'d', 'b'=>'d'} }
66
45
  # '--fields *:d' -> {:fields=>{'a'=>'d', 'b'=>'d', 'c'=>'d'} }
67
46
  #
68
- # This is a modified version of Yehuda Katz's Thor::Options class which is a modified version
69
- # of Daniel Berger's Getopt::Long class (licensed under Ruby's license).
47
+ # This is a modified version of Yehuda Katz's Thor::Options class which is a
48
+ # modified version of Daniel Berger's Getopt::Long class (Ruby license).
70
49
  class OptionParser
71
50
  # Raised for all OptionParser errors
72
51
  class Error < StandardError; end
@@ -75,42 +54,22 @@ module Boson
75
54
  LONG_RE = /^(--\w+[-\w+]*)$/
76
55
  SHORT_RE = /^(-[a-zA-Z])$/i
77
56
  EQ_RE = /^(--\w+[-\w+]*|-[a-zA-Z])=(.*)$/i
78
- SHORT_SQ_RE = /^-([a-zA-Z]{2,})$/i # Allow either -x -v or -xv style for single char args
57
+ # Allow either -x -v or -xv style for single char args
58
+ SHORT_SQ_RE = /^-([a-zA-Z]{2,})$/i
79
59
  SHORT_NUM = /^(-[a-zA-Z])#{NUMERIC}$/i
80
60
  STOP_STRINGS = %w{-- -}
81
-
82
- attr_reader :leading_non_opts, :trailing_non_opts, :opt_aliases
83
-
84
- # Given options to pass to OptionParser.new, this method parses ARGV and returns the remaining arguments
85
- # and a hash of parsed options. This is useful for scripts outside of Boson.
86
- def self.parse(options, args=ARGV)
87
- @opt_parser ||= new(options)
88
- parsed_options = @opt_parser.parse(args)
89
- [@opt_parser.non_opts, parsed_options]
90
- end
91
-
92
- # Usage string summarizing options defined in parse
93
- def self.usage
94
- @opt_parser.to_s
95
- end
96
61
 
97
- def self.make_mergeable!(opts) #:nodoc:
98
- opts.each {|k,v|
99
- if !v.is_a?(Hash) && !v.is_a?(Symbol)
100
- opts[k] = {:default=>v}
101
- end
102
- }
103
- end
62
+ attr_reader :leading_non_opts, :trailing_non_opts, :opt_aliases
104
63
 
105
64
  # Array of arguments left after defined options have been parsed out by parse.
106
65
  def non_opts
107
66
  leading_non_opts + trailing_non_opts
108
67
  end
109
68
 
110
- # Takes a hash of options. Each option, a key-value pair, must provide the option's
111
- # name and type. Names longer than one character are accessed with '--' while
112
- # one character names are accessed with '-'. Names can be symbols, strings
113
- # or even dasherized strings:
69
+ # Takes a hash of options. Each option, a key-value pair, must provide the
70
+ # option's name and type. Names longer than one character are accessed with
71
+ # '--' while one character names are accessed with '-'. Names can be
72
+ # symbols, strings or even dasherized strings:
114
73
  #
115
74
  # Boson::OptionParser.new :debug=>:boolean, 'level'=>:numeric,
116
75
  # '--fields'=>:array
@@ -121,47 +80,62 @@ module Boson
121
80
  # Boson::OptionParser.new :debug=>true, 'level'=>3.1, :fields=>%w{f1 f2}
122
81
  #
123
82
  # By default every option name longer than one character is given an alias,
124
- # the first character from its name. For example, the --fields option
125
- # has -f as its alias. You can override the default alias by providing your own
83
+ # the first character from its name. For example, the --fields option has -f
84
+ # as its alias. You can override the default alias by providing your own
126
85
  # option aliases as an array in the option's key.
127
86
  #
128
87
  # Boson::OptionParser.new [:debug, :damnit, :D]=>true
129
88
  #
130
- # Note that aliases are accessed the same way as option names. For the above,
131
- # --debug, --damnit and -D all refer to the same option.
89
+ # Note that aliases are accessed the same way as option names. For the
90
+ # above, --debug, --damnit and -D all refer to the same option.
91
+ #
92
+ # Options can have additional attributes by passing a hash to the option
93
+ # value instead of a type or default:
132
94
  #
133
- # Options can have additional attributes by passing a hash to the option value instead of
134
- # a type or default:
135
- #
136
95
  # Boson::OptionParser.new :fields=>{:type=>:array, :values=>%w{f1 f2 f3},
137
96
  # :enum=>false}
138
97
  #
139
- # These attributes are available when an option is parsed via current_attributes().
140
- # Here are the available option attributes for the default option types:
98
+ # These attributes are available when an option is parsed via
99
+ # current_attributes(). Here are the available option attributes for the
100
+ # default option types:
141
101
  #
142
- # [*:type*] This or :default is required. Available types are :string, :boolean, :array, :numeric, :hash.
143
- # [*:default*] This or :type is required. This is the default value an option has when not passed.
144
- # [*:bool_default*] This is the value an option has when passed as a boolean. However, by enabling this
145
- # an option can only have explicit values with '=' i.e. '--index=alias' and no '--index alias'.
146
- # If this value is a string, it is parsed as any option value would be. Otherwise, the value is
147
- # passed directly without parsing.
148
- # [*:required*] Boolean indicating if option is required. Option parses raises error if value not given.
149
- # Default is false.
150
- # [*:alias*] Alternative way to define option aliases with an option name or an array of them. Useful in yaml files.
151
- # Setting to false will prevent creating an automatic alias.
152
- # [*:values*] An array of values an option can have. Available for :array and :string options. Values here
153
- # can be aliased by typing a unique string it starts with or underscore aliasing (see Util.underscore_search).
154
- # For example, for values foo, odd and obnoxiously_long, f refers to foo, od to odd and o_l to obnoxiously_long.
155
- # [*:enum*] Boolean indicating if an option enforces values in :values or :keys. Default is true. For
156
- # :array, :hash and :string options.
157
- # [*:split*] For :array and :hash options. A string or regular expression on which an array value splits
158
- # to produce an array of values. Default is ','.
159
- # [*:keys*] :hash option only. An array of values a hash option's keys can have. Keys can be aliased just like :values.
160
- # [*:default_keys*] For :hash option only. Default keys to assume when only a value is given. Multiple keys can be joined
161
- # by the :split character. Defaults to first key of :keys if :keys given.
162
- # [*:regexp*] For :array option with a :values attribute. Boolean indicating that each option value does a regular
163
- # expression search of :values. If there are values that match, they replace the original option value. If none,
164
- # then the original option value is used.
102
+ # [*:type*] This or :default is required. Available types are :string,
103
+ # :boolean, :array, :numeric, :hash.
104
+ # [*:default*] This or :type is required. This is the default value an
105
+ # option has when not passed.
106
+ # [*:bool_default*] This is the value an option has when passed as a
107
+ # boolean. However, by enabling this an option can only
108
+ # have explicit values with '=' i.e. '--index=alias' and
109
+ # no '--index alias'. If this value is a string, it is
110
+ # parsed as any option value would be. Otherwise, the
111
+ # value is passed directly without parsing.
112
+ # [*:required*] Boolean indicating if option is required. Option parses
113
+ # raises error if value not given. Default is false.
114
+ # [*:alias*] Alternative way to define option aliases with an option name
115
+ # or an array of them. Useful in yaml files. Setting to false
116
+ # will prevent creating an automatic alias.
117
+ # [*:values*] An array of values an option can have. Available for :array
118
+ # and :string options. Values here can be aliased by typing a
119
+ # unique string it starts with or underscore aliasing (see
120
+ # Util.underscore_search). For example, for values foo, odd and
121
+ # obnoxiously_long, f refers to foo, od to odd and o_l to
122
+ # obnoxiously_long.
123
+ # [*:enum*] Boolean indicating if an option enforces values in :values or
124
+ # :keys. Default is true. For :array, :hash and :string options.
125
+ # [*:split*] For :array and :hash options. A string or regular expression
126
+ # on which an array value splits to produce an array of values.
127
+ # Default is ','.
128
+ # [*:keys*] :hash option only. An array of values a hash option's keys can
129
+ # have. Keys can be aliased just like :values.
130
+ # [*:default_keys*] For :hash option only. Default keys to assume when only
131
+ # a value is given. Multiple keys can be joined by the
132
+ # :split character. Defaults to first key of :keys if
133
+ # :keys given.
134
+ # [*:regexp*] For :array option with a :values attribute. Boolean indicating
135
+ # that each option value does a regular expression search of
136
+ # :values. If there are values that match, they replace the
137
+ # original option value. If none, then the original option
138
+ # value is used.
165
139
  def initialize(opts)
166
140
  @defaults = {}
167
141
  @opt_aliases = {}
@@ -187,10 +161,14 @@ module Boson
187
161
  @option_attributes[nice_name] = type
188
162
  @opt_aliases[nice_name] = Array(type[:alias]) if type.key?(:alias)
189
163
  @defaults[nice_name] = type[:default] if type[:default]
190
- @option_attributes[nice_name][:enum] = true if (type.key?(:values) || type.key?(:keys)) &&
191
- !type.key?(:enum)
192
- @option_attributes[nice_name][:default_keys] ||= type[:keys][0] if type.key?(:keys)
193
- type = type[:type] || (!type[:default].nil? ? determine_option_type(type[:default]) : :boolean)
164
+ if (type.key?(:values) || type.key?(:keys)) && !type.key?(:enum)
165
+ @option_attributes[nice_name][:enum] = true
166
+ end
167
+ if type.key?(:keys)
168
+ @option_attributes[nice_name][:default_keys] ||= type[:keys][0]
169
+ end
170
+ type = type[:type] || (!type[:default].nil? ?
171
+ determine_option_type(type[:default]) : :boolean)
194
172
  end
195
173
 
196
174
  # set defaults
@@ -207,10 +185,13 @@ module Boson
207
185
  @opt_aliases = @opt_aliases.sort.inject({}) {|h, (nice_name, aliases)|
208
186
  name = dasherize nice_name
209
187
  # allow for aliases as symbols
210
- aliases.map! {|e| e.to_s.index('-') == 0 || e == false ? e : dasherize(e.to_s) }
188
+ aliases.map! {|e|
189
+ e.to_s.index('-') == 0 || e == false ? e : dasherize(e.to_s) }
190
+
211
191
  if aliases.empty? and nice_name.length > 1
212
192
  opt_alias = nice_name[0,1]
213
- opt_alias = h.key?("-"+opt_alias) ? "-"+opt_alias.capitalize : "-"+opt_alias
193
+ opt_alias = h.key?("-"+opt_alias) ? "-"+opt_alias.capitalize :
194
+ "-"+opt_alias
214
195
  h[opt_alias] ||= name unless @opt_types.key?(opt_alias)
215
196
  else
216
197
  aliases.each {|e| h[e] = name if !@opt_types.key?(e) && e != false }
@@ -219,20 +200,24 @@ module Boson
219
200
  }
220
201
  end
221
202
 
222
- # Parses an array of arguments for defined options to return an IndifferentAccessHash. Once the parser
223
- # recognizes a valid option, it continues to parse until an non option argument is detected.
224
- # Flags that can be passed to the parser:
225
- # * :opts_before_args: When true options must come before arguments. Default is false.
226
- # * :delete_invalid_opts: When true deletes any invalid options left after parsing. Will stop deleting if
227
- # it comes across - or --. Default is false.
203
+ # Parses an array of arguments for defined options to return an indifferent
204
+ # access hash. Once the parser recognizes a valid option, it continues to
205
+ # parse until an non option argument is detected.
206
+ # @param [Hash] flags
207
+ # @option flags [Boolean] :opts_before_args When true options must come
208
+ # before arguments. Default is false.
209
+ # @option flags [Boolean] :delete_invalid_opts When true deletes any
210
+ # invalid options left after parsing. Will stop deleting if it comes
211
+ # across - or --. Default is false.
228
212
  def parse(args, flags={})
229
213
  @args = args
230
- # start with defaults
231
- hash = IndifferentAccessHash.new @defaults
232
-
214
+ # start with symbolized defaults
215
+ hash = Hash[@defaults.map {|k,v| [k.to_sym, v] }]
216
+
233
217
  @leading_non_opts = []
234
218
  unless flags[:opts_before_args]
235
- @leading_non_opts << shift until current_is_option? || @args.empty? || STOP_STRINGS.include?(peek)
219
+ @leading_non_opts << shift until current_is_option? || @args.empty? ||
220
+ STOP_STRINGS.include?(peek)
236
221
  end
237
222
 
238
223
  while current_is_option?
@@ -259,11 +244,11 @@ module Boson
259
244
  @trailing_non_opts = @args
260
245
  check_required! hash
261
246
  delete_invalid_opts if flags[:delete_invalid_opts]
262
- hash
247
+ indifferent_hash.tap {|h| h.update hash }
263
248
  end
264
249
 
265
- # Helper method to generate usage. Takes a dashed option and a string value indicating
266
- # an option value's format.
250
+ # Helper method to generate usage. Takes a dashed option and a string value
251
+ # indicating an option value's format.
267
252
  def default_usage(opt, val)
268
253
  opt + "=" + (@defaults[undasherize(opt)] || val).to_s
269
254
  end
@@ -272,7 +257,8 @@ module Boson
272
257
  def formatted_usage
273
258
  return "" if @opt_types.empty?
274
259
  @opt_types.map do |opt, type|
275
- val = respond_to?("usage_for_#{type}", true) ? send("usage_for_#{type}", opt) : "#{opt}=:#{type}"
260
+ val = respond_to?("usage_for_#{type}", true) ?
261
+ send("usage_for_#{type}", opt) : "#{opt}=:#{type}"
276
262
  "[" + val + "]"
277
263
  end.join(" ")
278
264
  end
@@ -280,47 +266,37 @@ module Boson
280
266
  alias :to_s :formatted_usage
281
267
 
282
268
  # More verbose option help in the form of a table.
283
- def print_usage_table(render_options={})
284
- user_fields = render_options.delete(:fields)
285
- fields = get_usage_fields user_fields
286
- (fields << :default).uniq! if render_options.delete(:local) || user_fields == '*'
287
- opts = all_options_with_fields fields
288
- fields.delete(:default) if fields.include?(:default) && opts.all? {|e| e[:default].nil? }
289
- render_options = default_render_options.merge(:fields=>fields).merge(render_options)
290
- View.render opts, render_options
269
+ def print_usage_table(options={})
270
+ fields = get_usage_fields options[:fields]
271
+ fields, opts = get_fields_and_options(fields, options)
272
+ render_table(fields, opts, options)
291
273
  end
292
274
 
293
- def all_options_with_fields(fields) #:nodoc:
294
- aliases = @opt_aliases.invert
295
- @opt_types.keys.sort.inject([]) {|t,e|
296
- nice_name = undasherize(e)
297
- h = {:name=>e, :type=>@opt_types[e], :alias=>aliases[e] || '' }
298
- h[:default] = @defaults[nice_name] if fields.include?(:default)
299
- (fields - h.keys).each {|f|
300
- h[f] = (option_attributes[nice_name] || {})[f]
301
- }
302
- t << h
303
- }
304
- end
275
+ module API
276
+ def get_fields_and_options(fields, options)
277
+ opts = all_options_with_fields fields
278
+ [fields, opts]
279
+ end
305
280
 
306
- def default_render_options #:nodoc:
307
- {:header_filter=>:capitalize, :description=>false, :filter_any=>true,
308
- :filter_classes=>{Array=>[:join, ',']}, :hide_empty=>true}
281
+ def render_table(fields, arr, options)
282
+ headers = options[:no_headers] ? [] : [['Name', 'Desc'], ['----', '----']]
283
+ arr_of_arr = headers + arr.map do |row|
284
+ [ row.values_at(:alias, :name).compact.join(', '), row[:desc].to_s ]
285
+ end
286
+ puts Util.format_table(arr_of_arr)
287
+ end
309
288
  end
289
+ include API
310
290
 
311
291
  # Hash of option names mapped to hash of its external attributes
312
292
  def option_attributes
313
293
  @option_attributes || {}
314
294
  end
315
295
 
316
- def get_usage_fields(fields) #:nodoc:
317
- fields || ([:name, :alias, :type] + [:desc, :values, :keys].select {|e|
318
- option_attributes.values.any? {|f| f.key?(e) } }).uniq
319
- end
320
-
321
296
  # Hash of option attributes for the currently parsed option. _Any_ hash keys
322
- # passed to an option are available here. This means that an option type can have any
323
- # user-defined attributes available during option parsing and object creation.
297
+ # passed to an option are available here. This means that an option type can
298
+ # have any user-defined attributes available during option parsing and
299
+ # object creation.
324
300
  def current_attributes
325
301
  @option_attributes && @option_attributes[@current_option] || {}
326
302
  end
@@ -350,15 +326,39 @@ module Boson
350
326
  @opt_aliases.keys.map {|e| undasherize e }
351
327
  end
352
328
 
329
+ # Creates a Hash with indifferent access
330
+ def indifferent_hash
331
+ Hash.new {|hash,key| hash[key.to_sym] if String === key }
332
+ end
333
+
334
+ private
335
+ def all_options_with_fields(fields)
336
+ aliases = @opt_aliases.invert
337
+ @opt_types.keys.sort.inject([]) {|t,e|
338
+ nice_name = undasherize(e)
339
+ h = {:name=>e, :type=>@opt_types[e], :alias=>aliases[e] || nil }
340
+ h[:default] = @defaults[nice_name] if fields.include?(:default)
341
+ (fields - h.keys).each {|f|
342
+ h[f] = (option_attributes[nice_name] || {})[f]
343
+ }
344
+ t << h
345
+ }
346
+ end
347
+
348
+ def get_usage_fields(fields)
349
+ fields || ([:name, :alias, :type] + [:desc, :values, :keys].select {|e|
350
+ option_attributes.values.any? {|f| f.key?(e) } }).uniq
351
+ end
352
+
353
353
  def option_type(opt)
354
354
  if opt =~ /^--no-(\w+)$/
355
- @opt_types[opt] || @opt_types[dasherize($1)] || @opt_types[original_no_opt($1)]
355
+ @opt_types[opt] || @opt_types[dasherize($1)] ||
356
+ @opt_types[original_no_opt($1)]
356
357
  else
357
358
  @opt_types[opt]
358
359
  end
359
360
  end
360
361
 
361
- private
362
362
  def determine_option_type(value)
363
363
  return value if value.is_a?(Symbol)
364
364
  case value
@@ -375,23 +375,26 @@ module Boson
375
375
  end
376
376
 
377
377
  def create_option_value(type)
378
- if current_attributes.key?(:bool_default) && (@original_current_option !~ EQ_RE) &&
378
+ if current_attributes.key?(:bool_default) &&
379
+ (@original_current_option !~ EQ_RE) &&
379
380
  !(bool_default = current_attributes[:bool_default]).is_a?(String)
380
381
  bool_default
381
382
  else
382
- respond_to?("create_#{type}", true) ? send("create_#{type}", type != :boolean ? value_shift : nil) :
383
- raise(Error, "Option '#{@current_option}' is invalid option type #{type.inspect}.")
383
+ respond_to?("create_#{type}", true) ?
384
+ send("create_#{type}", type != :boolean ? value_shift : nil) :
385
+ raise(Error, "Option '#{@current_option}' is invalid option type " +
386
+ "#{type.inspect}.")
384
387
  end
385
388
  end
386
389
 
387
390
  def auto_alias_value(values, possible_value)
388
- if Boson.repo.config[:option_underscore_search]
389
- self.class.send(:define_method, :auto_alias_value) {|values, possible_value|
390
- Util.underscore_search(possible_value, values, true) || possible_value
391
+ if Boson.config[:option_underscore_search]
392
+ self.class.send(:define_method, :auto_alias_value) {|values, possible_val|
393
+ Util.underscore_search(possible_val, values, true) || possible_val
391
394
  }.call(values, possible_value)
392
395
  else
393
- self.class.send(:define_method, :auto_alias_value) {|values, possible_value|
394
- values.find {|v| v.to_s =~ /^#{possible_value}/ } || possible_value
396
+ self.class.send(:define_method, :auto_alias_value) {|values, possible_val|
397
+ values.find {|v| v.to_s =~ /^#{possible_val}/ } || possible_val
395
398
  }.call(values, possible_value)
396
399
  end
397
400
  end
@@ -399,7 +402,9 @@ module Boson
399
402
  def validate_enum_values(values, possible_values)
400
403
  if current_attributes[:enum]
401
404
  Array(possible_values).each {|e|
402
- raise(Error, "invalid value '#{e}' for option '#{@current_option}'") if !values.include?(e)
405
+ if !values.include?(e)
406
+ raise(Error, "invalid value '#{e}' for option '#{@current_option}'")
407
+ end
403
408
  }
404
409
  end
405
410
  end
@@ -416,7 +421,7 @@ module Boson
416
421
  @trailing_non_opts.delete_if {|e|
417
422
  break if STOP_STRINGS.include? e
418
423
  invalid = e.to_s[/^-/]
419
- $stderr.puts "Deleted invalid option '#{e}'" if invalid
424
+ warn "Deleted invalid option '#{e}'" if invalid
420
425
  invalid
421
426
  }
422
427
  end
@@ -436,7 +441,7 @@ module Boson
436
441
  @args = arg + @args
437
442
  end
438
443
  end
439
-
444
+
440
445
  def valid?(arg)
441
446
  if arg.to_s =~ /^--no-(\w+)$/
442
447
  @opt_types.key?(arg) or (@opt_types[dasherize($1)] == :boolean) or
@@ -454,11 +459,11 @@ module Boson
454
459
  $1.split('').any? { |f| valid?("-#{f}") }
455
460
  end
456
461
  end
457
-
462
+
458
463
  def normalize_option(opt)
459
464
  @opt_aliases.key?(opt) ? @opt_aliases[opt] : opt
460
465
  end
461
-
466
+
462
467
  def original_no_opt(opt)
463
468
  @opt_aliases[dasherize(opt)]
464
469
  end