boson 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|