boson 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,54 @@
1
+ module Boson
2
+ # Runner used when starting irb. To use in irb, drop this in your ~/.irbrc:
3
+ # require 'boson'
4
+ # Boson.start
5
+ class ConsoleRunner < Runner
6
+ class <<self
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
10
+ # [:libraries] Array of libraries to load.
11
+ # [:verbose] Boolean to be verbose about libraries loading. Default is true.
12
+ # [:no_defaults] Boolean which turns off loading any default libraries. Default is false.
13
+ # [:autoload_libraries] Boolean which makes any command execution easier. It redefines
14
+ # method_missing on Boson.main_object so that commands with unloaded
15
+ # libraries are automatically loaded. Default is false.
16
+ def start(options={})
17
+ @options = {:verbose=>true}.merge options
18
+ init unless @initialized
19
+ Manager.load(@options[:libraries], load_options) if @options[:libraries]
20
+ end
21
+
22
+ # Loads libraries and then starts irb (or the configured console) from the commandline.
23
+ def bin_start(repl, libraries)
24
+ start :no_defaults=>true, :libraries=>libraries
25
+ repl = Boson.repo.config[:console] if Boson.repo.config[:console]
26
+ repl = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' unless repl.is_a?(String)
27
+ unless repl.index('/') == 0 || (repl = Util.which(repl))
28
+ $stderr.puts "Console not found. Please specify full path in config[:console]."
29
+ return
30
+ end
31
+ ARGV.replace ['-f']
32
+ Kernel.load $0 = repl
33
+ end
34
+
35
+ def init #:nodoc:
36
+ super
37
+ define_autoloader if @options[:autoload_libraries]
38
+ @initialized = true
39
+ end
40
+
41
+ def default_libraries #:nodoc:
42
+ defaults = super
43
+ unless @options[:no_defaults]
44
+ new_defaults = Boson.repos.map {|e| e.config[:console_defaults] }.flatten
45
+ new_defaults = detected_libraries if new_defaults.empty?
46
+ defaults += new_defaults
47
+ defaults.uniq!
48
+ end
49
+ defaults
50
+ end
51
+ end
52
+ end
53
+ end
54
+
@@ -1,27 +1,96 @@
1
1
  require 'shellwords'
2
2
  module Boson
3
+ # Scientist redefines the methods of commands that have options and/or take global options. This redefinition
4
+ # allows a command to receive its arguments normally or as a commandline app does. For a command's
5
+ # method to be redefined correctly, its last argument _must_ expect a hash.
6
+ #
7
+ # Take for example this basic method/command with an options definition:
8
+ # options :level=>:numeric, :verbose=>:boolean
9
+ # def foo(arg='', options={})
10
+ # [arg, options]
11
+ # end
12
+ #
13
+ # When Scientist wraps around foo(), argument defaults are respected:
14
+ # foo '', :verbose=>true # normal call
15
+ # foo '-v' # commandline call
16
+ #
17
+ # Both calls return: ['', {:verbose=>true}]
18
+ #
19
+ # Non-string arguments can be passed in:
20
+ # foo Object, :level=>1
21
+ # foo Object, 'l1'
22
+ #
23
+ # Both calls return: [Object, {:level=>1}]
24
+ #
25
+ # === Global Options
26
+ # Any command with options comes with default global options. For example '-hv' on such a command
27
+ # prints a help summarizing a command's options as well as the global options.
28
+ # When using global options along with command options, global options _must_ precede command options.
29
+ # Take for example using the global --pretend option with the method above:
30
+ # irb>> foo '-p -l=1'
31
+ # Arguments: ["", {:level=>1}]
32
+ # Global options: {:pretend=>true}
33
+ #
34
+ # If a global option conflicts with a command's option, the command's option takes precedence. You can get around
35
+ # this by passing a --global option which takes a string of options without their dashes. For example:
36
+ # foo '-p --fields=f1,f2 -l=1'
37
+ # # is the same as
38
+ # foo ' -g "p fields=f1,f2" -l=1 '
39
+ #
40
+ # === Rendering Views With Global Options
41
+ # Perhaps the most important global option is --render. This option toggles the rendering of your command's output
42
+ # with Hirb[http://github.com/cldwalker/hirb]. Since Hirb can be customized to generate any view, this option allows
43
+ # you toggle a predefined view for a command without embedding view code in your command!
44
+ #
45
+ # Here's a simple example, toggling Hirb's table view:
46
+ # # Defined in a library file:
47
+ # #@options {}
48
+ # def list(options={})
49
+ # [1,2,3]
50
+ # end
51
+ #
52
+ # Using it in irb:
53
+ # >> list
54
+ # => [1,2,3]
55
+ # >> list '-r'
56
+ # +-------+
57
+ # | value |
58
+ # +-------+
59
+ # | 1 |
60
+ # | 2 |
61
+ # | 3 |
62
+ # +-------+
63
+ # 3 rows in set
64
+ # => true
65
+ #
66
+ # To default to rendering a view for a command, add a render_options {method attribute}[link:classes/Boson/MethodInspector.html]
67
+ # above list() along with any options you want to pass to your Hirb helper class. In this case, using '-r' gives you the
68
+ # command's returned object instead of a formatted view!
3
69
  module Scientist
