boson 0.1.0 → 0.2.0
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/README.rdoc +15 -3
- data/Rakefile +2 -2
- data/VERSION.yml +1 -1
- data/lib/boson.rb +22 -4
- data/lib/boson/command.rb +12 -4
- data/lib/boson/commands/core.rb +19 -19
- data/lib/boson/commands/web_core.rb +18 -5
- data/lib/boson/index.rb +31 -22
- data/lib/boson/inspector.rb +12 -10
- data/lib/boson/inspectors/method_inspector.rb +1 -1
- data/lib/boson/libraries/file_library.rb +31 -11
- data/lib/boson/libraries/gem_library.rb +6 -0
- data/lib/boson/libraries/module_library.rb +24 -4
- data/lib/boson/libraries/require_library.rb +8 -0
- data/lib/boson/library.rb +75 -35
- data/lib/boson/loader.rb +41 -12
- data/lib/boson/manager.rb +19 -14
- data/lib/boson/option_parser.rb +144 -107
- data/lib/boson/options.rb +127 -0
- data/lib/boson/repo.rb +25 -6
- data/lib/boson/runner.rb +2 -2
- data/lib/boson/runners/bin_runner.rb +25 -15
- data/lib/boson/scientist.rb +98 -28
- data/lib/boson/util.rb +20 -14
- data/lib/boson/view.rb +70 -15
- data/test/bin_runner_test.rb +27 -11
- data/test/file_library_test.rb +14 -1
- data/test/index_test.rb +2 -1
- data/test/loader_test.rb +84 -21
- data/test/manager_test.rb +1 -9
- data/test/method_inspector_test.rb +2 -2
- data/test/option_parser_test.rb +176 -20
- data/test/repo_test.rb +1 -0
- data/test/scientist_test.rb +38 -2
- data/test/test_helper.rb +6 -3
- data/test/view_test.rb +58 -0
- metadata +7 -6
- data/test/commands_test.rb +0 -51
@@ -1,6 +1,12 @@
|
|
1
1
|
module Boson
|
2
2
|
# This library loads a gem by the given name. Unlike FileLibrary or ModuleLibrary, this library
|
3
3
|
# doesn't need a module to provide its functionality.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
# >> load_library 'httparty', :class_commands=>{'put'=>'HTTParty.put',
|
7
|
+
# 'delete'=>'HTTParty.delete' }
|
8
|
+
# => true
|
9
|
+
# >> put 'http://someurl.com'
|
4
10
|
class GemLibrary < Library
|
5
11
|
#:stopdoc:
|
6
12
|
def self.is_a_gem?(name)
|
@@ -1,6 +1,22 @@
|
|
1
1
|
module Boson
|
2
|
-
# This library takes a module as a library's name
|
3
|
-
#
|
2
|
+
# This library takes a module or class as a library's name and loads its class methods
|
3
|
+
# as commands. If no commands are given it defaults to loading all of its class methods
|
4
|
+
# as commands. The only method callback (see Loader) this library calls on the
|
5
|
+
# original module/class is config().
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
# >> load_library Math, :commands=>%w{sin cos tan}
|
9
|
+
# => true
|
10
|
+
#
|
11
|
+
# # Let's brush up on ol trig
|
12
|
+
# >> sin (Math::PI/2)
|
13
|
+
# => 1.0
|
14
|
+
# >> tan (Math::PI/4)
|
15
|
+
# => 1.0
|
16
|
+
# # Close enough :)
|
17
|
+
# >> cos (Math::PI/2)
|
18
|
+
# => 6.12323399573677e-17
|
19
|
+
|
4
20
|
class ModuleLibrary < Library
|
5
21
|
#:stopdoc:
|
6
22
|
handles {|source| source.is_a?(Module) }
|
@@ -11,7 +27,11 @@ module Boson
|
|
11
27
|
super Util.underscore(underscore_lib)
|
12
28
|
end
|
13
29
|
|
14
|
-
def
|
30
|
+
def initialize_library_module
|
31
|
+
@class_commands = {@module.to_s=>Array(@commands).empty? ? @module.methods(false) : @commands }
|
32
|
+
@module = nil
|
33
|
+
super
|
34
|
+
end
|
15
35
|
#:startdoc:
|
16
36
|
end
|
17
|
-
end
|
37
|
+
end
|
@@ -1,5 +1,13 @@
|
|
1
1
|
# This library requires the given name. This is useful for loading standard libraries,
|
2
2
|
# non-gem libraries (i.e. rip packages) and anything else in $LOAD_PATH.
|
3
|
+
#
|
4
|
+
# Example:
|
5
|
+
# >> load_library 'fileutils', :class_commands=>{'cd'=>'FileUtils.cd', 'cp'=>'FileUtils.cp'}
|
6
|
+
# => true
|
7
|
+
# >> cd '/home'
|
8
|
+
# => 0
|
9
|
+
# >> Dir.pwd
|
10
|
+
# >> '/home'
|
3
11
|
class Boson::RequireLibrary < Boson::GemLibrary
|
4
12
|
handles {|source|
|
5
13
|
begin
|
data/lib/boson/library.rb
CHANGED
@@ -2,7 +2,33 @@ module Boson
|
|
2
2
|
# A library is a group of commands (Command objects) usually grouped together by a module.
|
3
3
|
# Libraries are loaded from different sources depending on the library subclass. Default library
|
4
4
|
# subclasses are FileLibrary, GemLibrary, RequireLibrary and ModuleLibrary.
|
5
|
+
# See Loader for callbacks a library's module can have.
|
5
6
|
#
|
7
|
+
# == Naming a Library Module
|
8
|
+
# Although you can name a library module almost anything, here's the fine print:
|
9
|
+
# * A module can have any name if it's the only module in a library.
|
10
|
+
# * If there are multiple modules in a file library, the module's name must be a camelized version
|
11
|
+
# of the file's basename i.e. ~/.boson/commands/ruby_core.rb -> RubyCore.
|
12
|
+
# * Although modules are evaluated under the Boson::Commands namespace, Boson will warn you about creating
|
13
|
+
# modules whose name is the same as a top level class/module. The warning is to encourage users to stay
|
14
|
+
# away from error-prone libraries. Once you introduce such a module, _all_ libraries assume the nested module
|
15
|
+
# over the top level module and the top level module has to be prefixed with '::' _everywhere_.
|
16
|
+
#
|
17
|
+
# == Configuration
|
18
|
+
# To configure a library, pass a hash of {library attributes}[link:classes/Boson/Library.html#M000077] under
|
19
|
+
# {the :libraries key}[link:classes/Boson/Repo.html#M000070] of a config file. When not using FileLibrary,
|
20
|
+
# you can give most commands the functionality FileLibrary naturally gives its commands by configuring
|
21
|
+
# the :commands key. Here's a config example of a GemLibrary that does that:
|
22
|
+
# :libraries:
|
23
|
+
# httparty:
|
24
|
+
# :class_commands:
|
25
|
+
# delete: HTTParty.delete
|
26
|
+
# :commands:
|
27
|
+
# delete:
|
28
|
+
# :alias: d
|
29
|
+
# :description: Http delete a given url
|
30
|
+
#
|
31
|
+
# === Creating Your Own Library
|
6
32
|
# To create your own subclass you need to define what sources the subclass can handle with handles().
|
7
33
|
# If handles() returns true then the subclass is chosen to load. See Loader to see what instance methods
|
8
34
|
# to override for a subclass.
|
@@ -18,49 +44,49 @@ module Boson
|
|
18
44
|
end
|
19
45
|
|
20
46
|
# Public attributes for use outside of Boson.
|
21
|
-
ATTRIBUTES = [:gems, :dependencies, :commands, :loaded, :module, :name, :namespace]
|
47
|
+
ATTRIBUTES = [:gems, :dependencies, :commands, :loaded, :module, :name, :namespace, :indexed_namespace]
|
22
48
|
attr_reader *(ATTRIBUTES + [:commands_hash, :library_file, :object_namespace])
|
23
49
|
# Private attribute for use within Boson.
|
24
|
-
attr_reader :
|
50
|
+
attr_reader :no_alias_creation, :new_module, :new_commands
|
25
51
|
# Optional namespace name for a library. When enabled defaults to a library's name.
|
26
52
|
attr_writer :namespace
|
27
53
|
|
28
54
|
# Creates a library object with a hash of attributes which must include a :name attribute.
|
29
55
|
# Each hash pair maps directly to an instance variable and value. Defaults for attributes
|
30
|
-
# are read from config[:libraries][@library_name][@attribute].
|
56
|
+
# are read from config[:libraries][@library_name][@attribute]. When loading libraries, attributes
|
57
|
+
# can also be set via a library module's config() method (see Loader).
|
31
58
|
#
|
32
59
|
# Attributes that can be configured:
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
60
|
+
# [*:dependencies*] An array of libraries that this library depends on. A library won't load
|
61
|
+
# unless its dependencies are loaded first.
|
62
|
+
# [*:commands*] A hash or array of commands that belong to this library. A hash configures command attributes
|
63
|
+
# for the given commands with command names pointing to their configs. See Command.new for a
|
64
|
+
# command's configurable attributes. If an array, the commands are set for the given library,
|
65
|
+
# overidding default command detection. Example:
|
66
|
+
# :commands=>{'commands'=>{:description=>'Lists commands', :alias=>'com'}}
|
67
|
+
# [*:class_commands*] A hash of commands to create. A hash key-pair can map command names to any string of ruby code
|
68
|
+
# that ends with a method call. Or a key-pair can map a class to an array of its class methods
|
69
|
+
# to create commands of the same name. Example:
|
70
|
+
# :class_commands=>{'spy'=>'Bond.spy', 'create'=>'Alias.manager.create',
|
71
|
+
# 'Boson::Util'=>['detect', 'any_const_get']}
|
72
|
+
# [*:force*] Boolean which forces a library to ignore when a library's methods are overriding existing ones.
|
73
|
+
# Use with caution. Default is false.
|
74
|
+
# [*:object_methods*] Boolean which detects any Object/Kernel methods created when loading a library and automatically
|
75
|
+
# adds them to a library's commands. Default is true.
|
76
|
+
# [*:namespace*] Boolean or string which namespaces a library. When true, the library is automatically namespaced
|
77
|
+
# to the library's name. When a string, the library is namespaced to the string. Default is nil.
|
78
|
+
# To control the namespacing of all libraries see Boson::Repo.config.
|
79
|
+
# [*:no_alias_creation*] Boolean which doesn't create aliases for a library. Useful for libraries that configure command
|
80
|
+
# aliases outside of Boson's control. Default is false.
|
52
81
|
def initialize(hash)
|
53
|
-
@name = set_name hash.delete(:name)
|
54
|
-
@loaded = false
|
55
82
|
repo = set_repo
|
56
83
|
@repo_dir = repo.dir
|
84
|
+
@name = set_name hash.delete(:name)
|
85
|
+
@loaded = false
|
57
86
|
@commands_hash = {}
|
58
87
|
@commands = []
|
59
|
-
set_config (repo.config[:libraries][@name] || {}).merge(hash)
|
88
|
+
set_config (repo.config[:libraries][@name] || {}).merge(hash), true
|
60
89
|
set_command_aliases(repo.config[:command_aliases])
|
61
|
-
@namespace = true if Boson.repo.config[:auto_namespace] && @namespace.nil? &&
|
62
|
-
!Boson::Runner.default_libraries.include?(@module)
|
63
|
-
@namespace = clean_name if @namespace
|
64
90
|
end
|
65
91
|
|
66
92
|
# A concise symbol version of a library type i.e. FileLibrary -> :file.
|
@@ -69,9 +95,19 @@ module Boson
|
|
69
95
|
str.downcase.to_sym
|
70
96
|
end
|
71
97
|
|
98
|
+
def namespace(orig=@namespace)
|
99
|
+
@namespace = [String,FalseClass].include?(orig.class) ? orig : begin
|
100
|
+
if (@namespace == true || (Boson.repo.config[:auto_namespace] && !@index))
|
101
|
+
@namespace = clean_name
|
102
|
+
else
|
103
|
+
@namespace = false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
72
108
|
# The object a library uses for executing its commands.
|
73
109
|
def namespace_object
|
74
|
-
@namespace_object ||=
|
110
|
+
@namespace_object ||= namespace ? Boson.invoke(namespace) : Boson.main_object
|
75
111
|
end
|
76
112
|
|
77
113
|
#:stopdoc:
|
@@ -84,7 +120,7 @@ module Boson
|
|
84
120
|
name.to_s or raise ArgumentError, "New library missing required key :name"
|
85
121
|
end
|
86
122
|
|
87
|
-
def set_config(config)
|
123
|
+
def set_config(config, force=false)
|
88
124
|
if (commands = config.delete(:commands))
|
89
125
|
if commands.is_a?(Array)
|
90
126
|
@commands += commands
|
@@ -95,7 +131,7 @@ module Boson
|
|
95
131
|
end
|
96
132
|
end
|
97
133
|
set_command_aliases config.delete(:command_aliases) if config[:command_aliases]
|
98
|
-
set_attributes config,
|
134
|
+
set_attributes config, force
|
99
135
|
end
|
100
136
|
|
101
137
|
def set_command_aliases(command_aliases)
|
@@ -113,16 +149,20 @@ module Boson
|
|
113
149
|
hash.each {|k,v| instance_variable_set("@#{k}", v) if instance_variable_get("@#{k}").nil? || force }
|
114
150
|
end
|
115
151
|
|
116
|
-
def command_objects(names)
|
117
|
-
|
152
|
+
def command_objects(names=self.commands, command_array=Boson.commands)
|
153
|
+
command_array.select {|e| names.include?(e.name) && e.lib == self.name }
|
154
|
+
end
|
155
|
+
|
156
|
+
def command_object(name)
|
157
|
+
command_objects([name])[0]
|
118
158
|
end
|
119
159
|
|
120
160
|
def marshal_dump
|
121
|
-
[@name, @commands, @gems, @module.to_s, @repo_dir]
|
161
|
+
[@name, @commands, @gems, @module.to_s, @repo_dir, @indexed_namespace]
|
122
162
|
end
|
123
163
|
|
124
164
|
def marshal_load(ary)
|
125
|
-
@name, @commands, @gems, @module, @repo_dir = ary
|
165
|
+
@name, @commands, @gems, @module, @repo_dir, @indexed_namespace = ary
|
126
166
|
end
|
127
167
|
#:startdoc:
|
128
168
|
end
|
data/lib/boson/loader.rb
CHANGED
@@ -5,6 +5,27 @@ module Boson
|
|
5
5
|
# This module is mixed into Library to give it load() and reload() functionality.
|
6
6
|
# When creating your own Library subclass, you should override load_source_and_set_module and
|
7
7
|
# reload_source_and_set_module . You can override other methods in this module as needed.
|
8
|
+
#
|
9
|
+
# === Module Callbacks
|
10
|
+
# For libraries that have a module i.e. FileLibrary and GemLibrary, the following class methods
|
11
|
+
# are invoked in the order below when loading a library:
|
12
|
+
#
|
13
|
+
# [*:config*] This method returns a library's hash of attributes as explained by Library.new. This is useful
|
14
|
+
# for distributing libraries with a default configuration. The library attributes specified here
|
15
|
+
# are overridden by ones a user has in their config file except for the :commands attribute, which
|
16
|
+
# is recursively merged together.
|
17
|
+
# [*:append_features*] In addition to its normal behavior, this method's return value determines if a
|
18
|
+
# library is loaded in the current environment. This is useful for libraries that you
|
19
|
+
# want loaded by default but not in some environments i.e. different ruby versions or
|
20
|
+
# in irb but not in script/console. Remember to use super when returning true.
|
21
|
+
# [*:included*] In addition to its normal behavior, this method should be used to require external libraries.
|
22
|
+
# Although requiring dependencies could be done anywhere in a module, putting dependencies here
|
23
|
+
# are encouraged. By not having dependencies hardcoded in a module, it's possible to analyze
|
24
|
+
# and view a library's commands without having to install and load its dependencies.
|
25
|
+
# If creating commands here, note that conflicts with existing commands won't be detected.
|
26
|
+
# [*:after_included*] This method is called after included() to initialize functionality. This is useful for
|
27
|
+
# libraries that are primarily executing ruby code i.e. defining ruby extensions or
|
28
|
+
# setting irb features. This method isn't called when indexing a library.
|
8
29
|
module Loader
|
9
30
|
# Loads a library and its dependencies and returns true if library loads correctly.
|
10
31
|
def load
|
@@ -12,9 +33,9 @@ module Boson
|
|
12
33
|
load_source_and_set_module
|
13
34
|
module_callbacks if @module
|
14
35
|
yield if block_given?
|
15
|
-
(@module || @class_commands) ? detect_additions { load_module_commands } : @namespace =
|
16
|
-
@init_methods.each {|m| namespace_object.send(m) if namespace_object.respond_to?(m) } if @init_methods && !@index
|
36
|
+
(@module || @class_commands) ? detect_additions { load_module_commands } : @namespace = false
|
17
37
|
set_library_commands
|
38
|
+
@indexed_namespace = (@namespace == false) ? nil : @namespace if @index
|
18
39
|
loaded_correctly? && (@loaded = true)
|
19
40
|
end
|
20
41
|
|
@@ -26,7 +47,8 @@ module Boson
|
|
26
47
|
!!@module
|
27
48
|
end
|
28
49
|
|
29
|
-
# Reloads a library from its source and adds new commands.
|
50
|
+
# Reloads a library from its source and adds new commands. Only implemented
|
51
|
+
# for FileLibrary for now.
|
30
52
|
def reload
|
31
53
|
original_commands = @commands
|
32
54
|
reload_source_and_set_module
|
@@ -49,12 +71,13 @@ module Boson
|
|
49
71
|
end
|
50
72
|
|
51
73
|
def load_module_commands
|
52
|
-
|
74
|
+
initialize_library_module
|
53
75
|
rescue MethodConflictError=>e
|
54
|
-
if Boson.repo.config[:error_method_conflicts] ||
|
76
|
+
if Boson.repo.config[:error_method_conflicts] || namespace
|
55
77
|
raise MethodConflictError, e.message
|
56
78
|
else
|
57
79
|
@namespace = clean_name
|
80
|
+
@method_conflict = true
|
58
81
|
$stderr.puts "#{e.message}. Attempting load into the namespace #{@namespace}..."
|
59
82
|
initialize_library_module
|
60
83
|
end
|
@@ -71,20 +94,27 @@ module Boson
|
|
71
94
|
def initialize_library_module
|
72
95
|
@module = @module ? Util.constantize(@module) : Util.create_module(Boson::Commands, clean_name)
|
73
96
|
raise(LoaderError, "No module for library #{@name}") unless @module
|
74
|
-
|
97
|
+
if (conflict = Util.top_level_class_conflict(Boson::Commands, @module.to_s))
|
98
|
+
warn "Library module '#{@module}' may conflict with top level class/module '#{conflict}' references in"+
|
99
|
+
" your libraries. Rename your module to avoid this warning."
|
100
|
+
end
|
101
|
+
|
102
|
+
Manager.create_class_aliases(@module, @class_commands) unless @class_commands.to_s.empty? || @method_conflict
|
75
103
|
check_for_method_conflicts unless @force
|
76
104
|
@namespace = clean_name if @object_namespace
|
77
|
-
|
105
|
+
namespace ? Namespace.create(namespace, self) : include_in_universe
|
78
106
|
end
|
79
107
|
|
80
108
|
def include_in_universe(lib_module=@module)
|
81
109
|
Boson::Universe.send :include, lib_module
|
110
|
+
@module.after_included if lib_module.respond_to?(:after_included) && !@index
|
82
111
|
Boson::Universe.send :extend_object, Boson.main_object
|
83
112
|
end
|
84
113
|
|
85
114
|
def check_for_method_conflicts
|
86
|
-
conflicts =
|
87
|
-
|
115
|
+
conflicts = namespace ? (Boson.can_invoke?(namespace) ? [namespace] : []) :
|
116
|
+
(@module.instance_methods + @module.private_instance_methods) & (Boson.main_object.methods +
|
117
|
+
Boson.main_object.private_methods)
|
88
118
|
unless conflicts.empty?
|
89
119
|
raise MethodConflictError,"The following commands conflict with existing commands: #{conflicts.join(', ')}"
|
90
120
|
end
|
@@ -93,9 +123,8 @@ module Boson
|
|
93
123
|
def set_library_commands
|
94
124
|
aliases = @commands_hash.select {|k,v| @commands.include?(k) }.map {|k,v| v[:alias]}.compact
|
95
125
|
@commands -= aliases
|
96
|
-
@commands.delete(
|
97
|
-
@commands += Boson.invoke(
|
98
|
-
@commands -= @except if @except
|
126
|
+
@commands.delete(namespace) if namespace
|
127
|
+
@commands += Boson.invoke(namespace).boson_commands if namespace && !@pre_defined_commands
|
99
128
|
@commands.uniq!
|
100
129
|
end
|
101
130
|
#:startdoc:
|
data/lib/boson/manager.rb
CHANGED
@@ -7,17 +7,18 @@ module Boson
|
|
7
7
|
# Handles loading and reloading of libraries and commands.
|
8
8
|
class Manager
|
9
9
|
class <<self
|
10
|
+
attr_accessor :failed_libraries
|
11
|
+
|
10
12
|
# Loads a library or an array of libraries with options. Manager loads the first library subclass
|
11
13
|
# to meet a library subclass' criteria in this order: ModuleLibrary, FileLibrary, GemLibrary, RequireLibrary.
|
12
14
|
# ==== Examples:
|
13
15
|
# Manager.load 'my_commands' -> Loads a FileLibrary object from ~/.boson/commands/my_commands.rb
|
14
16
|
# Manager.load 'method_lister' -> Loads a GemLibrary object which requires the method_lister gem
|
17
|
+
# Any options that aren't listed here are passed as library attributes to the libraries (see Library.new)
|
15
18
|
# ==== Options:
|
16
19
|
# [:verbose] Boolean to print each library's loaded status along with more verbose errors. Default is false.
|
17
|
-
# [:index] Boolean to load in index mode. Default is false.
|
18
20
|
def load(libraries, options={})
|
19
|
-
libraries
|
20
|
-
libraries.map {|e|
|
21
|
+
Array(libraries).map {|e|
|
21
22
|
(@library = load_once(e, options)) ? after_load : false
|
22
23
|
}.all?
|
23
24
|
end
|
@@ -45,6 +46,10 @@ module Boson
|
|
45
46
|
end
|
46
47
|
|
47
48
|
#:stopdoc:
|
49
|
+
def failed_libraries
|
50
|
+
@failed_libraries ||= []
|
51
|
+
end
|
52
|
+
|
48
53
|
def add_library(lib)
|
49
54
|
Boson.libraries.delete(Boson.library(lib.name))
|
50
55
|
Boson.libraries << lib
|
@@ -59,19 +64,18 @@ module Boson
|
|
59
64
|
rescue AppendFeaturesFalseError
|
60
65
|
rescue LoaderError=>e
|
61
66
|
FileLibrary.reset_file_cache(library.to_s)
|
62
|
-
|
67
|
+
failed_libraries << library
|
68
|
+
$stderr.puts "Unable to #{load_method} library #{library}. Reason: #{e.message}"
|
63
69
|
rescue Exception=>e
|
64
70
|
FileLibrary.reset_file_cache(library.to_s)
|
65
|
-
|
66
|
-
|
71
|
+
failed_libraries << library
|
72
|
+
message = "Unable to #{load_method} library #{library}. Reason: #{$!}"
|
73
|
+
message += "\n" + e.backtrace.slice(0,3).map {|e| " " + e }.join("\n") if @options[:verbose]
|
74
|
+
$stderr.puts message
|
67
75
|
ensure
|
68
76
|
Inspector.disable if Inspector.enabled
|
69
77
|
end
|
70
78
|
|
71
|
-
def print_error_message(message)
|
72
|
-
$stderr.puts message if !@options[:index] || (@options[:index] && @options[:verbose])
|
73
|
-
end
|
74
|
-
|
75
79
|
def load_once(source, options={})
|
76
80
|
@options = options
|
77
81
|
rescue_load_action(source, :load) do
|
@@ -129,10 +133,6 @@ module Boson
|
|
129
133
|
end
|
130
134
|
|
131
135
|
def create_commands(lib, commands=lib.commands)
|
132
|
-
if lib.except
|
133
|
-
commands -= lib.except
|
134
|
-
lib.except.each {|e| lib.namespace_object.instance_eval("class<<self;self;end").send :undef_method, e }
|
135
|
-
end
|
136
136
|
before_create_commands(lib)
|
137
137
|
commands.each {|e| Boson.commands << Command.create(e, lib)}
|
138
138
|
create_command_aliases(lib, commands) if commands.size > 0 && !lib.no_alias_creation
|
@@ -170,6 +170,11 @@ module Boson
|
|
170
170
|
end
|
171
171
|
|
172
172
|
def create_class_aliases(mod, class_commands)
|
173
|
+
class_commands.each {|k,v|
|
174
|
+
if v.is_a?(Array)
|
175
|
+
class_commands.delete(k).each {|e| class_commands[e] = "#{k}.#{e}"}
|
176
|
+
end
|
177
|
+
}
|
173
178
|
Alias.manager.create_aliases(:any_to_instance_method, mod.to_s=>class_commands.invert)
|
174
179
|
end
|
175
180
|
|
data/lib/boson/option_parser.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
module Boson
|
2
|
-
# Simple Hash with indifferent
|
3
|
-
|
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:
|
4
6
|
def initialize(hash)
|
5
7
|
super()
|
6
8
|
hash.each {|k,v| self[k] = v }
|
@@ -19,22 +21,49 @@ module Boson
|
|
19
21
|
end
|
20
22
|
|
21
23
|
protected
|
22
|
-
|
23
|
-
|
24
|
-
|
24
|
+
def convert_key(key)
|
25
|
+
key.kind_of?(String) ? key.to_sym : key
|
26
|
+
end
|
27
|
+
#:startdoc:
|
25
28
|
end
|
26
29
|
|
27
|
-
# This class
|
28
|
-
#
|
29
|
-
# *nix
|
30
|
-
# *
|
31
|
-
#
|
32
|
-
#
|
33
|
-
# *
|
34
|
-
#
|
35
|
-
# *
|
36
|
-
#
|
37
|
-
#
|
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 OptionParser.parse, an IndifferentAccessHash hash is returned.
|
39
|
+
# * Options are also called switches, parameters, flags etc.
|
40
|
+
#
|
41
|
+
# Default option types:
|
42
|
+
# [*:boolean*] This option has no passed value. To toogle a boolean, prepend with '--no-'.
|
43
|
+
# Multiple booleans can be joined together.
|
44
|
+
# '--debug' -> {:debug=>true}
|
45
|
+
# '--no-debug' -> {:debug=>false}
|
46
|
+
# '--no-d' -> {:debug=>false}
|
47
|
+
# '-d -f -t' same as '-dft'
|
48
|
+
# [*:string*] Sets values by separating name from value with space or '='.
|
49
|
+
# '--color red' -> {:color=>'red'}
|
50
|
+
# '--color=red' -> {:color=>'red'}
|
51
|
+
# '--color "gotta love spaces"' -> {:color=>'gotta love spaces'}
|
52
|
+
# [*:numeric*] Sets values as :string does or by appending number right after aliased name. Shortened form
|
53
|
+
# can be appended to joined booleans.
|
54
|
+
# '-n3' -> {:num=>3}
|
55
|
+
# '-dn3' -> {:debug=>true, :num=>3}
|
56
|
+
# [*:array*] Sets values as :string does. Multiple values are split by a configurable character
|
57
|
+
# Default is ',' (see OptionParser.new). Passing '*' refers to all known :values.
|
58
|
+
# '--fields 1,2,3' -> {:fields=>['1','2','3']}
|
59
|
+
# '--fields *' -> {:fields=>['1','2','3']}
|
60
|
+
# [*:hash*] Sets values as :string does. Key-value pairs are split by ':' and pairs are split by
|
61
|
+
# a configurable character (default ','). Multiple keys can be joined to one value. Passing '*'
|
62
|
+
# as a key refers to all known :keys.
|
63
|
+
# '--fields a:b,c:d' -> {:fields=>{'a'=>'b', 'c'=>'d'} }
|
64
|
+
# '--fields a,b:d' -> {:fields=>{'a'=>'d', 'b'=>'d'} }
|
65
|
+
# '--fields *:d' -> {:fields=>{'a'=>'d', 'b'=>'d', 'c'=>'d'} }
|
66
|
+
#
|
38
67
|
# This is a modified version of Yehuda Katz's Thor::Options class which is a modified version
|
39
68
|
# of Daniel Berger's Getopt::Long class (licensed under Ruby's license).
|
40
69
|
class OptionParser
|
@@ -84,19 +113,29 @@ module Boson
|
|
84
113
|
# Boson::OptionParser.new :fields=>{:type=>:array, :values=>%w{f1 f2 f3},
|
85
114
|
# :enum=>false}
|
86
115
|
#
|
87
|
-
#
|
88
|
-
#
|
89
|
-
# * *:type*: This or :default is required. Available types are :string, :boolean, :array, :numeric.
|
90
|
-
# * *:default*: This or :type is required. This is the default value an option has when not passed.
|
91
|
-
# * *:values*: An array of values an option can have. Available for :array and :string options. Values here
|
92
|
-
# can be aliased by typing a unique string it starts with. For example:
|
93
|
-
#
|
94
|
-
# For values foo, odd, optional: f refers to foo, o to odd and op to optional.
|
116
|
+
# These attributes are available when an option is parsed via current_attributes().
|
117
|
+
# Here are the available option attributes for the default option types:
|
95
118
|
#
|
96
|
-
# *
|
97
|
-
#
|
98
|
-
# *
|
99
|
-
#
|
119
|
+
# [*:type*] This or :default is required. Available types are :string, :boolean, :array, :numeric, :hash.
|
120
|
+
# [*:default*] This or :type is required. This is the default value an option has when not passed.
|
121
|
+
# [*:bool_default*] This is the value an option has when passed as a boolean. However, by enabling this
|
122
|
+
# an option can only have explicit values with '=' i.e. '--index=alias' and no '--index alias'.
|
123
|
+
# If this value is a string, it is parsed as any option value would be. Otherwise, the value is
|
124
|
+
# passed directly without parsing.
|
125
|
+
# [*:required*] Boolean indicating if option is required. Option parses raises error if value not given.
|
126
|
+
# Default is false.
|
127
|
+
# [*:alias*] Alternative way to define option aliases with an option name or an array of them. Useful in yaml files.
|
128
|
+
# Setting to false will prevent creating an automatic alias.
|
129
|
+
# [*:values*] An array of values an option can have. Available for :array and :string options. Values here
|
130
|
+
# can be aliased by typing a unique string it starts with. For example, for values foo, odd, optional,
|
131
|
+
# f refers to foo, o to odd and op to optional.
|
132
|
+
# [*:enum*] Boolean indicating if an option enforces values in :values or :keys. Default is true. For
|
133
|
+
# :array, :hash and :string options.
|
134
|
+
# [*:split*] For :array and :hash options. A string or regular expression on which an array value splits
|
135
|
+
# to produce an array of values. Default is ','.
|
136
|
+
# [*:keys*] :hash option only. An array of values a hash option's keys can have. Keys can be aliased just like :values.
|
137
|
+
# [:default_keys] :hash option only. Default keys to assume when only a value is given. Multiple keys can be joined
|
138
|
+
# by the :split character. Defaults to first key of :keys if :keys given.
|
100
139
|
def initialize(opts)
|
101
140
|
@defaults = {}
|
102
141
|
@opt_aliases = {}
|
@@ -120,19 +159,22 @@ module Boson
|
|
120
159
|
if type.is_a?(Hash)
|
121
160
|
@option_attributes ||= {}
|
122
161
|
@option_attributes[nice_name] = type
|
162
|
+
@opt_aliases[nice_name] = Array(type[:alias]) if type.key?(:alias)
|
123
163
|
@defaults[nice_name] = type[:default] if type[:default]
|
124
|
-
@option_attributes[nice_name][:enum] = true if type.key?(:values)
|
125
|
-
|
164
|
+
@option_attributes[nice_name][:enum] = true if (type.key?(:values) || type.key?(:keys)) &&
|
165
|
+
!type.key?(:enum)
|
166
|
+
@option_attributes[nice_name][:default_keys] ||= type[:keys][0] if type.key?(:keys)
|
167
|
+
type = type[:type] || (!type[:default].nil? ? determine_option_type(type[:default]) : :boolean)
|
126
168
|
end
|
127
169
|
|
128
170
|
# set defaults
|
129
171
|
case type
|
130
|
-
|
131
|
-
|
132
|
-
|
172
|
+
when TrueClass then @defaults[nice_name] = true
|
173
|
+
when FalseClass then @defaults[nice_name] = false
|
174
|
+
else
|
175
|
+
@defaults[nice_name] = type unless type.is_a?(Symbol)
|
133
176
|
end
|
134
|
-
|
135
|
-
mem[name] = determine_option_type(type) || type
|
177
|
+
mem[name] = !type.nil? ? determine_option_type(type) : type
|
136
178
|
mem
|
137
179
|
end
|
138
180
|
|
@@ -146,13 +188,13 @@ module Boson
|
|
146
188
|
opt_alias = h.key?("-"+opt_alias) ? "-"+opt_alias.capitalize : "-"+opt_alias
|
147
189
|
h[opt_alias] ||= name unless @opt_types.key?(opt_alias)
|
148
190
|
else
|
149
|
-
aliases.each {
|
191
|
+
aliases.each {|e| h[e] = name if !@opt_types.key?(e) && e != false }
|
150
192
|
end
|
151
193
|
h
|
152
194
|
}
|
153
195
|
end
|
154
196
|
|
155
|
-
# Parses an array of arguments for defined options to return
|
197
|
+
# Parses an array of arguments for defined options to return an IndifferentAccessHash. Once the parser
|
156
198
|
# recognizes a valid option, it continues to parse until an non option argument is detected.
|
157
199
|
# Flags that can be passed to the parser:
|
158
200
|
# * :opts_before_args: When true options must come before arguments. Default is false.
|
@@ -168,7 +210,7 @@ module Boson
|
|
168
210
|
end
|
169
211
|
|
170
212
|
while current_is_option?
|
171
|
-
case shift
|
213
|
+
case @original_current_option = shift
|
172
214
|
when SHORT_SQ_RE
|
173
215
|
unshift $1.split('').map { |f| "-#{f}" }
|
174
216
|
next
|
@@ -178,12 +220,12 @@ module Boson
|
|
178
220
|
when LONG_RE, SHORT_RE
|
179
221
|
option = $1
|
180
222
|
end
|
181
|
-
|
223
|
+
|
182
224
|
dashed_option = normalize_option(option)
|
183
225
|
@current_option = undasherize(dashed_option)
|
184
226
|
type = option_type(dashed_option)
|
185
227
|
validate_option_value(type)
|
186
|
-
value =
|
228
|
+
value = create_option_value(type)
|
187
229
|
# set on different line since current_option may change
|
188
230
|
hash[@current_option.to_sym] = value
|
189
231
|
end
|
@@ -194,24 +236,18 @@ module Boson
|
|
194
236
|
hash
|
195
237
|
end
|
196
238
|
|
197
|
-
#
|
239
|
+
# Helper method to generate usage. Takes a dashed option and a string value indicating
|
240
|
+
# an option value's format.
|
241
|
+
def default_usage(opt, val)
|
242
|
+
opt + "=" + (@defaults[undasherize(opt)] || val).to_s
|
243
|
+
end
|
244
|
+
|
245
|
+
# Generates one-line usage of all options.
|
198
246
|
def formatted_usage
|
199
247
|
return "" if @opt_types.empty?
|
200
248
|
@opt_types.map do |opt, type|
|
201
|
-
|
202
|
-
|
203
|
-
"[#{opt}]"
|
204
|
-
when :required
|
205
|
-
opt + "=" + opt.gsub(/\-/, "").upcase
|
206
|
-
else
|
207
|
-
sample = @defaults[undasherize(opt)]
|
208
|
-
sample ||= case type
|
209
|
-
when :string then undasherize(opt).gsub(/\-/, "_").upcase
|
210
|
-
when :numeric then "N"
|
211
|
-
when :array then "A,B,C"
|
212
|
-
end
|
213
|
-
"[" + opt + "=" + sample.to_s + "]"
|
214
|
-
end
|
249
|
+
val = respond_to?("usage_for_#{type}", true) ? send("usage_for_#{type}", opt) : "#{opt}=:#{type}"
|
250
|
+
"[" + val + "]"
|
215
251
|
end.join(" ")
|
216
252
|
end
|
217
253
|
|
@@ -221,51 +257,61 @@ module Boson
|
|
221
257
|
def print_usage_table(render_options={})
|
222
258
|
aliases = @opt_aliases.invert
|
223
259
|
additional = [:desc, :values].select {|e| (@option_attributes || {}).values.any? {|f| f.key?(e) } }
|
260
|
+
additional_opts = {:desc=>[:desc], :values=>[:values, :keys]}
|
224
261
|
opts = @opt_types.keys.sort.inject([]) {|t,e|
|
225
262
|
h = {:name=>e, :aliases=>aliases[e], :type=>@opt_types[e]}
|
226
|
-
additional.each {|f|
|
263
|
+
additional.each {|f|
|
264
|
+
h[f] = additional_opts[f].map {|g| (@option_attributes[undasherize(e)] || {})[g]}.flatten.compact
|
265
|
+
}
|
227
266
|
t << h
|
228
267
|
}
|
229
|
-
render_options = {:headers=>{:name=>"Option", :aliases=>"Alias", :desc=>'Description', :values=>'Values'},
|
268
|
+
render_options = {:headers=>{:name=>"Option", :aliases=>"Alias", :desc=>'Description', :values=>'Values/Keys', :type=>'Type'},
|
230
269
|
:fields=>[:name, :aliases, :type] + additional, :description=>false, :filters=>{:values=>lambda {|e| (e || []).join(',')} }
|
231
270
|
}.merge(render_options)
|
232
271
|
View.render opts, render_options
|
233
272
|
end
|
234
273
|
|
274
|
+
# Hash of option attributes for the currently parsed option. _Any_ hash keys
|
275
|
+
# passed to an option are available here. This means that an option type can have any
|
276
|
+
# user-defined attributes available during option parsing and object creation.
|
277
|
+
def current_attributes
|
278
|
+
@option_attributes && @option_attributes[@current_option] || {}
|
279
|
+
end
|
280
|
+
|
281
|
+
# Removes dashes from a dashed option i.e. '--date' -> 'date' and '-d' -> 'd'.
|
282
|
+
def undasherize(str)
|
283
|
+
str.sub(/^-{1,2}/, '')
|
284
|
+
end
|
285
|
+
|
286
|
+
# Adds dashes to an option name i.e. 'date' -> '--date' and 'd' -> '-d'.
|
287
|
+
def dasherize(str)
|
288
|
+
(str.length > 1 ? "--" : "-") + str
|
289
|
+
end
|
290
|
+
|
235
291
|
private
|
236
292
|
def determine_option_type(value)
|
293
|
+
return value if value.is_a?(Symbol)
|
237
294
|
case value
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
else nil
|
295
|
+
when TrueClass, FalseClass then :boolean
|
296
|
+
when Numeric then :numeric
|
297
|
+
else
|
298
|
+
Util.underscore(value.class.to_s).to_sym
|
243
299
|
end
|
244
300
|
end
|
245
301
|
|
246
|
-
def
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
peek.index('.') ? shift.to_f : shift.to_i
|
260
|
-
when :array
|
261
|
-
splitter = (@option_attributes[@current_option][:split] rescue nil) || ','
|
262
|
-
array = shift.split(splitter)
|
263
|
-
if values = @option_attributes[@current_option][:values].sort_by {|e| e.to_s } rescue nil
|
264
|
-
array.each_with_index {|e,i|
|
265
|
-
(value = auto_alias_value(values, e)) && array[i] = value
|
266
|
-
}
|
267
|
-
end
|
268
|
-
array
|
302
|
+
def value_shift
|
303
|
+
return shift if !current_attributes.key?(:bool_default)
|
304
|
+
return shift if @original_current_option =~ EQ_RE
|
305
|
+
current_attributes[:bool_default]
|
306
|
+
end
|
307
|
+
|
308
|
+
def create_option_value(type)
|
309
|
+
if current_attributes.key?(:bool_default) && (@original_current_option !~ EQ_RE) &&
|
310
|
+
!(bool_default = current_attributes[:bool_default]).is_a?(String)
|
311
|
+
bool_default
|
312
|
+
else
|
313
|
+
respond_to?("create_#{type}", true) ? send("create_#{type}", type != :boolean ? value_shift : nil) :
|
314
|
+
raise(Error, "Option '#{@current_option}' is invalid option type #{type.inspect}.")
|
269
315
|
end
|
270
316
|
end
|
271
317
|
|
@@ -275,18 +321,11 @@ module Boson
|
|
275
321
|
end
|
276
322
|
|
277
323
|
def validate_option_value(type)
|
324
|
+
return if current_attributes.key?(:bool_default)
|
278
325
|
if type != :boolean && peek.nil?
|
279
326
|
raise Error, "no value provided for option '#{@current_option}'"
|
280
327
|
end
|
281
|
-
|
282
|
-
case type
|
283
|
-
when :required, :string
|
284
|
-
raise Error, "cannot pass '#{peek}' as an argument to option '#{@current_option}'" if valid?(peek)
|
285
|
-
when :numeric
|
286
|
-
unless peek =~ NUMERIC and $& == peek
|
287
|
-
raise Error, "expected numeric value for option '#{@current_option}'; got #{peek.inspect}"
|
288
|
-
end
|
289
|
-
end
|
328
|
+
send("validate_#{type}", peek) if respond_to?("validate_#{type}", true)
|
290
329
|
end
|
291
330
|
|
292
331
|
def delete_invalid_opts
|
@@ -299,14 +338,6 @@ module Boson
|
|
299
338
|
end
|
300
339
|
end
|
301
340
|
|
302
|
-
def undasherize(str)
|
303
|
-
str.sub(/^-{1,2}/, '')
|
304
|
-
end
|
305
|
-
|
306
|
-
def dasherize(str)
|
307
|
-
(str.length > 1 ? "--" : "-") + str
|
308
|
-
end
|
309
|
-
|
310
341
|
def peek
|
311
342
|
@args.first
|
312
343
|
end
|
@@ -325,7 +356,8 @@ module Boson
|
|
325
356
|
|
326
357
|
def valid?(arg)
|
327
358
|
if arg.to_s =~ /^--no-(\w+)$/
|
328
|
-
@opt_types.key?(arg) or (@opt_types["--#{$1}"] == :boolean)
|
359
|
+
@opt_types.key?(arg) or (@opt_types["--#{$1}"] == :boolean) or
|
360
|
+
(@opt_types[original_no_opt($1)] == :boolean)
|
329
361
|
else
|
330
362
|
@opt_types.key?(arg) or @opt_aliases.key?(arg)
|
331
363
|
end
|
@@ -346,16 +378,21 @@ module Boson
|
|
346
378
|
|
347
379
|
def option_type(opt)
|
348
380
|
if opt =~ /^--no-(\w+)$/
|
349
|
-
@opt_types[opt] || @opt_types["--#{$1}"]
|
381
|
+
@opt_types[opt] || @opt_types["--#{$1}"] || @opt_types[original_no_opt($1)]
|
350
382
|
else
|
351
383
|
@opt_types[opt]
|
352
384
|
end
|
353
385
|
end
|
354
|
-
|
386
|
+
|
387
|
+
def original_no_opt(opt)
|
388
|
+
@opt_aliases[dasherize(opt)]
|
389
|
+
end
|
390
|
+
|
355
391
|
def check_required!(hash)
|
356
392
|
for name, type in @opt_types
|
357
|
-
|
358
|
-
|
393
|
+
@current_option = undasherize(name)
|
394
|
+
if current_attributes[:required] && !hash.key?(@current_option.to_sym)
|
395
|
+
raise Error, "no value provided for required option '#{@current_option}'"
|
359
396
|
end
|
360
397
|
end
|
361
398
|
end
|