bosonson 0.304.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +108 -0
- data/LICENSE.txt +22 -0
- data/README.rdoc +181 -0
- data/bin/bss +6 -0
- data/bosonson.gemspec +24 -0
- data/deps.rip +2 -0
- data/lib/boson.rb +96 -0
- data/lib/boson/command.rb +196 -0
- data/lib/boson/commands.rb +7 -0
- data/lib/boson/commands/core.rb +77 -0
- data/lib/boson/commands/web_core.rb +153 -0
- data/lib/boson/index.rb +48 -0
- data/lib/boson/inspector.rb +120 -0
- data/lib/boson/inspectors/argument_inspector.rb +97 -0
- data/lib/boson/inspectors/comment_inspector.rb +100 -0
- data/lib/boson/inspectors/method_inspector.rb +98 -0
- data/lib/boson/libraries/file_library.rb +144 -0
- data/lib/boson/libraries/gem_library.rb +30 -0
- data/lib/boson/libraries/local_file_library.rb +30 -0
- data/lib/boson/libraries/module_library.rb +37 -0
- data/lib/boson/libraries/require_library.rb +23 -0
- data/lib/boson/library.rb +179 -0
- data/lib/boson/loader.rb +118 -0
- data/lib/boson/manager.rb +169 -0
- data/lib/boson/namespace.rb +31 -0
- data/lib/boson/option_command.rb +222 -0
- data/lib/boson/option_parser.rb +475 -0
- data/lib/boson/options.rb +146 -0
- data/lib/boson/pipe.rb +147 -0
- data/lib/boson/pipes.rb +75 -0
- data/lib/boson/repo.rb +107 -0
- data/lib/boson/repo_index.rb +124 -0
- data/lib/boson/runner.rb +81 -0
- data/lib/boson/runners/bin_runner.rb +208 -0
- data/lib/boson/runners/console_runner.rb +58 -0
- data/lib/boson/scientist.rb +182 -0
- data/lib/boson/util.rb +129 -0
- data/lib/boson/version.rb +3 -0
- data/lib/boson/view.rb +95 -0
- data/test/argument_inspector_test.rb +62 -0
- data/test/bin_runner_test.rb +223 -0
- data/test/command_test.rb +22 -0
- data/test/commands_test.rb +22 -0
- data/test/comment_inspector_test.rb +126 -0
- data/test/deps.rip +4 -0
- data/test/file_library_test.rb +42 -0
- data/test/loader_test.rb +235 -0
- data/test/manager_test.rb +114 -0
- data/test/method_inspector_test.rb +90 -0
- data/test/option_parser_test.rb +367 -0
- data/test/options_test.rb +189 -0
- data/test/pipes_test.rb +65 -0
- data/test/repo_index_test.rb +122 -0
- data/test/repo_test.rb +23 -0
- data/test/runner_test.rb +40 -0
- data/test/scientist_test.rb +341 -0
- data/test/test_helper.rb +130 -0
- data/test/util_test.rb +56 -0
- data/vendor/bundle/gems/bacon-bits-0.1.0/deps.rip +1 -0
- data/vendor/bundle/gems/hirb-0.6.0/test/deps.rip +4 -0
- 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
|