4
70
  extend self
71
+ # Handles all Scientist errors.
5
72
  class Error < StandardError; end
6
73
  class EscapeGlobalOption < StandardError; end
74
+
7
75
  attr_reader :global_options, :rendered
8
76
  @no_option_commands ||= []
9
77
  GLOBAL_OPTIONS = {
10
78
  :help=>{:type=>:boolean, :desc=>"Display a command's help"},
11
- :render=>{:type=>:boolean, :desc=>"Toggle a command's default render behavior"},
79
+ :render=>{:type=>:boolean, :desc=>"Toggle a command's default rendering behavior"},
12
80
  :verbose=>{:type=>:boolean, :desc=>"Increase verbosity for help, errors, etc."},
13
- :global=>{:type=>:string, :desc=>"Pass a string of global options without the dashes i.e. '-p -f=f1,f2' -> 'p f=f1,f2'"},
81
+ :global=>{:type=>:string, :desc=>"Pass a string of global options without the dashes"},
14
82
  :pretend=>{:type=>:boolean, :desc=>"Display what a command would execute without executing it"}
15
- }
83
+ } #:nodoc:
16
84
  RENDER_OPTIONS = {
17
85
  :fields=>{:type=>:array, :desc=>"Displays fields in the order given"},
18
86
  :sort=>{:type=>:string, :desc=>"Sort by given field"},
19
- :as=>{:type=>:string, :desc=>"Hirb helper class which renders"},
87
+ :class=>{:type=>:string, :desc=>"Hirb helper class which renders"},
20
88
  :reverse_sort=>{:type=>:boolean, :desc=>"Reverse a given sort"},
21
89
  :max_width=>{:type=>:numeric, :desc=>"Max width of a table"},
22
90
  :vertical=>{:type=>:boolean, :desc=>"Display a vertical table"}
23
- }
91
+ } #:nodoc:
24
92
 
93
+ # Redefines a command's method for the given object.
25
94
  def create_option_command(obj, command)
26
95
  cmd_block = create_option_command_block(obj, command)
27
96
  @no_option_commands << command if command.options.nil?
@@ -30,12 +99,14 @@ module Boson
30
99
  }
31
100
  end
32
101
 
102
+ # The actual method which replaces a command's original method
33
103
  def create_option_command_block(obj, command)
34
104
  lambda {|*args|
35
105
  Boson::Scientist.translate_and_render(obj, command, args) {|args| super(*args) }
36
106
  }
37
107
  end
38
108
 
109
+ #:stopdoc:
39
110
  def translate_and_render(obj, command, args)
40
111
  @global_options = {}
41
112
  args = translate_args(obj, command, args)
@@ -58,13 +129,9 @@ module Boson
58
129
  return @args if @no_option_commands.include?(@command)
59
130
  @args << parsed_options
60
131
  if @args.size != command.arg_size && !command.has_splat_args?
61
- command_size = @args.size > command.arg_size ? command.arg_size : command.arg_size - 1
62
- if @args.size - 1 == command_size
63
- raise Error, "Arguments are misaligned. Possible causes are incorrect argument "+
64
- "size or no argument for this method's options."
65
- else
66
- raise ArgumentError, "wrong number of arguments (#{@args.size - 1} for #{command_size})"
67
- end
132
+ command_size, args_size = @args.size > command.arg_size ? [command.arg_size, @args.size] :
133
+ [command.arg_size - 1, @args.size - 1]
134
+ raise ArgumentError, "wrong number of arguments (#{args_size} for #{command_size})"
68
135
  end
69
136
  end
70
137
  @args
@@ -110,7 +177,7 @@ module Boson
110
177
  @command.render_options[k] = {:default=>v}
111
178
  end
112
179
  }
113
- opts = Util.recursive_hash_merge(@command.render_options, RENDER_OPTIONS)
180
+ opts = Util.recursive_hash_merge(@command.render_options, Util.deep_copy(RENDER_OPTIONS))
114
181
  opts[:sort][:values] ||= opts[:fields][:values] if opts[:fields][:values]
