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