bosonson 0.304.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/CHANGELOG.rdoc +108 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.rdoc +181 -0
  4. data/bin/bss +6 -0
  5. data/bosonson.gemspec +24 -0
  6. data/deps.rip +2 -0
  7. data/lib/boson.rb +96 -0
  8. data/lib/boson/command.rb +196 -0
  9. data/lib/boson/commands.rb +7 -0
  10. data/lib/boson/commands/core.rb +77 -0
  11. data/lib/boson/commands/web_core.rb +153 -0
  12. data/lib/boson/index.rb +48 -0
  13. data/lib/boson/inspector.rb +120 -0
  14. data/lib/boson/inspectors/argument_inspector.rb +97 -0
  15. data/lib/boson/inspectors/comment_inspector.rb +100 -0
  16. data/lib/boson/inspectors/method_inspector.rb +98 -0
  17. data/lib/boson/libraries/file_library.rb +144 -0
  18. data/lib/boson/libraries/gem_library.rb +30 -0
  19. data/lib/boson/libraries/local_file_library.rb +30 -0
  20. data/lib/boson/libraries/module_library.rb +37 -0
  21. data/lib/boson/libraries/require_library.rb +23 -0
  22. data/lib/boson/library.rb +179 -0
  23. data/lib/boson/loader.rb +118 -0
  24. data/lib/boson/manager.rb +169 -0
  25. data/lib/boson/namespace.rb +31 -0
  26. data/lib/boson/option_command.rb +222 -0
  27. data/lib/boson/option_parser.rb +475 -0
  28. data/lib/boson/options.rb +146 -0
  29. data/lib/boson/pipe.rb +147 -0
  30. data/lib/boson/pipes.rb +75 -0
  31. data/lib/boson/repo.rb +107 -0
  32. data/lib/boson/repo_index.rb +124 -0
  33. data/lib/boson/runner.rb +81 -0
  34. data/lib/boson/runners/bin_runner.rb +208 -0
  35. data/lib/boson/runners/console_runner.rb +58 -0
  36. data/lib/boson/scientist.rb +182 -0
  37. data/lib/boson/util.rb +129 -0
  38. data/lib/boson/version.rb +3 -0
  39. data/lib/boson/view.rb +95 -0
  40. data/test/argument_inspector_test.rb +62 -0
  41. data/test/bin_runner_test.rb +223 -0
  42. data/test/command_test.rb +22 -0
  43. data/test/commands_test.rb +22 -0
  44. data/test/comment_inspector_test.rb +126 -0
  45. data/test/deps.rip +4 -0
  46. data/test/file_library_test.rb +42 -0
  47. data/test/loader_test.rb +235 -0
  48. data/test/manager_test.rb +114 -0
  49. data/test/method_inspector_test.rb +90 -0
  50. data/test/option_parser_test.rb +367 -0
  51. data/test/options_test.rb +189 -0
  52. data/test/pipes_test.rb +65 -0
  53. data/test/repo_index_test.rb +122 -0
  54. data/test/repo_test.rb +23 -0
  55. data/test/runner_test.rb +40 -0
  56. data/test/scientist_test.rb +341 -0
  57. data/test/test_helper.rb +130 -0
  58. data/test/util_test.rb +56 -0
  59. data/vendor/bundle/gems/bacon-bits-0.1.0/deps.rip +1 -0
  60. data/vendor/bundle/gems/hirb-0.6.0/test/deps.rip +4 -0
  61. metadata +217 -0