115
182
  opts
116
183
  end
@@ -129,11 +196,17 @@ module Boson
129
196
  parsed_options, @args = parse_options Shellwords.shellwords(@args[0])
130
197
  # last string argument interpreted as args + options
131
198
  elsif @args.size > 1 && @args[-1].is_a?(String)
132
- parsed_options, new_args = parse_options @args.pop.split(/\s+/)
199
+ args = caller.grep(/bin_runner.rb:/).empty? ? Shellwords.shellwords(@args.pop) : @args
200
+ parsed_options, new_args = parse_options args
133
201
  @args += new_args
134
- # default options
135
- elsif (@args.size <= @command.arg_size - 1) || (@command.has_splat_args? && !@args[-1].is_a?(Hash))
202
+ # add default options
203
+ elsif (!@command.has_splat_args? && @args.size <= @command.arg_size - 1) ||
204
+ (@command.has_splat_args? && !@args[-1].is_a?(Hash))
205
+ parsed_options = parse_options([])[0]
206
+ # merge default options with given hash of options
207
+ elsif (@command.has_splat_args? || (@args.size == @command.arg_size)) && @args[-1].is_a?(Hash)
136
208
  parsed_options = parse_options([])[0]
209
+ parsed_options.merge!(@args.pop)
137
210
  end
138
211
  parsed_options
139
212
  end
@@ -143,7 +216,8 @@ module Boson
143
216
  @global_options = option_parser.parse @command.option_parser.leading_non_opts
144
217
  new_args = option_parser.non_opts.dup + @command.option_parser.trailing_non_opts
145
218
  if @global_options[:global]
146
- global_opts = Shellwords.shellwords(@global_options[:global]).map {|str| (str.length > 1 ? "--" : "-") + str }
219
+ global_opts = Shellwords.shellwords(@global_options[:global]).map {|str|
220
+ ((str[/^(.*?)=/,1] || str).length > 1 ? "--" : "-") + str }
147
221
  @global_options.merge! option_parser.parse(global_opts)
148
222
  end
149
223
  raise EscapeGlobalOption if @global_options[:help]
@@ -164,5 +238,6 @@ module Boson
164
238
  }
165
239
  end
166
240
  end
241
+ #:startdoc:
167
242
  end
168
243
  end
@@ -1,7 +1,9 @@
1
1
  module Boson
2
+ # Collection of utility methods used throughout Boson.
2
3
  module Util
3
4
  extend self
4
- #From Rails ActiveSupport
5
+ # From Rails ActiveSupport, converts a camelcased string to an underscored string:
6
+ # 'Boson::MethodInspector' -> 'boson/method_inspector'
5
7
  def underscore(camel_cased_word)
6
8
  camel_cased_word.to_s.gsub(/::/, '/').
7
9
  gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
@@ -10,11 +12,14 @@ module Boson
10
12
  downcase
11
13
  end
12
14
 
13
- # from Rails ActiveSupport
15
+ # From Rails ActiveSupport, does the reverse of underscore:
16
+ # 'boson/method_inspector' -> 'Boson::MethodInspector'
14
17
  def camelize(string)
15
18
  string.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
16
19
  end
17
-
20
+
21
+ # Converts a module/class string to the actual constant.
22
+ # Returns nil if not found.
18
23
  def constantize(string)
19
24
  any_const_get(camelize(string))
20
25
  end
@@ -40,6 +45,9 @@ module Boson
40
45
  end
41
46
  end
42
47
 
48
+ # Detects new object/kernel methods, gems and modules created within a block.
49
+ # Returns a hash of what's detected.
50
+ # Valid options and possible returned keys are :methods, :object_methods, :modules, :gems.
43
51
  def detect(options={}, &block)
44
52
  options = {:methods=>true, :object_methods=>true}.merge!(options)
45
53
  original_gems = Gem.loaded_specs.keys if Object.const_defined? :Gem
@@ -56,6 +64,7 @@ module Boson
56
64
  detected
57
65
  end
58
66
 
67
+ # Safely calls require, returning false if LoadError occurs.
59
68
  def safe_require(lib)
60
69
  begin
61
70
  require lib
@@ -64,16 +73,20 @@ module Boson
64
73
  end
65
74
  end
66
75
 
76
+ # Returns all modules that currently exist.
67
77
  def modules
68
78
  all_modules = []
69
79
  ObjectSpace.each_object(Module) {|e| all_modules << e}
70
80
  all_modules
71
81
  end
72
82
 
83
+ # Returns array of _all_ common instance methods between two modules/classes.
73
84
  def common_instance_methods(module1, module2)
