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