boson-more 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/.gemspec +22 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +97 -0
  4. data/Rakefile +35 -0
  5. data/deps.rip +1 -0
  6. data/lib/boson/alias.rb +75 -0
  7. data/lib/boson/argument_inspector.rb +90 -0
  8. data/lib/boson/commands/core.rb +67 -0
  9. data/lib/boson/commands/view_core.rb +19 -0
  10. data/lib/boson/commands/web_core.rb +153 -0
  11. data/lib/boson/comment_inspector.rb +100 -0
  12. data/lib/boson/console.rb +40 -0
  13. data/lib/boson/console_runner.rb +60 -0
  14. data/lib/boson/index.rb +48 -0
  15. data/lib/boson/libraries/file_library.rb +144 -0
  16. data/lib/boson/libraries/gem_library.rb +30 -0
  17. data/lib/boson/libraries/local_file_library.rb +30 -0
  18. data/lib/boson/libraries/module_library.rb +37 -0
  19. data/lib/boson/libraries/require_library.rb +23 -0
  20. data/lib/boson/libraries.rb +183 -0
  21. data/lib/boson/more/version.rb +5 -0
  22. data/lib/boson/more.rb +18 -0
  23. data/lib/boson/more_commands.rb +14 -0
  24. data/lib/boson/more_inspector.rb +42 -0
  25. data/lib/boson/more_manager.rb +34 -0
  26. data/lib/boson/more_method_inspector.rb +74 -0
  27. data/lib/boson/more_option_parser.rb +28 -0
  28. data/lib/boson/more_scientist.rb +68 -0
  29. data/lib/boson/more_util.rb +30 -0
  30. data/lib/boson/namespace.rb +31 -0
  31. data/lib/boson/namespacer.rb +117 -0
  32. data/lib/boson/pipe.rb +156 -0
  33. data/lib/boson/pipe_runner.rb +44 -0
  34. data/lib/boson/pipes.rb +75 -0
  35. data/lib/boson/repo.rb +96 -0
  36. data/lib/boson/repo_index.rb +135 -0
  37. data/lib/boson/runner_options.rb +88 -0
  38. data/lib/boson/save.rb +198 -0
  39. data/lib/boson/science.rb +273 -0
  40. data/lib/boson/view.rb +98 -0
  41. data/lib/boson/viewable.rb +48 -0
  42. data/test/alias_test.rb +55 -0
  43. data/test/argument_inspector_test.rb +40 -0
  44. data/test/command_test.rb +22 -0
  45. data/test/commands_test.rb +53 -0
  46. data/test/comment_inspector_test.rb +126 -0
  47. data/test/console_runner_test.rb +58 -0
  48. data/test/deps.rip +4 -0
  49. data/test/file_library_test.rb +41 -0
  50. data/test/gem_library_test.rb +40 -0
  51. data/test/libraries_test.rb +55 -0
  52. data/test/loader_test.rb +38 -0
  53. data/test/module_library_test.rb +30 -0
  54. data/test/more_manager_test.rb +29 -0
  55. data/test/more_method_inspector_test.rb +42 -0
  56. data/test/more_scientist_test.rb +10 -0
  57. data/test/namespacer_test.rb +61 -0
  58. data/test/pipes_test.rb +65 -0
  59. data/test/repo_index_test.rb +123 -0
  60. data/test/repo_test.rb +23 -0
  61. data/test/runner_options_test.rb +29 -0
  62. data/test/save_test.rb +86 -0
  63. data/test/science_test.rb +58 -0
  64. data/test/scientist_test.rb +195 -0
  65. data/test/test_helper.rb +165 -0
  66. data/test/web_test.rb +22 -0
  67. 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
@@ -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