@@ -0,0 +1,31 @@
1
+ module Boson
2
+ # Used in all things namespace.
3
+ class Namespace
4
+ # Hash of created namespace names to namespace objects
5
+ def self.namespaces
6
+ @namespaces ||= {}
7
+ end
8
+
9
+ # Creates a namespace given its name and the library it belongs to.
10
+ def self.create(name, library)
11
+ namespaces[name.to_s] = new(name, library)
12
+ Commands::Namespace.send(:define_method, name) { Boson::Namespace.namespaces[name.to_s] }
13
+ end
14
+
15
+ def initialize(name, library)
16
+ raise ArgumentError unless library.module
17
+ @name, @library = name.to_s, library
18
+ class <<self; self end.send :include, @library.module
19
+ end
20
+
21
+ def method_missing(method, *args, &block)
22
+ Boson.can_invoke?(method) ? Boson.invoke(method, *args, &block) : super
23
+ end
24
+
25
+ #:startdoc:
26
+ # List of subcommands for the namespace.
27
+ def boson_commands
28
+ @library.module.instance_methods.map {|e| e.to_s }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,222 @@
1
+ require 'shellwords'
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.
9
+ #
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
57
+ class OptionCommand
58
+ # ArgumentError specific to @command's arguments
59
+ class CommandArgumentError < ::ArgumentError; end
60
+
61
+ BASIC_OPTIONS = {
62
+ :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:
83
+
84
+ class <<self
85
+ #:stopdoc:
86
+ def default_option_parser
87
+ @default_option_parser ||= OptionParser.new default_pipe_options.
88
+ merge(default_render_options.merge(BASIC_OPTIONS))
89
+ end
90
+
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) }
101
+ end
102
+ #:startdoc:
103
+ end
104
+
105
+ attr_accessor :command
106
+ def initialize(cmd)
107
+ @command = cmd
108
+ end
109
+
110
+ # Parses arguments and returns global options, local options and leftover arguments.
111
+ def parse(args)
112
+ if args.size == 1 && args[0].is_a?(String)
113
+ global_opt, parsed_options, args = parse_options Shellwords.shellwords(args[0])
114
+ # last string argument interpreted as args + options
115
+ elsif args.size > 1 && args[-1].is_a?(String)
116
+ temp_args = Runner.in_shell? ? args : Shellwords.shellwords(args.pop)
117
+ global_opt, parsed_options, new_args = parse_options temp_args
118
+ Runner.in_shell? ? args = new_args : args += new_args
119
+ # 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))
122
+ global_opt, parsed_options = parse_options([])[0,2]
123
+ # 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)
127
+ end
128
+ [global_opt || {}, parsed_options, args]
129
+ end
130
+
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
+ def parse_global_options(args)
155
+ option_parser.parse args
156
+ end
157
+
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] ||= "*"
187
+ end
188
+ opts
189
+ end
190
+
191
+ def modify_args(args)
192
+ if @command.default_option && @command.arg_size <= 1 && !@command.has_splat_args? &&
193
+ !args[0].is_a?(Hash) && args[0].to_s[/./] != '-' && !args.join.empty?
194
+ args[0] = "--#{@command.default_option}=#{args[0]}"
195
+ end
196
+ end
197
+
198
+ def check_argument_size(args)
199
+ 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] :
201
+ [@command.arg_size - 1, args.size - 1]
202
+ raise CommandArgumentError, "wrong number of arguments (#{args_size} for #{command_size})"
203
+ end
204
+ end
205
+
206
+ def add_default_args(args, obj)
207
+ if @command.args && args.size < @command.args.size - 1
208
+ # leave off last arg since its an option
209
+ @command.args.slice(0..-2).each_with_index {|arr,i|
210
+ next if args.size >= i + 1 # only fill in once args run out
211
+ break if arr.size != 2 # a default arg value must exist
212
+ begin
213
+ args[i] = @command.file_parsed_args? ? obj.instance_eval(arr[1]) : arr[1]
214
+ rescue Exception
215
+ raise Scientist::Error, "Unable to set default argument at position #{i+1}.\nReason: #{$!.message}"
216
+ end
217
+ }
218
+ end
219
+ end
220
+ #:startdoc:
221
+ end
222
+ end
@@ -0,0 +1,475 @@
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.
31
+ # 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.
39
+ # * Options are also called switches, parameters, flags etc.
40
+ # * Option parsing stops when it comes across a '--'.
41
+ #
42
+ # 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.
45
+ # '--debug' -> {:debug=>true}
46
+ # '--no-debug' -> {:debug=>false}
47
+ # '--no-d' -> {:debug=>false}
48
+ # '-d -f -t' same as '-dft'
49
+ # [*:string*] Sets values by separating name from value with space or '='.
50
+ # '--color red' -> {:color=>'red'}
51
+ # '--color=red' -> {:color=>'red'}
52
+ # '--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.
55
+ # '-n3' -> {:num=>3}
56
+ # '-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.
64
+ # '--fields a:b,c:d' -> {:fields=>{'a'=>'b', 'c'=>'d'} }
65
+ # '--fields a,b:d' -> {:fields=>{'a'=>'d', 'b'=>'d'} }
66
+ # '--fields *:d' -> {:fields=>{'a'=>'d', 'b'=>'d', 'c'=>'d'} }
67
+ #
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).
70
+ class OptionParser
71
+ # Raised for all OptionParser errors
72
+ class Error < StandardError; end
73
+
74
+ NUMERIC = /(\d*\.\d+|\d+)/
75
+ LONG_RE = /^(--\w+[-\w+]*)$/
76
+ SHORT_RE = /^(-[a-zA-Z])$/i
77
+ 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
79
+ SHORT_NUM = /^(-[a-zA-Z])#{NUMERIC}$/i
80
+ 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
+
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
104
+
105
+ # Array of arguments left after defined options have been parsed out by parse.
106
+ def non_opts
107
+ leading_non_opts + trailing_non_opts
108
+ end
109
+
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:
114
+ #
115
+ # Boson::OptionParser.new :debug=>:boolean, 'level'=>:numeric,
116
+ # '--fields'=>:array
117
+ #
118
+ # Options can have default values and implicit types simply by changing the
119
+ # option type for the default value:
120
+ #
121
+ # Boson::OptionParser.new :debug=>true, 'level'=>3.1, :fields=>%w{f1 f2}
122
+ #
123
+ # 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
126
+ # option aliases as an array in the option's key.
127
+ #
128
+ # Boson::OptionParser.new [:debug, :damnit, :D]=>true
129
+ #
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.
132
+ #
133
+ # Options can have additional attributes by passing a hash to the option value instead of
134
+ # a type or default:
135
+ #
136
+ # Boson::OptionParser.new :fields=>{:type=>:array, :values=>%w{f1 f2 f3},
137
+ # :enum=>false}
138
+ #
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:
141
+ #
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.
165
+ def initialize(opts)
166
+ @defaults = {}
167
+ @opt_aliases = {}
168
+ @leading_non_opts, @trailing_non_opts = [], []
169
+
170
+ # build hash of dashed options to option types
171
+ # type can be a hash of opt attributes, a default value or a type symbol
172
+ @opt_types = opts.inject({}) do |mem, (name, type)|
173
+ name, *aliases = name if name.is_a?(Array)
174
+ name = name.to_s
175
+ # we need both nice and dasherized form of option name
176
+ if name.index('-') == 0
177
+ nice_name = undasherize name
178
+ else
179
+ nice_name = name
180
+ name = dasherize name
181
+ end
182
+ # store for later
183
+ @opt_aliases[nice_name] = aliases || []
184
+
185
+ if type.is_a?(Hash)
186
+ @option_attributes ||= {}
187
+ @option_attributes[nice_name] = type
188
+ @opt_aliases[nice_name] = Array(type[:alias]) if type.key?(:alias)
189
+ @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)
194
+ end
195
+
196
+ # set defaults
197
+ case type
198
+ when TrueClass then @defaults[nice_name] = true
199
+ when FalseClass then @defaults[nice_name] = false
200
+ else @defaults[nice_name] = type unless type.is_a?(Symbol)
201
+ end
202
+ mem[name] = !type.nil? ? determine_option_type(type) : type
203
+ mem
204
+ end
205
+
206
+ # generate hash of dashed aliases to dashed options
207
+ @opt_aliases = @opt_aliases.sort.inject({}) {|h, (nice_name, aliases)|
208
+ name = dasherize nice_name
209
+ # allow for aliases as symbols
210
+ aliases.map! {|e| e.to_s.index('-') == 0 || e == false ? e : dasherize(e.to_s) }
211
+ if aliases.empty? and nice_name.length > 1
212
+ opt_alias = nice_name[0,1]
213
+ opt_alias = h.key?("-"+opt_alias) ? "-"+opt_alias.capitalize : "-"+opt_alias
214
+ h[opt_alias] ||= name unless @opt_types.key?(opt_alias)
215
+ else
216
+ aliases.each {|e| h[e] = name if !@opt_types.key?(e) && e != false }
217
+ end
218
+ h
219
+ }
220
+ end
221
+
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.
228
+ def parse(args, flags={})
229
+ @args = args
230
+ # start with defaults
231
+ hash = IndifferentAccessHash.new @defaults
232
+
233
+ @leading_non_opts = []
234
+ unless flags[:opts_before_args]
235
+ @leading_non_opts << shift until current_is_option? || @args.empty? || STOP_STRINGS.include?(peek)
236
+ end
237
+
238
+ while current_is_option?
239
+ case @original_current_option = shift
240
+ when SHORT_SQ_RE
241
+ unshift $1.split('').map { |f| "-#{f}" }
242
+ next
243
+ when EQ_RE, SHORT_NUM
244
+ unshift $2
245
+ option = $1
246
+ when LONG_RE, SHORT_RE
247
+ option = $1
248
+ end
249
+
250
+ dashed_option = normalize_option(option)
251
+ @current_option = undasherize(dashed_option)
252
+ type = option_type(dashed_option)
253
+ validate_option_value(type)
254
+ value = create_option_value(type)
255
+ # set on different line since current_option may change
256
+ hash[@current_option.to_sym] = value
257
+ end
258
+
259
+ @trailing_non_opts = @args
260
+ check_required! hash
261
+ delete_invalid_opts if flags[:delete_invalid_opts]
262
+ hash
263
+ end
264
+
265
+ # Helper method to generate usage. Takes a dashed option and a string value indicating
266
+ # an option value's format.
267
+ def default_usage(opt, val)
268
+ opt + "=" + (@defaults[undasherize(opt)] || val).to_s
269
+ end
270
+
271
+ # Generates one-line usage of all options.
272
+ def formatted_usage
273
+ return "" if @opt_types.empty?
274
+ @opt_types.map do |opt, type|
275
+ val = respond_to?("usage_for_#{type}", true) ? send("usage_for_#{type}", opt) : "#{opt}=:#{type}"
276
+ "[" + val + "]"
277
+ end.join(" ")
278
+ end
279
+
280
+ alias :to_s :formatted_usage
281
+
282
+ # 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
291
+ end
292
+
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
305
+
306
+ def default_render_options #:nodoc:
307
+ {:header_filter=>:capitalize, :description=>false, :filter_any=>true,
308
+ :filter_classes=>{Array=>[:join, ',']}, :hide_empty=>true}
309
+ end
310
+
311
+ # Hash of option names mapped to hash of its external attributes
312
+ def option_attributes
313
+ @option_attributes || {}
314
+ end
315
+
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
+ # 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.
324
+ def current_attributes
325
+ @option_attributes && @option_attributes[@current_option] || {}
326
+ end
327
+
328
+ # Removes dashes from a dashed option i.e. '--date' -> 'date' and '-d' -> 'd'.
329
+ def undasherize(str)
330
+ str.sub(/^-{1,2}/, '')
331
+ end
332
+
333
+ # Adds dashes to an option name i.e. 'date' -> '--date' and 'd' -> '-d'.
334
+ def dasherize(str)
335
+ (str.length > 1 ? "--" : "-") + str
336
+ end
337
+
338
+ # List of option types
339
+ def types
340
+ @opt_types.values
341
+ end
342
+
343
+ # List of option names
344
+ def names
345
+ @opt_types.keys.map {|e| undasherize e }
346
+ end
347
+
348
+ # List of option aliases
349
+ def aliases
350
+ @opt_aliases.keys.map {|e| undasherize e }
351
+ end
352
+
353
+ def option_type(opt)
354
+ if opt =~ /^--no-(\w+)$/
355
+ @opt_types[opt] || @opt_types[dasherize($1)] || @opt_types[original_no_opt($1)]
356
+ else
357
+ @opt_types[opt]
358
+ end
359
+ end
360
+
361
+ private
362
+ def determine_option_type(value)
363
+ return value if value.is_a?(Symbol)
364
+ case value
365
+ when TrueClass, FalseClass then :boolean
366
+ when Numeric then :numeric
367
+ else Util.underscore(value.class.to_s).to_sym
368
+ end
369
+ end
370
+
371
+ def value_shift
372
+ return shift if !current_attributes.key?(:bool_default)
373
+ return shift if @original_current_option =~ EQ_RE
374
+ current_attributes[:bool_default]
375
+ end
376
+
377
+ def create_option_value(type)
378
+ if current_attributes.key?(:bool_default) && (@original_current_option !~ EQ_RE) &&
379
+ !(bool_default = current_attributes[:bool_default]).is_a?(String)
380
+ bool_default
381
+ 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}.")
384
+ end
385
+ end
386
+
387
+ 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
+ }.call(values, possible_value)
392
+ else
393
+ self.class.send(:define_method, :auto_alias_value) {|values, possible_value|
394
+ values.find {|v| v.to_s =~ /^#{possible_value}/ } || possible_value
395
+ }.call(values, possible_value)
396
+ end
397
+ end
398
+
399
+ def validate_enum_values(values, possible_values)
400
+ if current_attributes[:enum]
401
+ Array(possible_values).each {|e|
402
+ raise(Error, "invalid value '#{e}' for option '#{@current_option}'") if !values.include?(e)
403
+ }
404
+ end
405
+ end
406
+
407
+ def validate_option_value(type)
408
+ return if current_attributes.key?(:bool_default)
409
+ if type != :boolean && peek.nil?
410
+ raise Error, "no value provided for option '#{@current_option}'"
411
+ end
412
+ send("validate_#{type}", peek) if respond_to?("validate_#{type}", true)
413
+ end
414
+
415
+ def delete_invalid_opts
416
+ @trailing_non_opts.delete_if {|e|
417
+ break if STOP_STRINGS.include? e
418
+ invalid = e.to_s[/^-/]
419
+ $stderr.puts "Deleted invalid option '#{e}'" if invalid
420
+ invalid
421
+ }
422
+ end
423
+
424
+ def peek
425
+ @args.first
426
+ end
427
+
428
+ def shift
429
+ @args.shift
430
+ end
431
+
432
+ def unshift(arg)
433
+ unless arg.kind_of?(Array)
434
+ @args.unshift(arg)
435
+ else
436
+ @args = arg + @args
437
+ end
438
+ end
439
+
440
+ def valid?(arg)
441
+ if arg.to_s =~ /^--no-(\w+)$/
442
+ @opt_types.key?(arg) or (@opt_types[dasherize($1)] == :boolean) or
443
+ (@opt_types[original_no_opt($1)] == :boolean)
444
+ else
445
+ @opt_types.key?(arg) or @opt_aliases.key?(arg)
446
+ end
447
+ end
448
+
449
+ def current_is_option?
450
+ case peek
451
+ when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
452
+ valid?($1)
453
+ when SHORT_SQ_RE
454
+ $1.split('').any? { |f| valid?("-#{f}") }
455
+ end
456
+ end
457
+
458
+ def normalize_option(opt)
459
+ @opt_aliases.key?(opt) ? @opt_aliases[opt] : opt
460
+ end
461
+
462
+ def original_no_opt(opt)
463
+ @opt_aliases[dasherize(opt)]
464
+ end
465
+
466
+ def check_required!(hash)
467
+ for name, type in @opt_types
468
+ @current_option = undasherize(name)
469
+ if current_attributes[:required] && !hash.key?(@current_option.to_sym)
470
+ raise Error, "no value provided for required option '#{@current_option}'"
471
+ end
472
+ end
473
+ end
474
+ end
475
+ end