boson 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +21 -14
- data/VERSION.yml +1 -1
- data/lib/boson.rb +5 -5
- data/lib/boson/command.rb +20 -21
- data/lib/boson/commands/core.rb +5 -6
- data/lib/boson/index.rb +21 -98
- data/lib/boson/inspector.rb +28 -2
- data/lib/boson/inspectors/comment_inspector.rb +5 -7
- data/lib/boson/inspectors/method_inspector.rb +4 -17
- data/lib/boson/libraries/file_library.rb +22 -27
- data/lib/boson/libraries/local_file_library.rb +30 -0
- data/lib/boson/libraries/module_library.rb +1 -1
- data/lib/boson/library.rb +15 -9
- data/lib/boson/loader.rb +3 -18
- data/lib/boson/manager.rb +4 -31
- data/lib/boson/option_command.rb +204 -0
- data/lib/boson/option_parser.rb +13 -2
- data/lib/boson/options.rb +1 -1
- data/lib/boson/pipe.rb +157 -0
- data/lib/boson/repo.rb +20 -1
- data/lib/boson/repo_index.rb +123 -0
- data/lib/boson/runner.rb +3 -4
- data/lib/boson/runners/bin_runner.rb +23 -8
- data/lib/boson/runners/console_runner.rb +1 -2
- data/lib/boson/scientist.rb +48 -225
- data/lib/boson/view.rb +50 -64
- data/test/bin_runner_test.rb +48 -7
- data/test/comment_inspector_test.rb +7 -6
- data/test/config/index.marshal +0 -0
- data/test/file_library_test.rb +1 -22
- data/test/loader_test.rb +5 -13
- data/test/manager_test.rb +4 -4
- data/test/method_inspector_test.rb +7 -2
- data/test/{view_test.rb → pipe_test.rb} +11 -11
- data/test/{index_test.rb → repo_index_test.rb} +26 -26
- data/test/scientist_test.rb +2 -2
- metadata +11 -6
data/lib/boson/pipe.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
module Boson
|
2
|
+
# This module passes a command's return value through methods/commands specified as pipe options. Pipe options
|
3
|
+
# are processed in this order:
|
4
|
+
# * A :query option searches an array of objects or hashes using Pipe.search_object.
|
5
|
+
# * A :sort option sorts an array of objects or hashes using Pipe.sort_object.
|
6
|
+
# * All user-defined pipe options (:pipe_options key in Repo.config) are processed in random order.
|
7
|
+
#
|
8
|
+
# Some points:
|
9
|
+
# * User-defined pipes call a command (the option's name by default). It's the user's responsibility to have this
|
10
|
+
# command loaded when used. The easiest way to do this is by adding the pipe command's library to :defaults in main config.
|
11
|
+
# * By default, pipe commands do not modify the value their given. This means you can activate multiple pipes using
|
12
|
+
# a method's original return value.
|
13
|
+
# * If you want a pipe command to modify the value its given, set its pipe option's :filter attribute to true.
|
14
|
+
# * A pipe command expects a command's return value as its first argument. If the pipe option takes an argument, it's passed
|
15
|
+
# on as a second argument.
|
16
|
+
# * When piping occurs in relation to rendering depends on the Hirb view. With the default Hirb view, piping occurs
|
17
|
+
# occurs in the middle of the rendering, after Hirb has converted the return value into an array of hashes.
|
18
|
+
# If using a custom Hirb view, piping occurs before rendering.
|
19
|
+
#
|
20
|
+
# === Default Pipes: Search and Sort
|
21
|
+
# The default pipe options, :query, :sort and :reverse_sort, are quite useful for searching and sorting arrays:
|
22
|
+
# Some examples using default commands:
|
23
|
+
# # Searches commands in the full_name field for 'lib' and sorts results by that field.
|
24
|
+
# bash> boson commands -q=f:lib -s=f # or commands --query=full_name:lib --sort=full_name
|
25
|
+
#
|
26
|
+
# # Multiple fields can be searched if separated by a ','. This searches the full_name and description fields.
|
27
|
+
# bash> boson commands -q=f,d:web # or commands --query=full_name,description:web
|
28
|
+
#
|
29
|
+
# # All fields can be queried using a '*'.
|
30
|
+
# # Searches all library fields and then reverse sorts on name field
|
31
|
+
# bash> boson libraries -q=*:core -s=n -R # or libraries --query=*:core --sort=name --reverse_sort
|
32
|
+
#
|
33
|
+
# # Multiple searches can be joined together by ','
|
34
|
+
# # Searches for libraries that have the name matching core or a library_type matching gem
|
35
|
+
# bash> boson libraries -q=n:core,l:gem # or libraries --query=name:core,library_type:gem
|
36
|
+
#
|
37
|
+
# In these examples, we queried commands and examples with an explicit --query. However, -q or --query isn't necessary
|
38
|
+
# for these commands because they already default to it when not present. This behavior comes from the default_option
|
39
|
+
# attribute a command can have.
|
40
|
+
#
|
41
|
+
# === User-defined Pipes
|
42
|
+
# Let's say you want to have two commands, browser and copy, you want to make available as pipe options:
|
43
|
+
# # Opens url in browser. This command already ships with Boson.
|
44
|
+
# def browser(url)
|
45
|
+
# system('open', url)
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# # Copy to clipboard
|
49
|
+
# def copy(str)
|
50
|
+
# IO.popen('pbcopy', 'w+') {|clipboard| clipboard.write(str)}
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# To configure them, drop the following config in ~/.boson/config/boson.yml:
|
54
|
+
# :pipe_options:
|
55
|
+
# :browser:
|
56
|
+
# :type: :boolean
|
57
|
+
# :desc: Open in browser
|
58
|
+
# :copy:
|
59
|
+
# :type: :boolean
|
60
|
+
# :desc: Copy to clipboard
|
61
|
+
#
|
62
|
+
# Now for any command that returns a url string, these pipe options can be turned on to execute the url.
|
63
|
+
#
|
64
|
+
# Some examples of these options using commands from {my libraries}[http://github.com/cldwalker/irbfiles]:
|
65
|
+
# # Creates a gist and then opens url in browser and copies it.
|
66
|
+
# bash> cat some_file | boson gist -bC # or cat some_file | boson gist --browser --copy
|
67
|
+
#
|
68
|
+
# # Generates rdoc in current directory and then opens it in browser
|
69
|
+
# irb>> rdoc '-b' # or rdoc '--browser'
|
70
|
+
module Pipe
|
71
|
+
extend self
|
72
|
+
|
73
|
+
# Main method which processes all pipe commands, both default and user-defined ones.
|
74
|
+
def process(object, options)
|
75
|
+
if object.is_a?(Array)
|
76
|
+
object = search_object(object, options[:query]) if options[:query]
|
77
|
+
object = sort_object(object, options[:sort], options[:reverse_sort]) if options[:sort]
|
78
|
+
end
|
79
|
+
process_user_pipes(object, options)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Searches an array of objects or hashes using the :query option.
|
83
|
+
# This option is a hash of fields mapped to their search terms. Searches are OR-ed.
|
84
|
+
def search_object(object, query_hash)
|
85
|
+
if object[0].is_a?(Hash)
|
86
|
+
TableCallbacks.search_callback(object, :query=>query_hash)
|
87
|
+
else
|
88
|
+
query_hash.map {|field,query| object.select {|e| e.send(field).to_s =~ /#{query}/i } }.flatten.uniq
|
89
|
+
end
|
90
|
+
rescue NoMethodError
|
91
|
+
$stderr.puts "Query failed with nonexistant method '#{$!.message[/`(.*)'/,1]}'"
|
92
|
+
end
|
93
|
+
|
94
|
+
# Sorts an array of objects or hashes using a sort field. Sort is reversed with reverse_sort set to true.
|
95
|
+
def sort_object(object, sort, reverse_sort=false)
|
96
|
+
if object[0].is_a?(Hash)
|
97
|
+
TableCallbacks.sort_callback(object, :sort=>sort, :reverse_sort=>reverse_sort)
|
98
|
+
else
|
99
|
+
sort_lambda = object.all? {|e| e.send(sort).respond_to?(:<=>) } ? lambda {|e| e.send(sort) || ''} :
|
100
|
+
lambda {|e| e.send(sort).to_s }
|
101
|
+
object = object.sort_by &sort_lambda
|
102
|
+
object = object.reverse if reverse_sort
|
103
|
+
object
|
104
|
+
end
|
105
|
+
rescue NoMethodError, ArgumentError
|
106
|
+
$stderr.puts "Sort failed with nonexistant method '#{sort}'"
|
107
|
+
end
|
108
|
+
|
109
|
+
#:stopdoc:
|
110
|
+
def pipe_options
|
111
|
+
@pipe_options ||= Boson.repo.config[:pipe_options] || {}
|
112
|
+
end
|
113
|
+
|
114
|
+
def process_user_pipes(result, options)
|
115
|
+
(options.keys & pipe_options.keys).each {|e|
|
116
|
+
command = pipe_options[e][:pipe] ||= e
|
117
|
+
pipe_result = pipe_options[e][:type] == :boolean ? Boson.invoke(command, result) :
|
118
|
+
Boson.invoke(command, result, options[e])
|
119
|
+
result = pipe_result if pipe_options[e][:filter]
|
120
|
+
}
|
121
|
+
result
|
122
|
+
end
|
123
|
+
#:startdoc:
|
124
|
+
|
125
|
+
# Callbacks used by Hirb::Helpers::Table to search,sort and run custom pipe commands on arrays of hashes.
|
126
|
+
module TableCallbacks
|
127
|
+
extend self
|
128
|
+
# Case-insensitive searches an array of hashes using the option :query. Numerical string keys
|
129
|
+
# in :query are converted to actual numbers to interface with Hirb. See Pipe.search_object for more
|
130
|
+
# about :query.
|
131
|
+
def search_callback(obj, options)
|
132
|
+
!options[:query] ? obj : begin
|
133
|
+
options[:query].map {|field,query|
|
134
|
+
field = field.to_i if field.to_s[/^\d+$/]
|
135
|
+
obj.select {|e| e[field].to_s =~ /#{query}/i }
|
136
|
+
}.flatten.uniq
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Sorts an array of hashes using :sort option and reverses the sort with :reverse_sort option.
|
141
|
+
def sort_callback(obj, options)
|
142
|
+
return obj unless options[:sort]
|
143
|
+
sort = options[:sort].to_s[/^\d+$/] ? options[:sort].to_i : options[:sort]
|
144
|
+
sort_lambda = (obj.all? {|e| e[sort].respond_to?(:<=>) } ? lambda {|e| e[sort] } : lambda {|e| e[sort].to_s })
|
145
|
+
obj = obj.sort_by &sort_lambda
|
146
|
+
obj = obj.reverse if options[:reverse_sort]
|
147
|
+
obj
|
148
|
+
end
|
149
|
+
|
150
|
+
# Processes user-defined pipes in random order.
|
151
|
+
def z_user_pipes_callback(obj, options)
|
152
|
+
Pipe.process_user_pipes(obj, options)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
Hirb::Helpers::Table.send :include, Boson::Pipe::TableCallbacks
|
data/lib/boson/repo.rb
CHANGED
@@ -35,7 +35,7 @@ module Boson
|
|
35
35
|
# A hash read from the YAML config file at config/boson.yml.
|
36
36
|
# {See here}[http://github.com/cldwalker/irbfiles/blob/master/boson/config/boson.yml] for an example config file.
|
37
37
|
# Top level config keys, library attributes and config attributes need to be symbols.
|
38
|
-
# ====
|
38
|
+
# ==== Config keys for all repositories:
|
39
39
|
# [:libraries] Hash of libraries mapping their name to attribute hashes. See Library.new for configurable attributes.
|
40
40
|
# Example:
|
41
41
|
# :libraries=>{'completion'=>{:namespace=>true}}
|
@@ -53,12 +53,23 @@ module Boson
|
|
53
53
|
# [:add_load_path] Boolean specifying whether to add a load path pointing to the lib subdirectory/. This is useful in sharing
|
54
54
|
# classes between libraries without resorting to packaging them as gems. Defaults to false if the lib
|
55
55
|
# subdirectory doesn't exist in the boson directory.
|
56
|
+
#
|
57
|
+
# ==== Config keys specific to the main repo config ~/.boson/config/boson.yml
|
58
|
+
# [:pipe_options] Hash of options available to all option commands for piping (see Pipe). A pipe option has the
|
59
|
+
# {normal option attributes}[link:classes/Boson/OptionParser.html#M000081] and these:
|
60
|
+
# * :pipe: Specifies the command to call when piping. Defaults to the pipe's option name.
|
61
|
+
# * :filter: Boolean which indicates that the pipe command will modify its input with what it returns.
|
62
|
+
# Default is false.
|
63
|
+
# [:render_options] Hash of render options available to all option commands to be passed to a Hirb view (see View). Since
|
64
|
+
# this merges with default render options, it's possible to override default render options.
|
56
65
|
# [:error_method_conflicts] Boolean specifying library loading behavior when its methods conflicts with existing methods in
|
57
66
|
# the global namespace. When set to false, Boson automatically puts the library in its own namespace.
|
58
67
|
# When set to true, the library fails to load explicitly. Default is false.
|
59
68
|
# [:console] Console to load when using --console from commandline. Default is irb.
|
60
69
|
# [:auto_namespace] Boolean which automatically namespaces all user-defined libraries. Be aware this can break libraries which
|
61
70
|
# depend on commands from other libraries. Default is false.
|
71
|
+
# [:ignore_directories] Array of directories to ignore when detecting local repositories for Boson.local_repo.
|
72
|
+
# [:no_auto_render] When set, turns off commandline auto-rendering of a command's output. Default is false.
|
62
73
|
def config(reload=false)
|
63
74
|
if reload || @config.nil?
|
64
75
|
begin
|
@@ -72,5 +83,13 @@ module Boson
|
|
72
83
|
end
|
73
84
|
@config
|
74
85
|
end
|
86
|
+
|
87
|
+
def detected_libraries #:nodoc:
|
88
|
+
Dir[File.join(commands_dir, '**/*.rb')].map {|e| e.gsub(/^#{commands_dir}\/|\.rb$/, '') }
|
89
|
+
end
|
90
|
+
|
91
|
+
def all_libraries #:nodoc:
|
92
|
+
(detected_libraries + config[:libraries].keys).uniq
|
93
|
+
end
|
75
94
|
end
|
76
95
|
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
module Boson
|
3
|
+
# This class provides an index for commands and libraries of a given a Repo.
|
4
|
+
# When this index updates, it detects library files whose md5 hash have changed and reindexes them.
|
5
|
+
# The index is stored with Marshal at config/index.marshal (relative to a Repo's root directory).
|
6
|
+
# Since the index is marshaled, putting lambdas/procs in it will break it.If an index gets corrupted,
|
7
|
+
# simply delete it and next time Boson needs it, the index will be recreated.
|
8
|
+
|
9
|
+
class RepoIndex
|
10
|
+
attr_reader :libraries, :commands, :repo
|
11
|
+
def initialize(repo)
|
12
|
+
@repo = repo
|
13
|
+
end
|
14
|
+
|
15
|
+
# Updates the index.
|
16
|
+
def update(options={})
|
17
|
+
libraries_to_update = !exists? ? repo.all_libraries : options[:libraries] || changed_libraries
|
18
|
+
read_and_transfer(libraries_to_update)
|
19
|
+
if options[:verbose]
|
20
|
+
puts !exists? ? "Generating index for all #{libraries_to_update.size} libraries. Patience ... is a bitch" :
|
21
|
+
(libraries_to_update.empty? ? "No libraries indexed" :
|
22
|
+
"Indexing the following libraries: #{libraries_to_update.join(', ')}")
|
23
|
+
end
|
24
|
+
Manager.failed_libraries = []
|
25
|
+
unless libraries_to_update.empty?
|
26
|
+
Manager.load(libraries_to_update, options.merge(:index=>true))
|
27
|
+
unless Manager.failed_libraries.empty?
|
28
|
+
$stderr.puts("Error: These libraries failed to load while indexing: #{Manager.failed_libraries.join(', ')}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
write(Manager.failed_libraries)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Reads and initializes index.
|
35
|
+
def read
|
36
|
+
return if @read
|
37
|
+
@libraries, @commands, @lib_hashes = exists? ? Marshal.load(File.read(marshal_file)) : [[], [], {}]
|
38
|
+
delete_stale_libraries_and_commands
|
39
|
+
set_command_namespaces
|
40
|
+
@read = true
|
41
|
+
end
|
42
|
+
|
43
|
+
# Writes/saves current index to config/index.marshal.
|
44
|
+
def write(failed_libraries=[])
|
45
|
+
latest = latest_hashes
|
46
|
+
failed_libraries.each {|e| latest.delete(e) }
|
47
|
+
save_marshal_index Marshal.dump([Boson.libraries, Boson.commands, latest])
|
48
|
+
end
|
49
|
+
|
50
|
+
#:stopdoc:
|
51
|
+
def read_and_transfer(ignored_libraries=[])
|
52
|
+
read
|
53
|
+
existing_libraries = (Boson.libraries.map {|e| e.name} + ignored_libraries).uniq
|
54
|
+
libraries_to_add = @libraries.select {|e| !existing_libraries.include?(e.name)}
|
55
|
+
Boson.libraries += libraries_to_add
|
56
|
+
# depends on saved commands being correctly associated with saved libraries
|
57
|
+
Boson.commands += libraries_to_add.map {|e| e.command_objects(e.commands, @commands) }.flatten
|
58
|
+
end
|
59
|
+
|
60
|
+
def exists?
|
61
|
+
File.exists? marshal_file
|
62
|
+
end
|
63
|
+
|
64
|
+
def save_marshal_index(marshal_string)
|
65
|
+
File.open(marshal_file, 'w') {|f| f.write marshal_string }
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete_stale_libraries_and_commands
|
69
|
+
cached_libraries = @lib_hashes.keys
|
70
|
+
libs_to_delete = @libraries.select {|e| !cached_libraries.include?(e.name) && e.is_a?(FileLibrary) }
|
71
|
+
names_to_delete = libs_to_delete.map {|e| e.name }
|
72
|
+
libs_to_delete.each {|e| @libraries.delete(e) }
|
73
|
+
@commands.delete_if {|e| names_to_delete.include? e.lib }
|
74
|
+
end
|
75
|
+
|
76
|
+
# set namespaces for commands
|
77
|
+
def set_command_namespaces
|
78
|
+
lib_commands = @commands.inject({}) {|t,e| (t[e.lib] ||= []) << e; t }
|
79
|
+
namespace_libs = @libraries.select {|e| e.namespace(e.indexed_namespace) }
|
80
|
+
namespace_libs.each {|lib|
|
81
|
+
(lib_commands[lib.name] || []).each {|e| e.namespace = lib.namespace }
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
def namespaces
|
86
|
+
nsps = @libraries.map {|e| e.namespace }.compact
|
87
|
+
nsps.delete(false)
|
88
|
+
nsps
|
89
|
+
end
|
90
|
+
|
91
|
+
def all_main_methods
|
92
|
+
@commands.reject {|e| e.namespace }.map {|e| [e.name, e.alias]}.flatten.compact + namespaces
|
93
|
+
end
|
94
|
+
|
95
|
+
def marshal_file
|
96
|
+
File.join(repo.config_dir, 'index.marshal')
|
97
|
+
end
|
98
|
+
|
99
|
+
def find_library(command)
|
100
|
+
read
|
101
|
+
namespace_command = command.split('.')[0]
|
102
|
+
if (lib = @libraries.find {|e| e.namespace == namespace_command })
|
103
|
+
lib.name
|
104
|
+
elsif (cmd = Command.find(command, @commands))
|
105
|
+
cmd.lib
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def changed_libraries
|
110
|
+
read
|
111
|
+
latest_hashes.select {|lib, hash| @lib_hashes[lib] != hash}.map {|e| e[0]}
|
112
|
+
end
|
113
|
+
|
114
|
+
def latest_hashes
|
115
|
+
repo.all_libraries.inject({}) {|h, e|
|
116
|
+
lib_file = FileLibrary.library_file(e, repo.dir)
|
117
|
+
h[e] = Digest::MD5.hexdigest(File.read(lib_file)) if File.exists?(lib_file)
|
118
|
+
h
|
119
|
+
}
|
120
|
+
end
|
121
|
+
#:startdoc:
|
122
|
+
end
|
123
|
+
end
|
data/lib/boson/runner.rb
CHANGED
@@ -11,18 +11,17 @@ module Boson
|
|
11
11
|
|
12
12
|
# Libraries that come with Boson
|
13
13
|
def default_libraries
|
14
|
-
[Boson::Commands::Core, Boson::Commands::WebCore] +
|
14
|
+
[Boson::Commands::Core, Boson::Commands::WebCore] + Boson.repos.map {|e| e.config[:defaults] || [] }.flatten
|
15
15
|
end
|
16
16
|
|
17
17
|
# Libraries detected in repositories
|
18
18
|
def detected_libraries
|
19
|
-
Boson.repos.map {|
|
20
|
-
map {|e| e.gsub(/^#{repo.commands_dir}\/|\.rb$/, '')} }.flatten
|
19
|
+
Boson.repos.map {|e| e.detected_libraries }.flatten.uniq
|
21
20
|
end
|
22
21
|
|
23
22
|
# Libraries specified in config files and detected_libraries
|
24
23
|
def all_libraries
|
25
|
-
|
24
|
+
Boson.repos.map {|e| e.all_libraries }.flatten.uniq
|
26
25
|
end
|
27
26
|
|
28
27
|
#:stopdoc:
|
@@ -2,7 +2,7 @@ module Boson
|
|
2
2
|
# This class handles the boson executable (boson command execution from the commandline). Any changes
|
3
3
|
# to your commands are immediately available from the commandline except for changes to the main config file.
|
4
4
|
# For those changes to take effect you need to explicitly load and index the libraries with --index.
|
5
|
-
# See
|
5
|
+
# See RepoIndex to understand how Boson can immediately detect the latest commands.
|
6
6
|
#
|
7
7
|
# Usage for the boson shell command looks like this:
|
8
8
|
# boson [GLOBAL OPTIONS] [COMMAND] [ARGS] [COMMAND OPTIONS]
|
@@ -20,9 +20,22 @@ module Boson
|
|
20
20
|
# [:index] Updates index for given libraries allowing you to use them. This is useful if Boson's autodetection of
|
21
21
|
# changed libraries isn't picking up your changes. Since this option has a :bool_default attribute, arguments
|
22
22
|
# passed to this option need to be passed with '=' i.e. '--index=my_lib'.
|
23
|
-
# [:render]
|
23
|
+
# [:render] Toggles the auto-rendering done for commands that don't have views. Doesn't affect commands that already have views.
|
24
|
+
# Default is false. Also see Auto Rendering section below.
|
24
25
|
# [:pager_toggle] Toggles Hirb's pager in case you'd like to pipe to another command.
|
26
|
+
#
|
27
|
+
# ==== Auto Rendering
|
28
|
+
# Commands that don't have views (defined via render_options) have their return value auto-rendered as a view as follows:
|
29
|
+
# * nil,false and true aren't rendered
|
30
|
+
# * arrays are rendered with Hirb's tables
|
31
|
+
# * non-arrays are printed with inspect()
|
32
|
+
# * Any of these cases can be toggled to render/not render with the global option :render
|
33
|
+
# To turn off auto-rendering by default, add a :no_auto_render: true entry to the main config.
|
25
34
|
class BinRunner < Runner
|
35
|
+
def self.all_libraries #:nodoc:
|
36
|
+
@all_libraries ||= ((libs = super) + libs.map {|e| File.basename(e) }).uniq
|
37
|
+
end
|
38
|
+
|
26
39
|
GLOBAL_OPTIONS = {
|
27
40
|
:verbose=>{:type=>:boolean, :desc=>"Verbose description of loading libraries or help"},
|
28
41
|
:index=>{:type=>:array, :desc=>"Libraries to index. Libraries must be passed with '='.",
|
@@ -30,7 +43,8 @@ module Boson
|
|
30
43
|
:execute=>{:type=>:string, :desc=>"Executes given arguments as a one line script"},
|
31
44
|
:console=>{:type=>:boolean, :desc=>"Drops into irb with default and explicit libraries loaded"},
|
32
45
|
:help=>{:type=>:boolean, :desc=>"Displays this help message or a command's help if given a command"},
|
33
|
-
:load=>{:type=>:array, :values=>all_libraries,
|
46
|
+
:load=>{:type=>:array, :values=>all_libraries,
|
47
|
+
:enum=>false, :desc=>"A comma delimited array of libraries to load"},
|
34
48
|
:render=>{:type=>:boolean, :desc=>"Renders a Hirb view from result of command without options"},
|
35
49
|
:pager_toggle=>{:type=>:boolean, :desc=>"Toggles Hirb's pager"}
|
36
50
|
} #:nodoc:
|
@@ -87,7 +101,7 @@ module Boson
|
|
87
101
|
end
|
88
102
|
|
89
103
|
def default_libraries
|
90
|
-
super +
|
104
|
+
super + Boson.repos.map {|e| e.config[:bin_defaults] || [] }.flatten + Dir.glob('Bosonfile')
|
91
105
|
end
|
92
106
|
|
93
107
|
def execute_command
|
@@ -107,10 +121,11 @@ module Boson
|
|
107
121
|
end
|
108
122
|
|
109
123
|
def render_output(output)
|
110
|
-
if
|
111
|
-
|
112
|
-
|
113
|
-
|
124
|
+
if (!Scientist.rendered && !View.silent_object?(output)) ^ @options[:render] ^
|
125
|
+
Boson.repo.config[:no_auto_render]
|
126
|
+
opts = output.is_a?(String) ? {:method=>'puts'} :
|
127
|
+
{:inspect=>!output.is_a?(Array) || (Scientist.global_options || {})[:render] }
|
128
|
+
View.render output, opts
|
114
129
|
end
|
115
130
|
end
|
116
131
|
|
@@ -5,8 +5,7 @@ module Boson
|
|
5
5
|
class ConsoleRunner < Runner
|
6
6
|
class <<self
|
7
7
|
# Starts Boson by loading configured libraries. If no default libraries are specified in the config,
|
8
|
-
# it will load up all detected libraries.
|
9
|
-
# ==== Options
|
8
|
+
# it will load up all detected libraries. Options:
|
10
9
|
# [:libraries] Array of libraries to load.
|
11
10
|
# [:verbose] Boolean to be verbose about libraries loading. Default is true.
|
12
11
|
# [:no_defaults] Boolean which turns off loading any default libraries. Default is false.
|
data/lib/boson/scientist.rb
CHANGED
@@ -1,109 +1,54 @@
|
|
1
|
-
require 'shellwords'
|
2
1
|
module Boson
|
3
|
-
# Scientist redefines
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
2
|
+
# Scientist wraps around and redefines an object's method to give it the following features:
|
3
|
+
# * Methods can take shell command input with options or receive its normal arguments. See the Commandification
|
4
|
+
# section.
|
5
|
+
# * Methods have a slew of global options available. See OptionCommand for an explanation of basic global options.
|
6
|
+
# * Before a method returns its value, it pipes its return value through pipe commands if pipe options are specified. See Pipe.
|
7
|
+
# * Methods can have any number of optional views associated with them via global render options (see View). Views can be toggled
|
8
|
+
# on/off with the global option --render (see OptionCommand).
|
9
|
+
#
|
10
|
+
# The main methods Scientist provides are redefine_command() for redefining an object's method with a Command object
|
11
|
+
# and commandify() for redefining with a hash of method attributes. Note that for an object's method to be redefined correctly,
|
7
12
|
# its last argument _must_ expect a hash.
|
8
13
|
#
|
9
|
-
# ===
|
14
|
+
# === Commandification
|
10
15
|
# Take for example this basic method/command with an options definition:
|
11
16
|
# options :level=>:numeric, :verbose=>:boolean
|
12
|
-
# def foo(
|
13
|
-
#
|
17
|
+
# def foo(*args)
|
18
|
+
# args
|
14
19
|
# end
|
15
20
|
#
|
16
|
-
# When Scientist wraps around foo(),
|
17
|
-
# foo '', :verbose=>true # normal call
|
18
|
-
# foo '-v' # commandline call
|
19
|
-
#
|
20
|
-
# Both calls return: ['', {:verbose=>true}]
|
21
|
-
#
|
22
|
-
# Non-string arguments can be passed in:
|
23
|
-
# foo Object, :level=>1
|
24
|
-
# foo Object, 'l1'
|
21
|
+
# When Scientist wraps around foo(), it can take arguments normally or as a shell command:
|
22
|
+
# foo 'one', 'two', :verbose=>true # normal call
|
23
|
+
# foo 'one two -v' # commandline call
|
25
24
|
#
|
26
|
-
# Both calls return: [
|
25
|
+
# Both calls return: ['one', 'two', {:verbose=>true}]
|
27
26
|
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
# When using global options along with command options, global options _must_ precede command options.
|
32
|
-
# Take for example using the global --pretend option with the method above:
|
33
|
-
# irb>> foo '-p -l=1'
|
34
|
-
# Arguments: ["", {:level=>1}]
|
35
|
-
# Global options: {:pretend=>true}
|
27
|
+
# Non-string arguments can be passed as well:
|
28
|
+
# foo Object, 'two', :level=>1
|
29
|
+
# foo Object, 'two -l1'
|
36
30
|
#
|
37
|
-
#
|
38
|
-
# this by passing a --global option which takes a string of options without their dashes. For example:
|
39
|
-
# foo '-p --fields=f1,f2 -l=1'
|
40
|
-
# # is the same as
|
41
|
-
# foo ' -g "p fields=f1,f2" -l=1 '
|
42
|
-
#
|
43
|
-
# === Rendering Views With Global Options
|
44
|
-
# Perhaps the most important global option is --render. This option toggles the rendering of your command's output
|
45
|
-
# with Hirb[http://github.com/cldwalker/hirb]. Since Hirb can be customized to generate any view, this option allows
|
46
|
-
# you toggle a predefined view for a command without embedding view code in your command!
|
47
|
-
#
|
48
|
-
# Here's a simple example, toggling Hirb's table view:
|
49
|
-
# # Defined in a library file:
|
50
|
-
# #@options {}
|
51
|
-
# def list(options={})
|
52
|
-
# [1,2,3]
|
53
|
-
# end
|
54
|
-
#
|
55
|
-
# Using it in irb:
|
56
|
-
# >> list
|
57
|
-
# => [1,2,3]
|
58
|
-
# >> list '-r'
|
59
|
-
# +-------+
|
60
|
-
# | value |
|
61
|
-
# +-------+
|
62
|
-
# | 1 |
|
63
|
-
# | 2 |
|
64
|
-
# | 3 |
|
65
|
-
# +-------+
|
66
|
-
# 3 rows in set
|
67
|
-
# => true
|
68
|
-
#
|
69
|
-
# To default to rendering a view for a command, add a render_options {method attribute}[link:classes/Boson/MethodInspector.html]
|
70
|
-
# above list() along with any options you want to pass to your Hirb helper class. In this case, using '-r' gives you the
|
71
|
-
# command's returned object instead of a formatted view!
|
31
|
+
# Both calls return: [Object, 'two', {:level=>1}]
|
72
32
|
module Scientist
|
73
33
|
extend self
|
74
34
|
# Handles all Scientist errors.
|
75
35
|
class Error < StandardError; end
|
76
36
|
class EscapeGlobalOption < StandardError; end
|
77
37
|
|
78
|
-
|
38
|
+
attr_accessor :global_options, :rendered
|
79
39
|
@no_option_commands ||= []
|
80
|
-
|
81
|
-
:help=>{:type=>:boolean, :desc=>"Display a command's help"},
|
82
|
-
:render=>{:type=>:boolean, :desc=>"Toggle a command's default rendering behavior"},
|
83
|
-
:verbose=>{:type=>:boolean, :desc=>"Increase verbosity for help, errors, etc."},
|
84
|
-
:global=>{:type=>:string, :desc=>"Pass a string of global options without the dashes"},
|
85
|
-
:pretend=>{:type=>:boolean, :desc=>"Display what a command would execute without executing it"},
|
86
|
-
} #:nodoc:
|
87
|
-
RENDER_OPTIONS = {
|
88
|
-
:fields=>{:type=>:array, :desc=>"Displays fields in the order given"},
|
89
|
-
:class=>{:type=>:string, :desc=>"Hirb helper class which renders"},
|
90
|
-
:max_width=>{:type=>:numeric, :desc=>"Max width of a table"},
|
91
|
-
:vertical=>{:type=>:boolean, :desc=>"Display a vertical table"},
|
92
|
-
:sort=>{:type=>:string, :desc=>"Sort by given field"},
|
93
|
-
:reverse_sort=>{:type=>:boolean, :desc=>"Reverse a given sort"},
|
94
|
-
:query=>{:type=>:hash, :desc=>"Queries fields given field:search pairs"},
|
95
|
-
} #:nodoc:
|
40
|
+
@option_commands ||= {}
|
96
41
|
|
97
42
|
# Redefines an object's method with a Command of the same name.
|
98
|
-
def
|
99
|
-
cmd_block =
|
43
|
+
def redefine_command(obj, command)
|
44
|
+
cmd_block = redefine_command_block(obj, command)
|
100
45
|
@no_option_commands << command if command.options.nil?
|
101
46
|
[command.name, command.alias].compact.each {|e|
|
102
47
|
obj.instance_eval("class<<self;self;end").send(:define_method, e, cmd_block)
|
103
48
|
}
|
104
49
|
end
|
105
50
|
|
106
|
-
# A wrapper around
|
51
|
+
# A wrapper around redefine_command that doesn't depend on a Command object. Rather you
|
107
52
|
# simply pass a hash of command attributes (see Command.new) or command methods and let OpenStruct mock a command.
|
108
53
|
# The only required attribute is :name, though to get any real use you should define :options and
|
109
54
|
# :arg_size (default is '*'). Example:
|
@@ -123,19 +68,23 @@ module Boson
|
|
123
68
|
hash[:has_splat_args?] = true if hash[:arg_size] == '*'
|
124
69
|
fake_cmd = OpenStruct.new(hash)
|
125
70
|
fake_cmd.option_parser ||= OptionParser.new(fake_cmd.options || {})
|
126
|
-
|
71
|
+
redefine_command(obj, fake_cmd)
|
127
72
|
end
|
128
73
|
|
129
|
-
# The actual method which
|
130
|
-
def
|
74
|
+
# The actual method which redefines a command's original method
|
75
|
+
def redefine_command_block(obj, command)
|
131
76
|
lambda {|*args|
|
132
77
|
Boson::Scientist.translate_and_render(obj, command, args) {|args| super(*args) }
|
133
78
|
}
|
134
79
|
end
|
135
80
|
|
136
81
|
#:stopdoc:
|
82
|
+
def option_command(cmd=@command)
|
83
|
+
@option_commands[cmd] ||= OptionCommand.new(cmd)
|
84
|
+
end
|
85
|
+
|
137
86
|
def translate_and_render(obj, command, args)
|
138
|
-
@global_options = {}
|
87
|
+
@global_options, @command = {}, command
|
139
88
|
args = translate_args(obj, command, args)
|
140
89
|
if @global_options[:verbose] || @global_options[:pretend]
|
141
90
|
puts "Arguments: #{args.inspect}", "Global options: #{@global_options.inspect}"
|
@@ -149,24 +98,16 @@ module Boson
|
|
149
98
|
end
|
150
99
|
|
151
100
|
def translate_args(obj, command, args)
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
add_default_args(@args)
|
161
|
-
return @args if @no_option_commands.include?(@command)
|
162
|
-
@args << parsed_options
|
163
|
-
if @args.size != command.arg_size && !command.has_splat_args?
|
164
|
-
command_size, args_size = @args.size > command.arg_size ? [command.arg_size, @args.size] :
|
165
|
-
[command.arg_size - 1, @args.size - 1]
|
166
|
-
raise ArgumentError, "wrong number of arguments (#{args_size} for #{command_size})"
|
167
|
-
end
|
101
|
+
option_command.prepend_default_option(args)
|
102
|
+
@global_options, parsed_options, args = option_command.parse(args)
|
103
|
+
raise EscapeGlobalOption if @global_options[:help]
|
104
|
+
if parsed_options
|
105
|
+
option_command.add_default_args(args, obj)
|
106
|
+
return args if @no_option_commands.include?(command)
|
107
|
+
args << parsed_options
|
108
|
+
option_command.check_argument_size(args)
|
168
109
|
end
|
169
|
-
|
110
|
+
args
|
170
111
|
rescue Error, ArgumentError, EscapeGlobalOption
|
171
112
|
raise
|
172
113
|
rescue Exception
|
@@ -176,137 +117,19 @@ module Boson
|
|
176
117
|
|
177
118
|
def render_or_raw(result)
|
178
119
|
if (@rendered = render?)
|
179
|
-
result =
|
180
|
-
|
181
|
-
View.render(result,
|
120
|
+
result = Pipe.process(result, @global_options) if @global_options.key?(:class) ||
|
121
|
+
@global_options.key?(:method)
|
122
|
+
View.render(result, OptionCommand.delete_non_render_options(@global_options.dup), false)
|
182
123
|
else
|
183
|
-
|
184
|
-
run_pipe_commands(result)
|
124
|
+
Pipe.process(result, @global_options)
|
185
125
|
end
|
186
126
|
rescue Exception
|
187
127
|
message = @global_options[:verbose] ? "#{$!}\n#{$!.backtrace.inspect}" : $!.message
|
188
128
|
raise Error, message
|
189
129
|
end
|
190
130
|
|
191
|
-
def pipe_options
|
192
|
-
@pipe_options ||= Hash[*default_global_options.select {|k,v| v[:pipe] }.flatten]
|
193
|
-
end
|
194
|
-
|
195
|
-
def run_pipe_commands(result)
|
196
|
-
(global_options.keys & pipe_options.keys).each {|e|
|
197
|
-
command = pipe_options[e][:pipe] != true ? pipe_options[e][:pipe] : e
|
198
|
-
pipe_result = pipe_options[e][:type] == :boolean ? Boson.invoke(command, result) :
|
199
|
-
Boson.invoke(command, result, global_options[e])
|
200
|
-
result = pipe_result if pipe_options[e][:filter]
|
201
|
-
}
|
202
|
-
result
|
203
|
-
end
|
204
|
-
|
205
|
-
# choose current parser
|
206
|
-
def option_parser
|
207
|
-
@command.render_options ? command_option_parser : default_option_parser
|
208
|
-
end
|
209
|
-
|
210
|
-
# current command parser
|
211
|
-
def command_option_parser
|
212
|
-
(@option_parsers ||= {})[@command] ||= OptionParser.new current_command_options
|
213
|
-
end
|
214
|
-
|
215
|
-
# set cmd and use its parser
|
216
|
-
def render_option_parser(cmd)
|
217
|
-
@command = cmd
|
218
|
-
option_parser
|
219
|
-
end
|
220
|
-
|
221
|
-
def default_option_parser
|
222
|
-
@default_option_parser ||= OptionParser.new default_render_options.merge(default_global_options)
|
223
|
-
end
|
224
|
-
|
225
|
-
def default_global_options
|
226
|
-
@default_global_options ||= GLOBAL_OPTIONS.merge Boson.repo.config[:global_options] || {}
|
227
|
-
end
|
228
|
-
|
229
|
-
def default_render_options
|
230
|
-
@default_render_options ||= RENDER_OPTIONS.merge Boson.repo.config[:render_options] || {}
|
231
|
-
end
|
232
|
-
|
233
|
-
def current_command_options
|
234
|
-
(@command_options ||= {})[@command] ||= begin
|
235
|
-
@command.render_options.each {|k,v|
|
236
|
-
if !v.is_a?(Hash) && !v.is_a?(Symbol)
|
237
|
-
@command.render_options[k] = {:default=>v}
|
238
|
-
end
|
239
|
-
}
|
240
|
-
render_opts = Util.recursive_hash_merge(@command.render_options, Util.deep_copy(default_render_options))
|
241
|
-
opts = Util.recursive_hash_merge render_opts, Util.deep_copy(default_global_options)
|
242
|
-
if !opts[:fields].key?(:values)
|
243
|
-
if opts[:fields][:default]
|
244
|
-
opts[:fields][:values] = opts[:fields][:default]
|
245
|
-
else
|
246
|
-
opts[:fields][:values] = opts[:change_fields][:default].values if opts[:change_fields] && opts[:change_fields][:default]
|
247
|
-
opts[:fields][:values] ||= opts[:headers][:default].keys if opts[:headers] && opts[:headers][:default]
|
248
|
-
end
|
249
|
-
opts[:fields][:enum] = false if opts[:fields][:values] && !opts[:fields].key?(:enum)
|
250
|
-
end
|
251
|
-
if opts[:fields][:values]
|
252
|
-
opts[:sort][:values] ||= opts[:fields][:values]
|
253
|
-
opts[:query][:keys] ||= opts[:fields][:values]
|
254
|
-
opts[:query][:default_keys] ||= "*"
|
255
|
-
end
|
256
|
-
opts
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
131
|
def render?
|
261
|
-
|
262
|
-
end
|
263
|
-
|
264
|
-
def parse_command_options
|
265
|
-
if @args.size == 1 && @args[0].is_a?(String)
|
266
|
-
parsed_options, @args = parse_options Shellwords.shellwords(@args[0])
|
267
|
-
# last string argument interpreted as args + options
|
268
|
-
elsif @args.size > 1 && @args[-1].is_a?(String)
|
269
|
-
args = Boson.const_defined?(:BinRunner) ? @args : Shellwords.shellwords(@args.pop)
|
270
|
-
parsed_options, new_args = parse_options args
|
271
|
-
@args += new_args
|
272
|
-
# add default options
|
273
|
-
elsif @command.options.empty? || (!@command.has_splat_args? &&
|
274
|
-
@args.size <= (@command.arg_size - 1).abs) || (@command.has_splat_args? && !@args[-1].is_a?(Hash))
|
275
|
-
parsed_options = parse_options([])[0]
|
276
|
-
# merge default options with given hash of options
|
277
|
-
elsif (@command.has_splat_args? || (@args.size == @command.arg_size)) && @args[-1].is_a?(Hash)
|
278
|
-
parsed_options = parse_options([])[0]
|
279
|
-
parsed_options.merge!(@args.pop)
|
280
|
-
end
|
281
|
-
parsed_options
|
282
|
-
end
|
283
|
-
|
284
|
-
def parse_options(args)
|
285
|
-
parsed_options = @command.option_parser.parse(args, :delete_invalid_opts=>true)
|
286
|
-
@global_options = option_parser.parse @command.option_parser.leading_non_opts
|
287
|
-
new_args = option_parser.non_opts.dup + @command.option_parser.trailing_non_opts
|
288
|
-
if @global_options[:global]
|
289
|
-
global_opts = Shellwords.shellwords(@global_options[:global]).map {|str|
|
290
|
-
((str[/^(.*?)=/,1] || str).length > 1 ? "--" : "-") + str }
|
291
|
-
@global_options.merge! option_parser.parse(global_opts)
|
292
|
-
end
|
293
|
-
raise EscapeGlobalOption if @global_options[:help]
|
294
|
-
[parsed_options, new_args]
|
295
|
-
end
|
296
|
-
|
297
|
-
def add_default_args(args)
|
298
|
-
if @command.args && args.size < @command.args.size - 1
|
299
|
-
# leave off last arg since its an option
|
300
|
-
@command.args.slice(0..-2).each_with_index {|arr,i|
|
301
|
-
next if args.size >= i + 1 # only fill in once args run out
|
302
|
-
break if arr.size != 2 # a default arg value must exist
|
303
|
-
begin
|
304
|
-
args[i] = @command.file_parsed_args? ? @obj.instance_eval(arr[1]) : arr[1]
|
305
|
-
rescue Exception
|
306
|
-
raise Error, "Unable to set default argument at position #{i+1}.\nReason: #{$!.message}"
|
307
|
-
end
|
308
|
-
}
|
309
|
-
end
|
132
|
+
!!@command.render_options ^ @global_options[:render]
|
310
133
|
end
|
311
134
|
#:startdoc:
|
312
135
|
end
|