74
85
  (module1.instance_methods + module1.private_instance_methods) & (module2.instance_methods + module2.private_instance_methods)
75
86
  end
76
87
 
88
+ # Creates a module under a given base module and possible name. If the module already exists, it attempts
89
+ # to create one with a number appended to the name.
77
90
  def create_module(base_module, name)
78
91
  desired_class = camelize(name)
79
92
  if (suffix = ([""] + (1..10).to_a).find {|e| !base_module.const_defined?(desired_class+e)})
@@ -81,10 +94,16 @@ module Boson
81
94
  end
82
95
  end
83
96
 
97
+ # Behaves just like the unix which command, returning the full path to an executable based on ENV['PATH'].
84
98
  def which(command)
85
99
  ENV['PATH'].split(File::PATH_SEPARATOR).map {|e| File.join(e, command) }.find {|e| File.exists?(e) }
86
100
  end
87
101
 
102
+ # Deep copies any object if it can be marshaled. Useful for deep hashes.
103
+ def deep_copy(obj)
104
+ Marshal::load(Marshal::dump(obj))
105
+ end
106
+
88
107
  # Recursively merge hash1 with hash2.
89
108
  def recursive_hash_merge(hash1, hash2)
90
109
  hash1.merge(hash2) {|k,o,n| (o.is_a?(Hash)) ? recursive_hash_merge(o,n) : n}
@@ -1,18 +1,30 @@
1
1
  module Boson
2
+ # Handles {Hirb}[http://tagaholic.me/hirb/]-based views.
2
3
  module View
3
4
  extend self
4
5
 
6
+ # Enables hirb and reads a config file from the main repo's config/hirb.yml.
5
7
  def enable
6
8
  Hirb::View.enable(:config_file=>File.join(Boson.repo.config_dir, 'hirb.yml')) unless @enabled
7
9
  @enabled = true
8
10
  end
9
11
 
12
+ # Renders any object via Hirb. Options are passed directly to
13
+ # {Hirb::Console.render_output}[http://tagaholic.me/hirb/doc/classes/Hirb/Console.html#M000011].
10
14
  def render(object, options={})
11
- [nil,false,true].include?(object) ? puts(object.inspect) : render_object(object, options)
15
+ if silent_object?(object)
16
+ puts(object.inspect) unless options[:silence_booleans]
17
+ else
18
+ render_object(object, options)
19
+ end
20
+ end
21
+
22
+ def silent_object?(obj)
23
+ [nil,false,true].include?(obj)
12
24
  end
13
25
 
14
- def render_object(object, options={})
15
- options[:class] = options.delete(:as) || :auto_table
26
+ def render_object(object, options={}) #:nodoc:
27
+ options[:class] ||= :auto_table
16
28
  if object.is_a?(Array) && object.size > 0 && (sort = options.delete(:sort))
17
29
  begin
