boson-more 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gemspec +22 -0
- data/LICENSE.txt +22 -0
- data/README.md +97 -0
- data/Rakefile +35 -0
- data/deps.rip +1 -0
- data/lib/boson/alias.rb +75 -0
- data/lib/boson/argument_inspector.rb +90 -0
- data/lib/boson/commands/core.rb +67 -0
- data/lib/boson/commands/view_core.rb +19 -0
- data/lib/boson/commands/web_core.rb +153 -0
- data/lib/boson/comment_inspector.rb +100 -0
- data/lib/boson/console.rb +40 -0
- data/lib/boson/console_runner.rb +60 -0
- data/lib/boson/index.rb +48 -0
- data/lib/boson/libraries/file_library.rb +144 -0
- data/lib/boson/libraries/gem_library.rb +30 -0
- data/lib/boson/libraries/local_file_library.rb +30 -0
- data/lib/boson/libraries/module_library.rb +37 -0
- data/lib/boson/libraries/require_library.rb +23 -0
- data/lib/boson/libraries.rb +183 -0
- data/lib/boson/more/version.rb +5 -0
- data/lib/boson/more.rb +18 -0
- data/lib/boson/more_commands.rb +14 -0
- data/lib/boson/more_inspector.rb +42 -0
- data/lib/boson/more_manager.rb +34 -0
- data/lib/boson/more_method_inspector.rb +74 -0
- data/lib/boson/more_option_parser.rb +28 -0
- data/lib/boson/more_scientist.rb +68 -0
- data/lib/boson/more_util.rb +30 -0
- data/lib/boson/namespace.rb +31 -0
- data/lib/boson/namespacer.rb +117 -0
- data/lib/boson/pipe.rb +156 -0
- data/lib/boson/pipe_runner.rb +44 -0
- data/lib/boson/pipes.rb +75 -0
- data/lib/boson/repo.rb +96 -0
- data/lib/boson/repo_index.rb +135 -0
- data/lib/boson/runner_options.rb +88 -0
- data/lib/boson/save.rb +198 -0
- data/lib/boson/science.rb +273 -0
- data/lib/boson/view.rb +98 -0
- data/lib/boson/viewable.rb +48 -0
- data/test/alias_test.rb +55 -0
- data/test/argument_inspector_test.rb +40 -0
- data/test/command_test.rb +22 -0
- data/test/commands_test.rb +53 -0
- data/test/comment_inspector_test.rb +126 -0
- data/test/console_runner_test.rb +58 -0
- data/test/deps.rip +4 -0
- data/test/file_library_test.rb +41 -0
- data/test/gem_library_test.rb +40 -0
- data/test/libraries_test.rb +55 -0
- data/test/loader_test.rb +38 -0
- data/test/module_library_test.rb +30 -0
- data/test/more_manager_test.rb +29 -0
- data/test/more_method_inspector_test.rb +42 -0
- data/test/more_scientist_test.rb +10 -0
- data/test/namespacer_test.rb +61 -0
- data/test/pipes_test.rb +65 -0
- data/test/repo_index_test.rb +123 -0
- data/test/repo_test.rb +23 -0
- data/test/runner_options_test.rb +29 -0
- data/test/save_test.rb +86 -0
- data/test/science_test.rb +58 -0
- data/test/scientist_test.rb +195 -0
- data/test/test_helper.rb +165 -0
- data/test/web_test.rb +22 -0
- metadata +169 -0
data/lib/boson/pipe.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
module Boson
|
2
|
+
# This module passes an original 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 Pipes.query_pipe.
|
5
|
+
# * A :sort option sorts an array of objects or hashes using Pipes.sort_pipe.
|
6
|
+
# * A :reverse_sort pipe option reverses an array.
|
7
|
+
# * A :pipes option takes an array of commands that modify the return value using Pipes.pipes_pipe.
|
8
|
+
# * All user-defined pipe options (:pipe_options key in Repo.config) are processed in random order.
|
9
|
+
#
|
10
|
+
# Some points:
|
11
|
+
# * User-defined pipes call a command (the option's name by default). It's the user's responsibility to have this
|
12
|
+
# command loaded when used. The easiest way to do this is by adding the pipe command's library to :defaults in main config.
|
13
|
+
# * By default, pipe commands do not modify the value their given. This means you can activate multiple pipes using
|
14
|
+
# a method's original return value.
|
15
|
+
# * A pipe command expects a command's return value as its first argument. If the pipe option takes an argument, it's passed
|
16
|
+
# on as a second argument.
|
17
|
+
# * When piping occurs in relation to rendering depends on the Hirb view. With the default Hirb view, piping occurs
|
18
|
+
# occurs in the middle of the rendering, after Hirb has converted the return value into an array of hashes.
|
19
|
+
# If using a custom Hirb view, piping occurs before rendering.
|
20
|
+
# * What the pipe command should expect as a return value depends on the type of command. If it's a command rendered with hirb's
|
21
|
+
# tables, the return value is a an array of hashes. For everything else, it's the method's original return value.
|
22
|
+
#
|
23
|
+
# === User Pipes
|
24
|
+
# User pipes have the following attributes which alter their behavior:
|
25
|
+
# [*:pipe*] Pipe command the pipe executes when called. Default is the pipe's name.
|
26
|
+
# [*:env*] Boolean which enables passing an additional hash to the pipe command. This hash contains information from the first
|
27
|
+
# command's input with the following keys: :args (command's arguments), :options (command's options),
|
28
|
+
# :global_options (command's global options) and :config (a command's configuration hash). Default is false.
|
29
|
+
# [*:filter*] Boolean which has the pipe command modify the original command's output with the value it returns. Default is false.
|
30
|
+
# [*:no_render*] Boolean to turn off auto-rendering of the original command's final output. Only applicable to :filter enabled
|
31
|
+
# pipes. Default is false.
|
32
|
+
# [*:solo*] Boolean to indicate this pipe can't run with other user pipes or pipes from :pipes option.
|
33
|
+
# If a user calls multiple solo pipes, only the first one detected is called.
|
34
|
+
#
|
35
|
+
# == Repo Config
|
36
|
+
# This class adds the following key to the main repo config:
|
37
|
+
#
|
38
|
+
# [:pipe_options] Hash of options available to all option commands for piping (see Pipe). A pipe option has the
|
39
|
+
# {normal option attributes}[link:classes/Boson/OptionParser.html#M000081] and these:
|
40
|
+
# * :pipe: Specifies the command to call when piping. Defaults to the pipe's option name.
|
41
|
+
# * :filter: Boolean which indicates that the pipe command will modify its input with what it returns.
|
42
|
+
# Default is false.
|
43
|
+
#
|
44
|
+
# === User Pipes Example
|
45
|
+
# Let's say you want to have two commands, browser and copy, you want to make available as pipe options:
|
46
|
+
# # Opens url in browser. This command already ships with Boson.
|
47
|
+
# def browser(url)
|
48
|
+
# system('open', url)
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# # Copy to clipboard
|
52
|
+
# def copy(str)
|
53
|
+
# IO.popen('pbcopy', 'w+') {|clipboard| clipboard.write(str)}
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# To configure them, drop the following config in ~/.boson/config/boson.yml:
|
57
|
+
# :pipe_options:
|
58
|
+
# :browser:
|
59
|
+
# :type: :boolean
|
60
|
+
# :desc: Open in browser
|
61
|
+
# :copy:
|
62
|
+
# :type: :boolean
|
63
|
+
# :desc: Copy to clipboard
|
64
|
+
#
|
65
|
+
# Now for any command that returns a url string, these pipe options can be turned on to execute the url.
|
66
|
+
#
|
67
|
+
# Some examples of these options using commands from {my libraries}[http://github.com/cldwalker/irbfiles]:
|
68
|
+
# # Creates a gist and then opens url in browser and copies it.
|
69
|
+
# $ cat some_file | boson gist -bC # or cat some_file | boson gist --browser --copy
|
70
|
+
#
|
71
|
+
# # Generates rdoc in current directory and then opens it in browser
|
72
|
+
# irb>> rdoc '-b' # or rdoc '--browser'
|
73
|
+
module Pipe
|
74
|
+
extend self
|
75
|
+
|
76
|
+
# Process pipes for Scientist
|
77
|
+
def scientist_process(object, global_opt, env={})
|
78
|
+
@env = env
|
79
|
+
[:query, :sort, :reverse_sort].each {|e| global_opt.delete(e) } unless object.is_a?(Array)
|
80
|
+
process_pipes(object, global_opt)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Main method which processes all pipe commands, both default and user-defined ones.
|
84
|
+
def process_pipes(obj, options)
|
85
|
+
internal_pipes(options).each {|pipe|
|
86
|
+
obj = Pipes.send("#{pipe}_pipe", obj, options[pipe]) if options[pipe]
|
87
|
+
}
|
88
|
+
process_user_pipes(obj, options)
|
89
|
+
end
|
90
|
+
|
91
|
+
# A hash that defines user pipes in the same way as the :pipe_options key in Repo.config.
|
92
|
+
# This method should be called when a pipe's library is loading.
|
93
|
+
def add_pipes(hash)
|
94
|
+
pipe_options.merge! setup_pipes(hash)
|
95
|
+
end
|
96
|
+
|
97
|
+
#:stopdoc:
|
98
|
+
def internal_pipes(global_opt)
|
99
|
+
internals = [:query, :sort, :reverse_sort, :pipes]
|
100
|
+
internals.delete(:pipes) if pipes_to_process(global_opt).any? {|e| pipe(e)[:solo] }
|
101
|
+
internals
|
102
|
+
end
|
103
|
+
|
104
|
+
def pipe_options
|
105
|
+
@pipe_options ||= setup_pipes(Boson.repo.config[:pipe_options] || {})
|
106
|
+
end
|
107
|
+
|
108
|
+
def setup_pipes(hash)
|
109
|
+
hash.each {|k,v| v[:pipe] ||= k }
|
110
|
+
end
|
111
|
+
|
112
|
+
def pipe(key)
|
113
|
+
pipe_options[key] || {}
|
114
|
+
end
|
115
|
+
|
116
|
+
# global_opt can come from Hirb callback or Scientist
|
117
|
+
def process_user_pipes(result, global_opt)
|
118
|
+
pipes_to_process(global_opt).each {|e|
|
119
|
+
args = [pipe(e)[:pipe], result]
|
120
|
+
args << global_opt[e] unless pipe(e)[:type] == :boolean
|
121
|
+
args << get_env(e, global_opt) if pipe(e)[:env]
|
122
|
+
pipe_result = Boson.invoke(*args)
|
123
|
+
result = pipe_result if pipe(e)[:filter]
|
124
|
+
}
|
125
|
+
result
|
126
|
+
end
|
127
|
+
|
128
|
+
def get_env(key, global_opt)
|
129
|
+
{ :global_options=>global_opt.merge(:delete_callbacks=>[:z_boson_pipes]),
|
130
|
+
:config=>(@env[:config].dup[key] || {}),
|
131
|
+
:args=>@env[:args],
|
132
|
+
:options=>@env[:options] || {}
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
def any_no_render_pipes?(global_opt)
|
137
|
+
!(pipes = pipes_to_process(global_opt)).empty? &&
|
138
|
+
pipes.any? {|e| pipe(e)[:no_render] }
|
139
|
+
end
|
140
|
+
|
141
|
+
def pipes_to_process(global_opt)
|
142
|
+
pipes = (global_opt.keys & pipe_options.keys)
|
143
|
+
(solo_pipe = pipes.find {|e| pipe(e)[:solo] }) ? [solo_pipe] : pipes
|
144
|
+
end
|
145
|
+
#:startdoc:
|
146
|
+
|
147
|
+
# Callbacks used by Hirb::Helpers::Table to search,sort and run custom pipe commands on arrays of hashes.
|
148
|
+
module TableCallbacks
|
149
|
+
# Processes boson's pipes
|
150
|
+
def z_boson_pipes_callback(obj, options)
|
151
|
+
Pipe.process_pipes(obj, options)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
Hirb::Helpers::Table.send :include, Boson::Pipe::TableCallbacks
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Boson
|
2
|
+
module PipeRunner
|
3
|
+
PIPE = '+'
|
4
|
+
|
5
|
+
# Splits array into array of arrays with given element
|
6
|
+
def self.split_array_by(arr, divider)
|
7
|
+
arr.inject([[]]) {|results, element|
|
8
|
+
(divider == element) ? (results << []) : (results.last << element)
|
9
|
+
results
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse_args(args)
|
14
|
+
@all_args = PipeRunner.split_array_by(args, PIPE)
|
15
|
+
args = @all_args[0]
|
16
|
+
super(args).tap do |result|
|
17
|
+
@all_args[0] = ([result[0]] + Array(result[2])).compact
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def execute_command(command, args)
|
22
|
+
@all_args.inject(nil) do |acc, (cmd,*args)|
|
23
|
+
args = translate_args(args, acc)
|
24
|
+
super(cmd, args)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def translate_args(args, piped)
|
29
|
+
args.unshift piped if piped
|
30
|
+
args
|
31
|
+
end
|
32
|
+
|
33
|
+
# Commands to executed, in order given by user
|
34
|
+
def commands
|
35
|
+
@commands ||= @all_args.map {|e| e[0]}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
if defined? BinRunner
|
40
|
+
class BinRunner < BareRunner
|
41
|
+
class << self; include PipeRunner; end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/boson/pipes.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
module Boson
|
2
|
+
# === Default Pipes: Search and Sort
|
3
|
+
# The default pipe options, :query, :sort and :reverse_sort, are quite useful for searching and sorting arrays:
|
4
|
+
# Some examples using default commands:
|
5
|
+
# # Searches commands in the full_name field for 'lib' and sorts results by that field.
|
6
|
+
# $ boson commands -q=f:lib -s=f # or commands --query=full_name:lib --sort=full_name
|
7
|
+
#
|
8
|
+
# # Multiple fields can be searched if separated by a ','. This searches the full_name and desc fields.
|
9
|
+
# $ boson commands -q=f,d:web # or commands --query=full_name,desc:web
|
10
|
+
#
|
11
|
+
# # All fields can be queried using a '*'.
|
12
|
+
# # Searches all library fields and then reverse sorts on name field
|
13
|
+
# $ boson libraries -q=*:core -s=n -R # or libraries --query=*:core --sort=name --reverse_sort
|
14
|
+
#
|
15
|
+
# # Multiple searches can be joined together by ','
|
16
|
+
# # Searches for libraries that have the name matching core or a library_type matching gem
|
17
|
+
# $ boson libraries -q=n:core,l:gem # or libraries --query=name:core,library_type:gem
|
18
|
+
#
|
19
|
+
# In these examples, we queried commands and examples with an explicit --query. However, -q or --query isn't necessary
|
20
|
+
# for these commands because they already default to it when not present. This behavior comes from the default_option
|
21
|
+
# attribute a command can have.
|
22
|
+
module Pipes
|
23
|
+
extend self
|
24
|
+
|
25
|
+
# Case-insensitive search an array of objects or hashes for the :query option.
|
26
|
+
# This option is a hash of fields mapped to their search terms. Searches are OR-ed.
|
27
|
+
# When searching hashes, numerical string keys in query_hash are converted to actual numbers to
|
28
|
+
# interface with Hirb.
|
29
|
+
def query_pipe(object, query_hash)
|
30
|
+
if object[0].is_a?(Hash)
|
31
|
+
query_hash.map {|field,query|
|
32
|
+
field = field.to_i if field.to_s[/^\d+$/]
|
33
|
+
object.select {|e| e[field].to_s =~ /#{query}/i }
|
34
|
+
}.flatten.uniq
|
35
|
+
else
|
36
|
+
query_hash.map {|field,query| object.select {|e| e.send(field).to_s =~ /#{query}/i } }.flatten.uniq
|
37
|
+
end
|
38
|
+
rescue NoMethodError
|
39
|
+
$stderr.puts "Query failed with nonexistant method '#{$!.message[/`(.*)'/,1]}'"
|
40
|
+
end
|
41
|
+
|
42
|
+
# Sorts an array of objects or hashes using a sort field. Sort is reversed with reverse_sort set to true.
|
43
|
+
def sort_pipe(object, sort)
|
44
|
+
sort_lambda = lambda {}
|
45
|
+
if object[0].is_a?(Hash)
|
46
|
+
if sort.to_s[/^\d+$/]
|
47
|
+
sort = sort.to_i
|
48
|
+
elsif object[0].keys.all? {|e| e.is_a?(Symbol) }
|
49
|
+
sort = sort.to_sym
|
50
|
+
end
|
51
|
+
sort_lambda = untouched_sort?(object.map {|e| e[sort] }) ? lambda {|e| e[sort] } : lambda {|e| e[sort].to_s }
|
52
|
+
else
|
53
|
+
sort_lambda = untouched_sort?(object.map {|e| e.send(sort) }) ? lambda {|e| e.send(sort) || ''} :
|
54
|
+
lambda {|e| e.send(sort).to_s }
|
55
|
+
end
|
56
|
+
object.sort_by &sort_lambda
|
57
|
+
rescue NoMethodError, ArgumentError
|
58
|
+
$stderr.puts "Sort failed with nonexistant method '#{sort}'"
|
59
|
+
end
|
60
|
+
|
61
|
+
def untouched_sort?(values) #:nodoc:
|
62
|
+
values.all? {|e| e.respond_to?(:<=>) } && values.map {|e| e.class }.uniq.size == 1
|
63
|
+
end
|
64
|
+
|
65
|
+
# Reverse an object
|
66
|
+
def reverse_sort_pipe(object, extra=nil)
|
67
|
+
object.reverse
|
68
|
+
end
|
69
|
+
|
70
|
+
# Pipes output of multiple commands recursively, given initial object
|
71
|
+
def pipes_pipe(obj, arr)
|
72
|
+
arr.inject(obj) {|acc,e| Boson.full_invoke(e, [acc]) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/boson/repo.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
%w{yaml fileutils}.each {|e| require e }
|
2
|
+
module Boson
|
3
|
+
# A class for repositories. A repository has a root directory with required subdirectories config/ and
|
4
|
+
# commands/ and optional subdirectory lib/. Each repository has a primary config file at config/boson.yml.
|
5
|
+
class Repo
|
6
|
+
def self.commands_dir(dir) #:nodoc:
|
7
|
+
File.join(dir, 'commands')
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :dir, :config
|
11
|
+
# Creates a repository given a root directory.
|
12
|
+
def initialize(dir)
|
13
|
+
@dir = dir
|
14
|
+
end
|
15
|
+
|
16
|
+
# Points to the config/ subdirectory and is automatically created when called. Used for config files.
|
17
|
+
def config_dir
|
18
|
+
@config_dir ||= FileUtils.mkdir_p(config_dir_path) && config_dir_path
|
19
|
+
end
|
20
|
+
|
21
|
+
def config_dir_path
|
22
|
+
"#{dir}/config"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Path name of main config file. If passed true, parent directory of file is created.
|
26
|
+
def config_file(create_dir=false)
|
27
|
+
File.join((create_dir ? config_dir : config_dir_path), 'boson.yml')
|
28
|
+
end
|
29
|
+
|
30
|
+
# Points to the commands/ subdirectory and is automatically created when called. Used for command libraries.
|
31
|
+
def commands_dir
|
32
|
+
@commands_dir ||= (cdir = self.class.commands_dir(@dir)) && FileUtils.mkdir_p(cdir) && cdir
|
33
|
+
end
|
34
|
+
|
35
|
+
# A hash read from the YAML config file at config/boson.yml.
|
36
|
+
# {See here}[http://github.com/cldwalker/irbfiles/blob/master/boson/config/boson.yml] for an example config file.
|
37
|
+
# Top level config keys, library attributes and config attributes need to be symbols.
|
38
|
+
# ==== Config keys for all repositories:
|
39
|
+
# [:libraries] Hash of libraries mapping their name to attribute hashes. See Library.new for configurable attributes.
|
40
|
+
# Example:
|
41
|
+
# :libraries=>{'completion'=>{:namespace=>true}}
|
42
|
+
# [:command_aliases] Hash of commands names and their aliases. Since this is global it will be read by _all_ libraries.
|
43
|
+
# This is useful for quickly creating aliases without having to worry about placing them under
|
44
|
+
# the correct library config. For non-global aliasing, aliases should be placed under the :command_aliases
|
45
|
+
# key of a library entry in :libraries.
|
46
|
+
# Example:
|
47
|
+
# :command_aliases=>{'libraries'=>'lib', 'commands'=>'com'}
|
48
|
+
# [:defaults] Array of libraries to load at start up for commandline and irb. This is useful for extending boson i.e. adding your
|
49
|
+
# own option types since these are loaded before any other libraries. Default is no libraries.
|
50
|
+
# [:bin_defaults] Array of libraries to load at start up when used from the commandline. Default is no libraries.
|
51
|
+
# [:add_load_path] Boolean specifying whether to add a load path pointing to the lib subdirectory/. This is useful in sharing
|
52
|
+
# classes between libraries without resorting to packaging them as gems. Defaults to false if the lib
|
53
|
+
# subdirectory doesn't exist in the boson directory.
|
54
|
+
#
|
55
|
+
# ==== Config keys specific to the main repo config ~/.boson/config/boson.yml
|
56
|
+
# [:error_method_conflicts] Boolean specifying library loading behavior when its methods conflicts with existing methods in
|
57
|
+
# the global namespace. When set to false, Boson automatically puts the library in its own namespace.
|
58
|
+
# When set to true, the library fails to load explicitly. Default is false.
|
59
|
+
# [:auto_namespace] Boolean which automatically namespaces all user-defined libraries. Be aware this can break libraries which
|
60
|
+
# depend on commands from other libraries. Default is false.
|
61
|
+
# [:ignore_directories] Array of directories to ignore when detecting local repositories for Boson.local_repo.
|
62
|
+
# [:option_underscore_search] When set, OptionParser option values (with :values or :keys) are auto aliased with underscore searching.
|
63
|
+
# Default is true. See Util.underscore_search.
|
64
|
+
def config(reload=false)
|
65
|
+
if reload || @config.nil?
|
66
|
+
begin
|
67
|
+
@config = Boson::CONFIG.dup
|
68
|
+
@config.merge!(YAML::load_file(config_file(true))) if File.exists?(config_file)
|
69
|
+
rescue ArgumentError
|
70
|
+
message = $!.message !~ /syntax error on line (\d+)/ ? "Error"+$!.message :
|
71
|
+
"Error: Syntax error in line #{$1} of config file '#{config_file}'"
|
72
|
+
Kernel.abort message
|
73
|
+
end
|
74
|
+
end
|
75
|
+
@config
|
76
|
+
end
|
77
|
+
|
78
|
+
# Updates main config file by passing config into a block to be modified and then saved
|
79
|
+
def update_config
|
80
|
+
yield(config)
|
81
|
+
write_config_file
|
82
|
+
end
|
83
|
+
|
84
|
+
def write_config_file #:nodoc:
|
85
|
+
File.open(config_file, 'w') {|f| f.write config.to_yaml }
|
86
|
+
end
|
87
|
+
|
88
|
+
def detected_libraries #:nodoc:
|
89
|
+
Dir[File.join(commands_dir, '**/*.rb')].map {|e| e.gsub(/^#{commands_dir}\/|\.rb$/, '') }
|
90
|
+
end
|
91
|
+
|
92
|
+
def all_libraries #:nodoc:
|
93
|
+
(detected_libraries + config[:libraries].keys).uniq
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,135 @@
|
|
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.instance.failed_libraries = []
|
25
|
+
unless libraries_to_update.empty?
|
26
|
+
Manager.load(libraries_to_update, options.merge(:index=>true))
|
27
|
+
unless Manager.instance.failed_libraries.empty?
|
28
|
+
$stderr.puts("Error: These libraries failed to load while indexing: #{Manager.instance.failed_libraries.join(', ')}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
write(Manager.instance.failed_libraries)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Reads and initializes index.
|
35
|
+
def read
|
36
|
+
return if @read
|
37
|
+
@libraries, @commands, @lib_hashes = exists? ?
|
38
|
+
File.open(marshal_file, 'rb') do |f|
|
39
|
+
f.flock(File::LOCK_EX)
|
40
|
+
Marshal.load(f)
|
41
|
+
end : [[], [], {}]
|
42
|
+
delete_stale_libraries_and_commands
|
43
|
+
set_command_namespaces
|
44
|
+
@read = true
|
45
|
+
end
|
46
|
+
|
47
|
+
# Writes/saves current index to config/index.marshal.
|
48
|
+
def write(failed_libraries=[])
|
49
|
+
latest = latest_hashes
|
50
|
+
failed_libraries.each {|e| latest.delete(e) }
|
51
|
+
save_marshal_index Marshal.dump([Boson.libraries, Boson.commands, latest])
|
52
|
+
end
|
53
|
+
|
54
|
+
#:stopdoc:
|
55
|
+
def read_and_transfer(ignored_libraries=[])
|
56
|
+
read
|
57
|
+
existing_libraries = (Boson.libraries.map {|e| e.name} + ignored_libraries).uniq
|
58
|
+
libraries_to_add = @libraries.select {|e| !existing_libraries.include?(e.name)}
|
59
|
+
Boson.libraries += libraries_to_add
|
60
|
+
# depends on saved commands being correctly associated with saved libraries
|
61
|
+
Boson.commands += libraries_to_add.map {|e| e.command_objects(e.commands, @commands) }.flatten
|
62
|
+
end
|
63
|
+
|
64
|
+
def exists?
|
65
|
+
File.exists? marshal_file
|
66
|
+
end
|
67
|
+
|
68
|
+
def save_marshal_index(marshal_string)
|
69
|
+
binmode = defined?(File::BINARY) ? File::BINARY : 0
|
70
|
+
rdwr_access = File::RDWR | File::CREAT | binmode
|
71
|
+
# To protect the file truncing with a lock we cannot use the 'wb' options.
|
72
|
+
# The w option truncates the file before calling the File.open block
|
73
|
+
File.open(marshal_file, rdwr_access) do |f|
|
74
|
+
f.flock(File::LOCK_EX)
|
75
|
+
f.truncate 0
|
76
|
+
f.write(marshal_string)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def delete_stale_libraries_and_commands
|
81
|
+
cached_libraries = @lib_hashes.keys
|
82
|
+
libs_to_delete = @libraries.select {|e| !cached_libraries.include?(e.name) && e.is_a?(FileLibrary) }
|
83
|
+
names_to_delete = libs_to_delete.map {|e| e.name }
|
84
|
+
libs_to_delete.each {|e| @libraries.delete(e) }
|
85
|
+
@commands.delete_if {|e| names_to_delete.include? e.lib }
|
86
|
+
end
|
87
|
+
|
88
|
+
# set namespaces for commands
|
89
|
+
def set_command_namespaces
|
90
|
+
lib_commands = @commands.inject({}) {|t,e| (t[e.lib] ||= []) << e; t }
|
91
|
+
namespace_libs = @libraries.select {|e| e.namespace(e.indexed_namespace) }
|
92
|
+
namespace_libs.each {|lib|
|
93
|
+
(lib_commands[lib.name] || []).each {|e| e.namespace = lib.namespace }
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
def namespaces
|
98
|
+
nsps = @libraries.map {|e| e.namespace }.compact
|
99
|
+
nsps.delete(false)
|
100
|
+
nsps
|
101
|
+
end
|
102
|
+
|
103
|
+
def all_main_methods
|
104
|
+
@commands.reject {|e| e.namespace }.map {|e| [e.name, e.alias]}.flatten.compact + namespaces
|
105
|
+
end
|
106
|
+
|
107
|
+
def marshal_file
|
108
|
+
File.join(repo.config_dir, 'index.marshal')
|
109
|
+
end
|
110
|
+
|
111
|
+
def find_library(command, object=false)
|
112
|
+
read
|
113
|
+
namespace_command = command.split(NAMESPACE)[0]
|
114
|
+
if (lib = @libraries.find {|e| e.namespace == namespace_command })
|
115
|
+
object ? lib : lib.name
|
116
|
+
elsif (cmd = Command.find(command, @commands))
|
117
|
+
object ? @libraries.find {|e| e.name == cmd.lib} : cmd.lib
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def changed_libraries
|
122
|
+
read
|
123
|
+
latest_hashes.select {|lib, hash| @lib_hashes[lib] != hash}.map {|e| e[0]}
|
124
|
+
end
|
125
|
+
|
126
|
+
def latest_hashes
|
127
|
+
repo.all_libraries.inject({}) {|h, e|
|
128
|
+
lib_file = FileLibrary.library_file(e, repo.dir)
|
129
|
+
h[e] = Digest::MD5.hexdigest(File.read(lib_file)) if File.exists?(lib_file)
|
130
|
+
h
|
131
|
+
}
|
132
|
+
end
|
133
|
+
#:startdoc:
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'boson/save'
|
2
|
+
|
3
|
+
module Boson
|
4
|
+
module RunnerOptions
|
5
|
+
# [:help] Gives a basic help of global options. When a command is given the help shifts to a command's help.
|
6
|
+
# [:verbose] Using this along with :help option shows more help. Also gives verbosity to other actions i.e. loading.
|
7
|
+
# [:backtrace] Prints full backtrace on error. Default is false.
|
8
|
+
# [:index] Updates index for given libraries allowing you to use them. This is useful if Boson's autodetection of
|
9
|
+
# changed libraries isn't picking up your changes. Since this option has a :bool_default attribute, arguments
|
10
|
+
# passed to this option need to be passed with '=' i.e. '--index=my_lib'.
|
11
|
+
# [:load] Explicitly loads a list of libraries separated by commas. Most useful when used with :console option.
|
12
|
+
# Can also be used to explicitly load libraries that aren't being detected automatically.
|
13
|
+
# [:pager_toggle] Toggles Hirb's pager in case you'd like to pipe to another command.
|
14
|
+
def init
|
15
|
+
super
|
16
|
+
Boson.verbose = true if options[:verbose]
|
17
|
+
|
18
|
+
if @options.key?(:index)
|
19
|
+
Index.update(:verbose=>true, :libraries=>@options[:index])
|
20
|
+
@index_updated = true
|
21
|
+
elsif !@options[:help] && @command && Boson.can_invoke?(@command)
|
22
|
+
Index.update(:verbose=>@options[:verbose])
|
23
|
+
@index_updated = true
|
24
|
+
end
|
25
|
+
|
26
|
+
Manager.load @options[:load], load_options if @options[:load]
|
27
|
+
View.toggle_pager if @options[:pager_toggle]
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_libraries
|
31
|
+
libs = super
|
32
|
+
@options[:unload] ? libs.select {|e| e !~ /#{@options[:unload]}/} : libs
|
33
|
+
end
|
34
|
+
|
35
|
+
def update_index
|
36
|
+
unless @index_updated
|
37
|
+
super
|
38
|
+
@index_updated = true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def verbose
|
43
|
+
@options[:verbose]
|
44
|
+
end
|
45
|
+
|
46
|
+
def abort_with(message)
|
47
|
+
if verbose || options[:backtrace]
|
48
|
+
message += "\nOriginal error: #{$!}\n #{$!.backtrace.join("\n ")}"
|
49
|
+
end
|
50
|
+
super(message)
|
51
|
+
end
|
52
|
+
|
53
|
+
def print_usage
|
54
|
+
super
|
55
|
+
if @options[:verbose]
|
56
|
+
Manager.load [Boson::Commands::Core]
|
57
|
+
puts "\n\nDEFAULT COMMANDS"
|
58
|
+
Boson.invoke :commands, :fields=>["name", "usage", "description"], :description=>false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def execute_option_or_command(options, command, args)
|
63
|
+
if options[:help]
|
64
|
+
autoload_command command
|
65
|
+
Boson.invoke(:usage, command, verbose: verbose)
|
66
|
+
else
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if defined? BinRunner
|
73
|
+
class BinRunner < BareRunner
|
74
|
+
GLOBAL_OPTIONS.update({
|
75
|
+
:backtrace=>{:type=>:boolean, :desc=>'Prints full backtrace'},
|
76
|
+
:verbose=>{:type=>:boolean, :desc=>"Verbose description of loading libraries, errors or help"},
|
77
|
+
:index=>{
|
78
|
+
:type=>:array, :desc=>"Libraries to index. Libraries must be passed with '='.",
|
79
|
+
:bool_default=>nil, :values=>all_libraries, :regexp=>true, :enum=>false},
|
80
|
+
:pager_toggle=>{:type=>:boolean, :desc=>"Toggles Hirb's pager"},
|
81
|
+
:unload=>{:type=>:string, :desc=>"Acts as a regular expression to unload default libraries"},
|
82
|
+
:load=>{:type=>:array, :values=>all_libraries, :regexp=>true, :enum=>false,
|
83
|
+
:desc=>"A comma delimited array of libraries to load"}
|
84
|
+
})
|
85
|
+
extend RunnerOptions
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|