boson 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|