18
30
  sort_lambda = object[0].is_a?(Hash) ? (object[0][sort].respond_to?(:<=>) ?
@@ -36,17 +36,17 @@ module Boson
36
36
  start('-l', 'blah', 'libraries')
37
37
  end
38
38
 
39
- test "repl option starts repl" do
40
- ReplRunner.expects(:start)
39
+ test "console option starts irb" do
40
+ ConsoleRunner.expects(:start)
41
41
  Util.expects(:which).returns("/usr/bin/irb")
42
42
  Kernel.expects(:load).with("/usr/bin/irb")
43
- start("--repl")
43
+ start("--console")
44
44
  end
45
45
 
46
- test "repl option but no repl found prints error" do
47
- ReplRunner.expects(:start)
46
+ test "console option but no irb found prints error" do
47
+ ConsoleRunner.expects(:start)
48
48
  Util.expects(:which).returns(nil)
49
- capture_stderr { start("--repl") } =~ /Repl not found/
49
+ capture_stderr { start("--console") }.should =~ /Console not found/
50
50
  end
51
51
 
52
52
  test "execute option executes string" do
@@ -54,17 +54,27 @@ module Boson
54
54
  capture_stdout { start("-e", "p 1 + 1") }.should == "2\n"
55
55
  end
56
56
 
57
+ test "global option takes value with whitespace" do
58
+ View.expects(:render).with(anything, {:sort => :lib, :fields => [:name, :lib]})
59
+ start('commands', '-g', 'f=name,lib s=lib')
60
+ end
61
+
57
62
  test "execute option errors are caught" do
58
63
  capture_stderr { start("-e", "raise 'blah'") }.should =~ /^Error:/
59
64
  end
60
65
 
61
66
  test "command and too many arguments prints error" do
62
- capture_stdout { start('commands','1','2','3') }.should =~ /Wrong number/
67
+ capture_stdout { capture_stderr { start('commands','1','2','3') }.should =~ /'commands'.*incorrect/ }
68
+ end
69
+
70
+ test "failed subcommand prints error and not command not found" do
71
+ BinRunner.expects(:execute_command).raises("bling")
72
+ capture_stderr { start("commands.blah") }.should =~ /Error: bling/
63
73
  end
64
74
 
65
75
  test "undiscovered command prints error" do
66
76
  BinRunner.expects(:load_command_by_index).returns(false)
67
- capture_stderr { start('blah') }.should =~ /Error.*blah/
77
+ capture_stderr { start('blah') }.should =~ /Error.*not found/
68
78
  end
69
79
 
70
80
  test "basic command executes" do
@@ -30,7 +30,7 @@ module Boson
30
30
  end
31
31
 
32
32
  test "loads and strips aliases from a library's commands" do
33
- with_config(:commands=>{"blah"=>{:alias=>'b'}}) do
33
+ with_config(:command_aliases=>{"blah"=>'b'}) do
34
34
  load :blah, :file_string=>"module Blah; def blah; end; alias_method(:b, :blah); end"
35
35
  library_loaded?('blah')
36
36
  library('blah').commands.should == ['blah']
@@ -37,7 +37,7 @@ module Boson
37
37
  after(:each) { Object.send(:remove_const, "Aquateen") }
38
38
 
39
39
  test "created with command specific config" do
40
- with_config(:commands=>{'frylock'=>{:alias=>'fr'}}) do
40
+ with_config(:command_aliases=>{'frylock'=>'fr'}) do
41
41
  Manager.expects(:create_instance_aliases).with({"Aquateen"=>{"frylock"=>"fr"}})
42
42
  load_library :name=>'aquateen', :commands=>['frylock'], :module=>Aquateen
43
43
  library_loaded? 'aquateen'
@@ -53,7 +53,7 @@ module Boson
53
53
  end
54
54
 
55
55
  test "not created and warns for commands with no module" do
56
- with_config(:commands=>{'frylock'=>{:alias=>'fr'}}) do
56
+ with_config(:command_aliases=>{'frylock'=>'fr'}) do
57
57
  capture_stderr {
58
58
  load_library(:name=>'aquateen', :commands=>['frylock'])
59
59
  }.should =~ /No aliases/
@@ -16,7 +16,7 @@ class Boson::RepoTest < Test::Unit::TestCase
16
16
  end
17
17
 
18
18
  test "ignores nonexistent file and sets config defaults" do
19
- assert @repo.config[:commands].is_a?(Hash) && @repo.config[:libraries].is_a?(Hash)
19
+ assert @repo.config[:command_aliases].is_a?(Hash) && @repo.config[:libraries].is_a?(Hash)
20
20
  end
21
21
  end
22
22
  end
@@ -3,17 +3,17 @@ require File.join(File.dirname(__FILE__), 'test_helper')
3
3
  module Boson
4
4
  class RunnerTest < Test::Unit::TestCase
5
5
  context "repl_runner" do
6
- def start(*args)
6
+ def start(hash={})
7
7
  Hirb.stubs(:enable)
8
- Boson.start(*args)
8
+ Boson.start(hash.merge(:verbose=>false))
9
9
  end
10
10
 
11
11
  before(:all) { reset }
12
- before(:each) { Boson::ReplRunner.instance_eval("@initialized = false") }
12
+ before(:each) { Boson::ConsoleRunner.instance_eval("@initialized = false") }
13
13
 
14
- test "loads default libraries and libraries in :defaults config" do
14
+ test "loads default libraries and libraries in :console_defaults config" do
15
15
  defaults = Boson::Runner.default_libraries + ['yo']
16
- with_config(:defaults=>['yo']) do
16
+ with_config(:console_defaults=>['yo']) do
17
17
  Manager.expects(:load).with {|*args| args[0] == defaults }
18
18
  start
19
19
  end
@@ -21,12 +21,12 @@ module Boson
21
21
 
22
22
  test "doesn't call init twice" do
23
23
  start
24
- ReplRunner.expects(:init).never
24
+ ConsoleRunner.expects(:init).never
25
25
  start
26
26
  end
27
27
 
28
28
  test "loads multiple libraries with :libraries option" do
29
- ReplRunner.expects(:init)
29
+ ConsoleRunner.expects(:init)
30
30
  Manager.expects(:load).with([:lib1,:lib2], anything)
31
31
  start(:libraries=>[:lib1, :lib2])
32
32
  end