bosonson 0.304.1

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 (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