boson 0.4.0 → 1.0.0

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