boson 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,12 +9,17 @@ Gem::Specification.new do |s|
9
9
  s.email = "gabriel.horner@gmail.com"
10
10
  s.homepage = "http://tagaholic.me/boson/"
11
11
  s.summary = "A command/task framework similar to rake and thor that opens your ruby universe to the commandline and irb."
12
- s.description = "Boson provides users with the power to turn any ruby method into a full-fledged commandline tool. Boson achieves this with powerful options (borrowed from thor) and views (thanks to hirb). Some other unique features that differentiate it from rake and thor include being accessible from irb and the commandline, being able to write boson commands in non-dsl ruby and toggling a pretty view of a command's output without additional view code."
12
+ s.description = "Boson is a command/task framework with the power to turn any ruby method into a full-fledged executable with options. Some unique features that differentiate it from rake and thor include being usable from irb and the commandline, optional automated views generated by hirb and allowing libraries to be written as plain ruby. For my libraries that use this, see irbfiles. Works with all major ruby versions."
13
13
  s.required_rubygems_version = ">= 1.3.6"
14
14
  s.rubyforge_project = 'tagaholic'
15
15
  s.executables = ['boson']
16
16
  s.add_dependency 'hirb', '>= 0.3.2'
17
- s.add_dependency 'alias', '>= 0.2.1'
18
- s.files = Dir.glob(%w[{lib,test}/**/*.rb bin/* [A-Z]*.{txt,rdoc} ext/**/*.{rb,c}]) + %w{Rakefile gemspec}
17
+ s.add_dependency 'alias', '>= 0.2.2'
18
+ s.add_development_dependency 'mocha'
19
+ s.add_development_dependency 'bacon', '>= 1.1.0'
20
+ s.add_development_dependency 'mocha-on-bacon'
21
+ s.add_development_dependency 'bacon-bits'
22
+ s.files = Dir.glob(%w[{lib,test}/**/*.rb bin/* [A-Z]*.{txt,rdoc} ext/**/*.{rb,c} **/deps.rip]) + %w{Rakefile .gemspec}
19
23
  s.extra_rdoc_files = ["README.rdoc", "LICENSE.txt"]
24
+ s.license = 'MIT'
20
25
  end
@@ -1,3 +1,16 @@
1
+ == 0.3.0
2
+ * Added --debug to executable with multiple debug hooks
3
+ * Added --ruby_debug and -I to executable to change $LOAD_PATH and $DEBUG
4
+ * Added @option method attribute as a more readable complement to @options
5
+ * Added proper exit code for failed commands (#12)
6
+ * Added friendlier errors for libraries with SyntaxError or LoaderError
7
+ * Added validation to method attributes
8
+ * Improved RequireLibrary to more robustly handle gems like httparty
9
+ * Fixed 1.9.2-rc2 bugs including #14
10
+ * Fixed finding commands with same names
11
+ * Fixed --console for ruby >= 1.8.7
12
+ * Fixed --help for namespaced commands
13
+
1
14
  == 0.2.5
2
15
  * Fixed critical gemspec error
3
16
 
@@ -1,9 +1,11 @@
1
1
  To read a linkable version of this README, {see here}[http://tagaholic.me/boson/doc/].
2
2
 
3
3
  == Description
4
- A command/task framework similar to rake and thor that opens your ruby universe to the commandline
5
- and irb. For my libraries that use this, see {irbfiles}[http://github.com/cldwalker/irbfiles].
6
- Works with all major ruby versions.
4
+ Boson is a command/task framework with the power to turn any ruby method into a full-fledged
5
+ executable with options. Some unique features that differentiate it from rake and thor include
6
+ being usable from irb and the commandline, optional automated views generated by hirb and allowing
7
+ libraries to be written as plain ruby. For my libraries that use this, see
8
+ {irbfiles}[http://github.com/cldwalker/irbfiles]. Works with all major ruby versions.
7
9
 
8
10
  == Features
9
11
  * Simple organization: Commands are just methods on an object (default is main) and command libraries are just modules.
@@ -159,10 +161,10 @@ My {tagging obsession}[http://github.com/cldwalker/tag-tree] from the ruby conso
159
161
  * http://tagaholic.me/2009/10/15/boson-and-hirb-interactions.html
160
162
  * http://tagaholic.me/2009/10/19/how-boson-enhances-your-irb-experience.html
161
163
 
162
- == Acknowledgements
164
+ == Credits
163
165
  Boson stands on the shoulders of these people and their ideas:
164
166
  * Yehuda Katz for inspiring me with Thor and its awesome option parser (Boson::OptionParser).
165
167
  * Daniel Berger for his original work on thor's awesome option parser.
166
168
  * Dave Thomas for scraping a method's comments (Boson::CommentInspector)
167
169
  * Mauricio Fernandez for scraping a method's arguments (Boson::ArgumentInspector)
168
- * Chris Wanstrath for inspiring Boson's libraries with Rip's packages.
170
+ * Chris Wanstrath for inspiring Boson's libraries with Rip's packages.
data/Rakefile CHANGED
@@ -2,12 +2,12 @@ require 'rake'
2
2
  require 'fileutils'
3
3
 
4
4
  def gemspec
5
- @gemspec ||= eval(File.read('gemspec'), binding, 'gemspec')
5
+ @gemspec ||= eval(File.read('.gemspec'), binding, '.gemspec')
6
6
  end
7
7
 
8
8
  desc "Build the gem"
9
9
  task :gem=>:gemspec do
10
- sh "gem build gemspec"
10
+ sh "gem build .gemspec"
11
11
  FileUtils.mkdir_p 'pkg'
12
12
  FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", 'pkg'
13
13
  end
@@ -32,4 +32,4 @@ task :test do |t|
32
32
  sh 'bacon -q -Ilib -I. test/*_test.rb'
33
33
  end
34
34
 
35
- task :default => :test
35
+ task :default => :test
@@ -0,0 +1,2 @@
1
+ hirb >=0.3.2
2
+ alias >=0.2.2
@@ -20,13 +20,20 @@ module Boson
20
20
  # Finds a command, namespaced or not and aliased or not. If found returns the
21
21
  # command object, otherwise returns nil.
22
22
  def self.find(command, commands=Boson.commands)
23
- command, subcommand = command.to_s.split(NAMESPACE, 2)
24
- is_namespace_command = lambda {|current_command|
25
- [current_command.name, current_command.alias].include?(subcommand) &&
26
- current_command.library && (current_command.library.namespace == command)
27
- }
28
- find_lambda = subcommand ? is_namespace_command : lambda {|e| [e.name, e.alias].include?(command)}
29
- commands.find(&find_lambda)
23
+ if command.to_s.include?(NAMESPACE)
24
+ command, subcommand = command.to_s.split(NAMESPACE, 2)
25
+ commands.find {|current_command|
26
+ [current_command.name, current_command.alias].include?(subcommand) &&
27
+ current_command.library && (current_command.library.namespace == command)
28
+ }
29
+ else
30
+ commands.find {|e| [e.name, e.alias].include?(command) && !e.namespace}
31
+ end
32
+ end
33
+
34
+ # One line usage for a command if it exists
35
+ def self.usage(command)
36
+ (cmd = find(command)) ? "#{command} #{cmd.usage}" : "Command '#{command}' not found"
30
37
  end
31
38
 
32
39
  ATTRIBUTES = [:name, :lib, :alias, :desc, :options, :args, :config]
@@ -61,17 +61,17 @@ module Boson::Commands::Core #:nodoc:
61
61
  end
62
62
 
63
63
  def usage(command, options={})
64
- msg = (cmd = Boson::Command.find(command)) ? "#{command} #{cmd.usage}" : "Command '#{command}' not found"
65
- puts msg
66
- return if options[:one_line] || !cmd
64
+ puts Boson::Command.usage(command)
67
65
 
68
- if cmd.options && !cmd.options.empty?
69
- puts "\nLOCAL OPTIONS"
70
- cmd.option_parser.print_usage_table options[:render_options].dup.merge(:local=>true)
71
- end
72
- if options[:verbose] && cmd.render_option_parser
73
- puts "\nGLOBAL OPTIONS"
74
- cmd.render_option_parser.print_usage_table options[:render_options].dup
66
+ if (cmd = Boson::Command.find(command))
67
+ if cmd.options && !cmd.options.empty?
68
+ puts "\nLOCAL OPTIONS"
69
+ cmd.option_parser.print_usage_table options[:render_options].dup.merge(:local=>true)
70
+ end
71
+ if options[:verbose] && cmd.render_option_parser
72
+ puts "\nGLOBAL OPTIONS"
73
+ cmd.render_option_parser.print_usage_table options[:render_options].dup
74
+ end
75
75
  end
76
76
  end
77
77
  end
@@ -9,16 +9,19 @@ module Boson
9
9
  # # @render_options :fields=>%w{one two}
10
10
  # # @config :alias=>'so'
11
11
  # options :verbose=>:boolean
12
+ # option :count, :numeric
12
13
  # # Something descriptive perhaps
13
14
  # def some_method(opts)
14
15
  # # ...
15
16
  # end
16
17
  # end
17
18
  #
18
- # Method attributes serve as configuration for a method's command. Available method attributes:
19
+ # Method attributes serve as configuration for a method's command. All attributes should only be called once per
20
+ # method except for option. Available method attributes:
19
21
  # * config: Hash to define any command attributes (see Command.new).
20
22
  # * desc: String to define a command's description for a command. Defaults to first commented line above a method.
21
23
  # * options: Hash to define an OptionParser object for a command's options.
24
+ # * option: Option name and value to be merged in with options. See OptionParser for what an option value can be.
22
25
  # * render_options: Hash to define an OptionParser object for a command's local/global render options (see View).
23
26
  #
24
27
  # When deciding whether to use commented or normal Module methods, remember that commented Module methods allow
@@ -31,9 +34,9 @@ module Boson
31
34
  # loading its methods.
32
35
  def enable
33
36
  @enabled = true
34
- body = MethodInspector::METHODS.map {|e|
35
- %[def #{e}(val)
36
- Boson::MethodInspector.#{e}(self, val)
37
+ body = MethodInspector::ALL_METHODS.map {|e|
38
+ %[def #{e}(*args)
39
+ Boson::MethodInspector.#{e}(self, *args)
37
40
  end]
38
41
  }.join("\n") +
39
42
  %[
@@ -50,7 +53,7 @@ module Boson
50
53
  # Disable scraping method data.
51
54
  def disable
52
55
  ::Module.module_eval %[
53
- Boson::MethodInspector::METHODS.each {|e| remove_method e }
56
+ Boson::MethodInspector::ALL_METHODS.each {|e| remove_method e }
54
57
  alias_method :method_added, :_old_method_added
55
58
  ]
56
59
  @enabled = false
@@ -71,7 +74,17 @@ module Boson
71
74
  (MethodInspector::METHODS + [:args]).each do |key|
72
75
  (@store[key] || []).each do |cmd, val|
73
76
  @commands_hash[cmd] ||= {}
74
- add_scraped_data_to_config(key, val, cmd)
77
+ add_valid_data_to_config(key, val, cmd)
78
+ end
79
+ end
80
+ end
81
+
82
+ def add_valid_data_to_config(key, value, cmd)
83
+ if valid_attr_value?(key, value)
84
+ add_scraped_data_to_config(key, value, cmd)
85
+ else
86
+ if Runner.debug
87
+ warn "DEBUG: Command '#{cmd}' has #{key.inspect} attribute with invalid value '#{value.inspect}'"
75
88
  end
76
89
  end
77
90
  end
@@ -88,12 +101,17 @@ module Boson
88
101
  end
89
102
  end
90
103
 
104
+ def valid_attr_value?(key, value)
105
+ return true if (klass = MethodInspector::METHOD_CLASSES[key]).nil?
106
+ value.is_a?(klass) || value.nil?
107
+ end
108
+
91
109
  def add_comment_scraped_data
92
110
  (@store[:method_locations] || []).select {|k,(f,l)| f == @library_file }.each do |cmd, (file, lineno)|
93
111
  scraped = CommentInspector.scrape(FileLibrary.read_library_file(file), lineno, MethodInspector.current_module)
94
112
  @commands_hash[cmd] ||= {}
95
113
  MethodInspector::METHODS.each do |e|
96
- add_scraped_data_to_config(e, scraped[e], cmd)
114
+ add_valid_data_to_config(e, scraped[e], cmd)
97
115
  end
98
116
  end
99
117
  end
@@ -26,11 +26,15 @@ module Boson::ArgumentInspector
26
26
  return if local_variables == params # nothing new found
27
27
  format_arguments(params, values, arity, num_args)
28
28
  rescue Exception
29
- # puts "#{klass}.#{meth}: #{$!.message}"
29
+ print_debug_message(klass, meth) if Boson::Runner.debug
30
30
  ensure
31
31
  set_trace_func(nil)
32
32
  end
33
33
 
34
+ def print_debug_message(klass, meth) #:nodoc:
35
+ warn "DEBUG: Error while scraping arguments from #{klass.to_s[/\w+$/]}##{meth}: #{$!.message}"
36
+ end
37
+
34
38
  # process params + values to return array of argument arrays
35
39
  def format_arguments(params, values, arity, num_args) #:nodoc:
36
40
  params ||= []
@@ -55,10 +59,11 @@ module Boson::ArgumentInspector
55
59
  begin
56
60
  if event[/call/] && classname == klass && id == meth
57
61
  params = eval("local_variables", binding)
58
- values = eval("local_variables.map{|x| eval(x)}", binding)
62
+ values = eval("local_variables.map{|x| eval(x.to_s)}", binding)
59
63
  throw :done
60
64
  end
61
65
  rescue Exception
66
+ print_debug_message(klass, meth) if Boson::Runner.debug
62
67
  end
63
68
  }
64
69
  if arity >= 0
@@ -70,7 +75,7 @@ module Boson::ArgumentInspector
70
75
  MAX_ARGS.downto(arity.abs - 1) do |i|
71
76
  catch(:done) do
72
77
  begin
73
- object.send(meth, *(0...i))
78
+ object.send(meth, *(0...i))
74
79
  rescue Exception
75
80
  end
76
81
  end
@@ -10,7 +10,8 @@ module Boson
10
10
  # * If no @desc is found in the comment block, then the first comment line directly above the method
11
11
  # is assumed to be the value for @desc. This means that no multi-line attribute definitions can occur
12
12
  # without a description since the last line is assumed to be a description.
13
- # * options, config and render_options attributes can take any valid ruby since they're evaled in their module's context.
13
+ # * options, option, config and render_options attributes can take any valid ruby since they're evaled in
14
+ # their module's context.
14
15
  # * desc attribute is not evaled and is simply text to be set as a string.
15
16
  #
16
17
  # This module was inspired by
@@ -23,16 +24,35 @@ module Boson
23
24
  # of attributes defined for that method.
24
25
  def scrape(file_string, line, mod, attribute=nil)
25
26
  hash = scrape_file(file_string, line) || {}
27
+ options = (arr = hash.delete(:option)) ? parse_option_comments(arr, mod) : {}
26
28
  hash.select {|k,v| v && (attribute.nil? || attribute == k) }.each do |k,v|
27
- hash[k] = EVAL_ATTRIBUTES.include?(k) ? eval_comment(v.join(' '), mod) : v.join(' ')
29
+ hash[k] = EVAL_ATTRIBUTES.include?(k) ? eval_comment(v.join(' '), mod, k) : v.join(' ')
28
30
  end
31
+ (hash[:options] ||= {}).merge!(options) if !options.empty?
29
32
  attribute ? hash[attribute] : hash
30
33
  end
31
34
 
32
35
  #:stopdoc:
33
- def eval_comment(value, mod)
36
+ def parse_option_comments(arr, mod)
37
+ arr.inject({}) {|t,e|
38
+ key, val = e.join(' ').split(/\s*,\s*/, 2)
39
+ if val
40
+ key = key.sub(/^\s*:/, '').to_sym
41
+ t[key] = eval_comment(val, mod, 'option')
42
+ end
43
+ t
44
+ }
45
+ end
46
+
47
+ def eval_comment(value, mod, mattr)
34
48
  value = "{#{value}}" if !value[/^\s*\{/] && value[/=>/]
35
- begin mod.module_eval(value); rescue(Exception); nil end
49
+ mod.module_eval(value)
50
+ rescue Exception
51
+ if Runner.debug
52
+ warn "DEBUG: Error while evaluating @#{mattr} in module #{mod.to_s[/\w+$/]}:\n " +
53
+ $!.message.gsub(/\n/, "\n ")
54
+ end
55
+ nil
36
56
  end
37
57
 
38
58
  # Scrapes a given string for commented @keywords, starting with the line above the given line
@@ -58,6 +78,7 @@ module Boson
58
78
  lines << last_line unless hash[:desc]
59
79
  end
60
80
 
81
+ option = []
61
82
  while i < lines.size
62
83
  while lines[i] =~ /^\s*#\s*@(\w+)\s*(.*)/
63
84
  key = $1.to_sym
@@ -67,9 +88,11 @@ module Boson
67
88
  hash[key] << $1
68
89
  i+= 1
69
90
  end
91
+ option << hash.delete(:option) if key == :option
70
92
  end
71
93
  i += 1
72
94
  end
95
+ hash[:option] = option if !option.empty?
73
96
  hash
74
97
  end
75
98
  #:startdoc:
@@ -8,6 +8,8 @@ module Boson
8
8
  attr_reader :mod_store
9
9
  @mod_store ||= {}
10
10
  METHODS = [:config, :desc, :options, :render_options]
11
+ METHOD_CLASSES = {:config=>Hash, :desc=>String, :options=>Hash, :render_options=>Hash}
12
+ ALL_METHODS = METHODS + [:option]
11
13
 
12
14
  # The method_added used while scraping method attributes.
13
15
  def new_method_added(mod, meth)
@@ -17,8 +19,9 @@ module Boson
17
19
  METHODS.each do |e|
18
20
  store[e][meth.to_s] = store[:temp][e] if store[:temp][e]
19
21
  end
22
+ (store[:options][meth.to_s] ||= {}).merge! store[:temp][:option] if store[:temp][:option]
20
23
 
21
- if store[:temp].size < METHODS.size
24
+ if store[:temp].size < ALL_METHODS.size
22
25
  store[:method_locations] ||= {}
23
26
  if (result = find_method_locations(caller))
24
27
  store[:method_locations][meth.to_s] = result
@@ -35,6 +38,12 @@ module Boson
35
38
  end
36
39
  end
37
40
 
41
+ def option(mod, name, value)
42
+ (@mod_store[mod] ||= {})[:options] ||= {}
43
+ (store(mod)[:temp] ||= {})[:option] ||= {}
44
+ (store(mod)[:temp] ||= {})[:option][name] = value
45
+ end
46
+
38
47
  # Scrapes a method's arguments using ArgumentInspector.
39
48
  def scrape_arguments(meth)
40
49
  store[:args] ||= {}
@@ -9,11 +9,11 @@
9
9
  # >> Dir.pwd
10
10
  # >> '/home'
11
11
  class Boson::RequireLibrary < Boson::GemLibrary
12
+ EXTENSIONS = ['', '.rb', '.rbw', '.so', '.bundle', '.dll', '.sl', '.jar']
12
13
  handles {|source|
13
- begin
14
- Kernel.load("#{source}.rb", true)
15
- rescue LoadError
16
- false
17
- end
14
+ extensions_glob = "{#{EXTENSIONS.join(',')}}"
15
+ $LOAD_PATH.any? {|dir|
16
+ Dir["#{File.expand_path source.to_s, dir}#{extensions_glob}"].size > 0
17
+ }
18
18
  }
19
19
  end
@@ -40,15 +40,20 @@ module Boson
40
40
  def rescue_load_action(library, load_method)
41
41
  yield
42
42
  rescue AppendFeaturesFalseError
43
+ warn "DEBUG: Library #{library} didn't load due to append_features" if Runner.debug
43
44
  rescue LoaderError=>e
44
45
  FileLibrary.reset_file_cache(library.to_s)
45
46
  failed_libraries << library
46
47
  $stderr.puts "Unable to #{load_method} library #{library}. Reason: #{e.message}"
47
- rescue StandardError=>e
48
+ rescue StandardError, SyntaxError, LoadError =>e
48
49
  FileLibrary.reset_file_cache(library.to_s)
49
50
  failed_libraries << library
50
51
  message = "Unable to #{load_method} library #{library}. Reason: #{$!}"
51
- message += "\n" + e.backtrace.slice(0,3).map {|e| " " + e }.join("\n") if @options[:verbose]
52
+ if Runner.debug
53
+ message += "\n" + e.backtrace.map {|e| " " + e }.join("\n")
54
+ elsif @options[:verbose]
55
+ message += "\n" + e.backtrace.slice(0,3).map {|e| " " + e }.join("\n")
56
+ end
52
57
  $stderr.puts message
53
58
  ensure
54
59
  Inspector.disable if Inspector.enabled
@@ -143,7 +148,7 @@ module Boson
143
148
  end
144
149
 
145
150
  def create_class_aliases(mod, class_commands)
146
- class_commands.each {|k,v|
151
+ class_commands.dup.each {|k,v|
147
152
  if v.is_a?(Array)
148
153
  class_commands.delete(k).each {|e| class_commands[e] = "#{k}.#{e}"}
149
154
  end
@@ -43,7 +43,11 @@ module Boson
43
43
  def sort_pipe(object, sort)
44
44
  sort_lambda = lambda {}
45
45
  if object[0].is_a?(Hash)
46
- sort = sort.to_i if sort.to_s[/^\d+$/]
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
47
51
  sort_lambda = untouched_sort?(object.map {|e| e[sort] }) ? lambda {|e| e[sort] } : lambda {|e| e[sort].to_s }
48
52
  else
49
53
  sort_lambda = untouched_sort?(object.map {|e| e.send(sort) }) ? lambda {|e| e.send(sort) || ''} :
@@ -2,6 +2,8 @@ module Boson
2
2
  # Base class for runners.
3
3
  class Runner
4
4
  class<<self
5
+ attr_accessor :debug
6
+
5
7
  # Enables view, adds local load path and loads default_libraries
6
8
  def init
7
9
  View.enable
@@ -45,7 +45,10 @@ module Boson
45
45
  :unload=>{:type=>:string, :desc=>"Acts as a regular expression to unload default libraries"},
46
46
  :render=>{:type=>:boolean, :desc=>"Renders a Hirb view from result of command without options"},
47
47
  :pager_toggle=>{:type=>:boolean, :desc=>"Toggles Hirb's pager"},
48
- :option_commands=>{:type=>:boolean, :desc=>"Toggles on all commands to be defined as option commands" }
48
+ :option_commands=>{:type=>:boolean, :desc=>"Toggles on all commands to be defined as option commands" },
49
+ :ruby_debug=>{:type=>:boolean, :desc=>"Sets $DEBUG", :alias=>'D'},
50
+ :debug=>{:type=>:boolean, :desc=>"Prints debug info for boson"},
51
+ :load_path=>{:type=>:string, :desc=>"Add to front of $LOAD_PATH", :alias=>'I'}
49
52
  } #:nodoc:
50
53
 
51
54
  PIPE = '+'
@@ -58,7 +61,10 @@ module Boson
58
61
  @command, @options, @args = parse_args(args)
59
62
  return puts("boson #{Boson::VERSION}") if @options[:version]
60
63
  return print_usage if args.empty? || (@command.nil? && !@options[:console] && !@options[:execute])
64
+ $:.unshift(*options[:load_path].split(":")) if options[:load_path]
65
+ Runner.debug = true if @options[:debug]
61
66
  return ConsoleRunner.bin_start(@options[:console], @options[:load]) if @options[:console]
67
+ $DEBUG = true if options[:ruby_debug]
62
68
  init
63
69
 
64
70
  if @options[:help]
@@ -71,9 +77,9 @@ module Boson
71
77
  execute_command
72
78
  end
73
79
  rescue NoMethodError
74
- print_error_message no_method_error_message
80
+ abort_with no_method_error_message
75
81
  rescue
76
- print_error_message default_error_message
82
+ abort_with default_error_message
77
83
  end
78
84
 
79
85
  def no_method_error_message #:nodoc:
@@ -114,10 +120,11 @@ module Boson
114
120
  def commands
115
121
  @commands ||= @all_args.map {|e| e[0]}
116
122
  end
123
+
117
124
  #:stopdoc:
118
- def print_error_message(message)
125
+ def abort_with(message)
119
126
  message += "\nOriginal error: #{$!}\n" + $!.backtrace.slice(0,10).map {|e| " " + e }.join("\n") if options[:verbose]
120
- $stderr.puts message
127
+ abort message
121
128
  end
122
129
 
123
130
  def default_error_message
@@ -149,9 +156,7 @@ module Boson
149
156
  rescue ArgumentError
150
157
  if $!.class == OptionCommand::CommandArgumentError || ($!.message[/wrong number of arguments/] &&
151
158
  (cmd_obj = Command.find(cmd)) && cmd_obj.arg_size != args.size)
152
- print_error_message "'#{cmd}' was called incorrectly."
153
- Boson.invoke(:usage, cmd, :one_line=>true)
154
- return
159
+ abort_with "'#{cmd}' was called incorrectly.\n" + Command.usage(cmd)
155
160
  else
156
161
  raise
157
162
  end
@@ -24,10 +24,16 @@ module Boson
24
24
  repl = Boson.repo.config[:console] if Boson.repo.config[:console]
25
25
  repl = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' unless repl.is_a?(String)
26
26
  unless repl.index('/') == 0 || (repl = Util.which(repl))
27
- $stderr.puts "Console not found. Please specify full path in config[:console]."
28
- return
27
+ abort "Console not found. Please specify full path in config[:console]."
28
+ else
29
+ load_repl(repl)
29
30
  end
31
+ end
32
+
33
+ def load_repl(repl) #:nodoc:
30
34
  ARGV.replace ['-f']
35
+ $progname = $0
36
+ alias $0 $progname
31
37
  Kernel.load $0 = repl
32
38
  end
33
39
 
@@ -147,7 +147,7 @@ module Boson
147
147
  def run_help_option
148
148
  opts = @global_options[:verbose] ? ['--verbose'] : []
149
149
  opts << "--render_options=#{@global_options[:usage_options]}" if @global_options[:usage_options]
150
- Boson.invoke :usage, @command.name + " " + opts.join(' ')
150
+ Boson.invoke :usage, @command.full_name + " " + opts.join(' ')
151
151
  end
152
152
 
153
153
  def run_pretend_option(args)
@@ -1,3 +1,3 @@
1
1
  module Boson
2
- VERSION = '0.2.5'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -19,7 +19,7 @@ describe "BinRunner" do
19
19
  end
20
20
 
21
21
  it "invalid option value prints error" do
22
- capture_stderr { start("-l") }.should =~ /Error:/
22
+ aborts_with(/Error: no value/) { start("-l") }
23
23
  end
24
24
 
25
25
  it "help option but no arguments prints usage" do
@@ -36,17 +36,18 @@ describe "BinRunner" do
36
36
  start('-l', 'blah', 'libraries')
37
37
  end
38
38
 
39
- # it "console option starts irb" do
40
- # ConsoleRunner.expects(:start)
41
- # Util.expects(:which).returns("/usr/bin/irb")
42
- # Kernel.expects(:load).with("/usr/bin/irb")
43
- # start("--console")
44
- # end
39
+ it "console option starts irb" do
40
+ ConsoleRunner.expects(:start)
41
+ Util.expects(:which).returns("/usr/bin/irb")
42
+ ConsoleRunner.expects(:load_repl).with("/usr/bin/irb")
43
+ start("--console")
44
+ end
45
45
 
46
46
  it "console option but no irb found prints error" do
47
47
  ConsoleRunner.expects(:start)
48
48
  Util.expects(:which).returns(nil)
49
- capture_stderr { start("--console") }.should =~ /Console not found/
49
+ ConsoleRunner.expects(:abort).with {|arg| arg[/Console not found/] }
50
+ start '--console'
50
51
  end
51
52
 
52
53
  it "execute option executes string" do
@@ -59,34 +60,48 @@ describe "BinRunner" do
59
60
  start('commands', '-f', 'f1, f2')
60
61
  end
61
62
 
63
+ it "debug option sets Runner.debug" do
64
+ View.expects(:render)
65
+ start('-d', 'commands')
66
+ Runner.debug.should == true
67
+ Runner.debug = nil
68
+ end
69
+
70
+ it "ruby_debug option sets $DEBUG" do
71
+ View.expects(:render)
72
+ start('-D', 'commands')
73
+ $DEBUG.should == true
74
+ $DEBUG = nil
75
+ end
76
+
62
77
  it "execute option errors are caught" do
63
- capture_stderr { start("-e", "raise 'blah'") }.should =~ /^Error:/
78
+ aborts_with(/^Error:/) { start("-e", "raise 'blah'") }
64
79
  end
65
80
 
66
81
  it "option command and too many arguments prints error" do
67
82
  capture_stdout {
68
- capture_stderr { start('commands','1','2','3') }.should =~ /'commands'.*incorrect/
83
+ aborts_with(/'commands'.*incorrect/) { start('commands','1','2','3') }
69
84
  }
70
85
  end
71
86
 
72
87
  it "normal command and too many arguments prints error" do
73
88
  capture_stdout {
74
- capture_stderr { start('render') }.should =~ /'render'.*incorrect/
89
+ aborts_with(/'render'.*incorrect/) { start('render') }
75
90
  }
76
91
  end
77
92
 
78
93
  it "failed subcommand prints error and not command not found" do
79
94
  BinRunner.expects(:execute_command).raises("bling")
80
- capture_stderr { start("commands.to_s") }.should =~ /Error: bling/
95
+ aborts_with(/Error: bling/) { start("commands.to_s") }
81
96
  end
82
97
 
83
98
  it "nonexistant subcommand prints command not found" do
84
- capture_stderr { start("to_s.bling") }.should =~ /'to_s.bling' not found/
99
+ aborts_with(/'to_s.bling' not found/) { start("to_s.bling") }
85
100
  end
86
101
 
87
102
  it "undiscovered command prints error" do
88
- BinRunner.expects(:autoload_command).returns(false)
89
- capture_stderr { start('blah') }.should =~ /Error.*not found/
103
+ BinRunner.expects(:autoload_command).returns(false)
104
+ aborts_with(/Error.*not found/) { start 'blah' }
90
105
  end
91
106
 
92
107
  it "basic command executes" do
@@ -108,7 +123,7 @@ describe "BinRunner" do
108
123
  defaults = Runner.default_libraries + ['yo']
109
124
  with_config(:bin_defaults=>['yo']) do
110
125
  Manager.expects(:load).with {|*args| args[0] == defaults }
111
- capture_stderr { start 'blah' }
126
+ aborts_with(/blah/) { start 'blah' }
112
127
  end
113
128
  end
114
129
  end
@@ -151,7 +166,7 @@ describe "BinRunner" do
151
166
  Index.expects(:find_library).returns(nil, 'sweet_lib')
152
167
  Manager.expects(:load).with {|*args| args[0].is_a?(String) ? args[0] == 'sweet_lib' : true}.at_least(1)
153
168
  Index.indexes[0].expects(:update).returns(true)
154
- capture_stderr { start("sweet") }.should =~ /sweet/
169
+ aborts_with(/sweet/) { start 'sweet' }
155
170
  end
156
171
  end
157
172
 
@@ -0,0 +1,22 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ describe "Command" do
4
+ describe ".find" do
5
+ before_all {
6
+ reset_boson
7
+ Boson.libraries << Library.new(:name=>'bling', :namespace=>true)
8
+ @namespace_command = Command.new(:name=>'blah', :lib=>'bling', :namespace=>'bling')
9
+ @top_level_command = Command.new(:name=>'blah', :lib=>'bling')
10
+ Boson.commands << @namespace_command
11
+ Boson.commands << @top_level_command
12
+ }
13
+
14
+ it 'finds correct command when a subcommand of the same name exists' do
15
+ Command.find('blah').should == @top_level_command
16
+ end
17
+
18
+ it 'finds correct command when a top level command of the same name exists' do
19
+ Command.find('bling.blah').should == @namespace_command
20
+ end
21
+ end
22
+ end
@@ -47,39 +47,67 @@ describe "CommentInspector" do
47
47
  }
48
48
  def options(opts={})
49
49
  @lines[1] = opts[:value] if opts[:value]
50
- args = [@lines.join("\n"), 3, Optional]
50
+ args = [@lines.join("\n"), opts[:line] || 3, Optional]
51
51
  CommentInspector.scrape(*args)[:options]
52
52
  end
53
53
 
54
- it "that are basic return options" do
54
+ it "that are basic" do
55
55
  options.should == {:a=>true}
56
56
  end
57
57
 
58
- it "that are hash-like returns hashified options" do
58
+ it "that are hash-like" do
59
59
  options(:value=>'#@options :a => 2').should == {:a=>2}
60
60
  end
61
61
 
62
- it "that are whitespaced return options" do
62
+ it "that are whitespaced" do
63
63
  options(:value=>"\t"+ '# @options {:a=>1}').should == {:a=>1}
64
64
  end
65
65
 
66
- it "that have a local value return options" do
66
+ it "that have a local value" do
67
67
  options(:value=>'#@options bling').should == {:a=>'bling'}
68
68
  end
69
69
 
70
- it "that are multi-line return options" do
70
+ it "that are multi-line" do
71
71
  @lines.delete_at(1)
72
72
  @lines.insert(1, '#@options {:a =>', " # 1}", "# some comments")
73
- CommentInspector.scrape(@lines.join("\n"), 5, Optional)[:options].should == {:a=>1}
73
+ options(:line=>5).should == {:a=>1}
74
74
  end
75
75
 
76
- it "with failed eval return nil" do
76
+ it "with failed eval and returns nil" do
77
77
  options(:value=>'#@options !--').should == nil
78
78
  end
79
79
 
80
- it "that are empty return nil" do
80
+ it "that are empty and returns nil" do
81
81
  options(:value=>"# nada").should == nil
82
82
  end
83
+
84
+ it "when @option overwrites @options" do
85
+ @lines.insert(1, ' #@option :a, :boolean')
86
+ options(:line=>4).should == {:a=>:boolean}
87
+ end
88
+
89
+ it "when set by @option and @options" do
90
+ @lines.insert(1, ' #@option :b, :boolean')
91
+ options(:line=>4).should == {:b=>:boolean, :a=>true}
92
+ end
93
+
94
+ it "when set by @option" do
95
+ @lines.delete_at(1)
96
+ @lines.insert(1, ' #@option :b, :string', ' #@option :a, 4')
97
+ options(:line=>4).should == {:b=>:string, :a=>4}
98
+ end
99
+
100
+ it "when set by multi-line @option" do
101
+ @lines.delete_at(1)
102
+ @lines.insert(1, ' #@option :b, :type=>:string,', ' # :values=>%w{one two}', '# some comments')
103
+ options(:line=>5).should == {:b=>{:type=>:string, :values=>%w{one two}}}
104
+ end
105
+
106
+ it "and ignores invalid @option's" do
107
+ @lines.delete_at(1)
108
+ @lines.insert(1, ' #@option :b=>:string', ' #@option :a, :string')
109
+ options(:line=>4).should == {:a=>:string}
110
+ end
83
111
  end
84
112
 
85
113
  it "scrapes all comment types with implicit desc" do
@@ -0,0 +1,4 @@
1
+ mocha >=0
2
+ bacon >=1.1.0
3
+ mocha-on-bacon >=0
4
+ bacon-bits >=0
@@ -2,7 +2,7 @@ require File.join(File.dirname(__FILE__), 'test_helper')
2
2
 
3
3
  describe "file library" do
4
4
  before { reset; FileLibrary.reset_file_cache }
5
- before { Gem.stubs(:loaded_specs).returns({}) } if RUBY_VERSION >= '1.9.2'
5
+ before { Gem.stubs(:loaded_specs).returns({}) } if RUBY_VERSION >= '1.9.2' && defined? Gem
6
6
 
7
7
  it "loads" do
8
8
  load :blah, :file_string=>"module Blah; def blah; end; end"
@@ -5,7 +5,7 @@ describe "Loader" do
5
5
  Manager.load([Boson::Commands::Namespace])
6
6
  end
7
7
 
8
- before { Gem.stubs(:loaded_specs).returns({}) } if RUBY_VERSION >= '1.9.2'
8
+ before { Gem.stubs(:loaded_specs).returns({}) } if RUBY_VERSION >= '1.9.2' && defined? Gem
9
9
  describe "config" do
10
10
  before { reset }
11
11
  it "from callback overridden by user's config" do
@@ -1,7 +1,7 @@
1
1
  require File.join(File.dirname(__FILE__), 'test_helper')
2
2
 
3
3
  describe "Manager" do
4
- describe "after_load" do
4
+ describe ".after_load" do
5
5
  def load_library(hash)
6
6
  new_attributes = {:name=>hash[:name], :commands=>[], :created_dependencies=>[], :loaded=>true}
7
7
  [:module, :commands].each {|e| new_attributes[e] = hash.delete(e) if hash[e] }
@@ -23,6 +23,20 @@ describe "Manager" do
23
23
  command_exists?('meatwad')
24
24
  end
25
25
 
26
+ it "prints error for library with SyntaxError" do
27
+ Manager.expects(:loader_create).raises(SyntaxError)
28
+ capture_stderr {
29
+ Manager.load 'blah'
30
+ }.should =~ /Unable to load library blah. Reason: SyntaxError/
31
+ end
32
+
33
+ it "prints error for library with LoadError" do
34
+ Manager.expects(:loader_create).raises(LoadError)
35
+ capture_stderr {
36
+ Manager.load 'blah'
37
+ }.should =~ /Unable to load library blah. Reason: LoadError/
38
+ end
39
+
26
40
  describe "command aliases" do
27
41
  before { eval %[module ::Aquateen; def frylock; end; end] }
28
42
  after { Object.send(:remove_const, "Aquateen") }
@@ -84,7 +98,7 @@ describe "Manager" do
84
98
  end
85
99
  end
86
100
 
87
- describe "loaded" do
101
+ describe ".loaded?" do
88
102
  before { reset_libraries }
89
103
 
90
104
  it "returns false when library isn't loaded" do
@@ -31,6 +31,21 @@ describe "MethodInspector" do
31
31
  parse("options :z=>'b'; def zee; end")[:options].should == {"zee"=>{:z=>'b'}}
32
32
  end
33
33
 
34
+ it "option sets options" do
35
+ parse("option :z, 'b'; option :y, :boolean; def zee; end")[:options].should ==
36
+ {"zee"=>{:z=>'b', :y=>:boolean}}
37
+ end
38
+
39
+ it "option(s) sets options" do
40
+ parse("options :z=>'b'; option :y, :string; def zee; end")[:options].should ==
41
+ {"zee"=>{:z=>'b', :y=>:string}}
42
+ end
43
+
44
+ it "option(s) option overrides options" do
45
+ parse("options :z=>'b'; option :z, :string; def zee; end")[:options].should ==
46
+ {"zee"=>{:z=>:string}}
47
+ end
48
+
34
49
  it "render_options sets render_options" do
35
50
  parse("render_options :z=>true; def zee; end")[:render_options].should == {"zee"=>{:z=>true}}
36
51
  end
@@ -42,7 +57,7 @@ describe "MethodInspector" do
42
57
  it "not all method attributes set causes method_locations to be set" do
43
58
  MethodInspector.stubs(:find_method_locations).returns(["/some/path", 10])
44
59
  parsed = parse "desc 'yo'; def yo; end; options :yep=>1; def yep; end; " +
45
- "render_options :a=>1; config :a=>1; desc 'z'; options :a=>1; def az; end"
60
+ "option :b, :boolean; render_options :a=>1; config :a=>1; desc 'z'; options :a=>1; def az; end"
46
61
  parsed[:method_locations].key?('yo').should == true
47
62
  parsed[:method_locations].key?('yep').should == true
48
63
  parsed[:method_locations].key?('az').should == false
@@ -49,6 +49,15 @@ describe "Pipes" do
49
49
  Pipes.sort_pipe(hashes, :a).should == hashes.reverse
50
50
  end
51
51
 
52
+ it "sorts numeric hash keys by string" do
53
+ hashes = [{2=>'thing'}, {2=>'some'}]
54
+ Pipes.sort_pipe(hashes, '2').should == hashes.reverse
55
+ end
56
+
57
+ it "sorts numeric hash keys by string" do
58
+ Pipes.sort_pipe(@hashes, 'a').should == @hashes.reverse
59
+ end
60
+
52
61
  it "prints error for invalid sort field" do
53
62
  capture_stderr { Pipes.sort_pipe(@objects, :blah)}.should =~ /failed.*'blah'/
54
63
  end
@@ -1,6 +1,5 @@
1
- require 'mocha'
2
1
  require 'bacon'
3
- require File.dirname(__FILE__)+'/bacon_extensions'
2
+ require 'bacon/bits'
4
3
  require 'mocha'
5
4
  require 'mocha-on-bacon'
6
5
  require 'boson'
@@ -119,9 +118,13 @@ module TestHelpers
119
118
  Manager.add_library(lib); lib
120
119
  }
121
120
  end
121
+
122
+ def aborts_with(regex)
123
+ BinRunner.expects(:abort).with {|e| e[regex] }
124
+ yield
125
+ end
122
126
  end
123
127
 
124
128
  class Bacon::Context
125
129
  include TestHelpers
126
- include BaconExtensions
127
- end
130
+ end
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: boson
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 19
4
5
  prerelease: false
5
6
  segments:
6
7
  - 0
7
- - 2
8
- - 5
9
- version: 0.2.5
8
+ - 3
9
+ - 0
10
+ version: 0.3.0
10
11
  platform: ruby
11
12
  authors:
12
13
  - Gabriel Horner
@@ -14,7 +15,7 @@ autorequire:
14
15
  bindir: bin
15
16
  cert_chain: []
16
17
 
17
- date: 2010-06-07 00:00:00 -04:00
18
+ date: 2010-08-13 00:00:00 -04:00
18
19
  default_executable:
19
20
  dependencies:
20
21
  - !ruby/object:Gem::Dependency
@@ -25,6 +26,7 @@ dependencies:
25
26
  requirements:
26
27
  - - ">="
27
28
  - !ruby/object:Gem::Version
29
+ hash: 23
28
30
  segments:
29
31
  - 0
30
32
  - 3
@@ -40,14 +42,73 @@ dependencies:
40
42
  requirements:
41
43
  - - ">="
42
44
  - !ruby/object:Gem::Version
45
+ hash: 19
43
46
  segments:
44
47
  - 0
45
48
  - 2
46
- - 1
47
- version: 0.2.1
49
+ - 2
50
+ version: 0.2.2
48
51
  type: :runtime
49
52
  version_requirements: *id002
50
- description: Boson provides users with the power to turn any ruby method into a full-fledged commandline tool. Boson achieves this with powerful options (borrowed from thor) and views (thanks to hirb). Some other unique features that differentiate it from rake and thor include being accessible from irb and the commandline, being able to write boson commands in non-dsl ruby and toggling a pretty view of a command's output without additional view code.
53
+ - !ruby/object:Gem::Dependency
54
+ name: mocha
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 3
62
+ segments:
63
+ - 0
64
+ version: "0"
65
+ type: :development
66
+ version_requirements: *id003
67
+ - !ruby/object:Gem::Dependency
68
+ name: bacon
69
+ prerelease: false
70
+ requirement: &id004 !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ hash: 19
76
+ segments:
77
+ - 1
78
+ - 1
79
+ - 0
80
+ version: 1.1.0
81
+ type: :development
82
+ version_requirements: *id004
83
+ - !ruby/object:Gem::Dependency
84
+ name: mocha-on-bacon
85
+ prerelease: false
86
+ requirement: &id005 !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ hash: 3
92
+ segments:
93
+ - 0
94
+ version: "0"
95
+ type: :development
96
+ version_requirements: *id005
97
+ - !ruby/object:Gem::Dependency
98
+ name: bacon-bits
99
+ prerelease: false
100
+ requirement: &id006 !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ hash: 3
106
+ segments:
107
+ - 0
108
+ version: "0"
109
+ type: :development
110
+ version_requirements: *id006
111
+ description: Boson is a command/task framework with the power to turn any ruby method into a full-fledged executable with options. Some unique features that differentiate it from rake and thor include being usable from irb and the commandline, optional automated views generated by hirb and allowing libraries to be written as plain ruby. For my libraries that use this, see irbfiles. Works with all major ruby versions.
51
112
  email: gabriel.horner@gmail.com
52
113
  executables:
53
114
  - boson
@@ -91,8 +152,8 @@ files:
91
152
  - lib/boson/view.rb
92
153
  - lib/boson.rb
93
154
  - test/argument_inspector_test.rb
94
- - test/bacon_extensions.rb
95
155
  - test/bin_runner_test.rb
156
+ - test/command_test.rb
96
157
  - test/comment_inspector_test.rb
97
158
  - test/file_library_test.rb
98
159
  - test/loader_test.rb
@@ -111,12 +172,14 @@ files:
111
172
  - LICENSE.txt
112
173
  - CHANGELOG.rdoc
113
174
  - README.rdoc
175
+ - deps.rip
176
+ - test/deps.rip
114
177
  - Rakefile
115
- - gemspec
178
+ - .gemspec
116
179
  has_rdoc: true
117
180
  homepage: http://tagaholic.me/boson/
118
- licenses: []
119
-
181
+ licenses:
182
+ - MIT
120
183
  post_install_message:
121
184
  rdoc_options: []
122
185
 
@@ -127,6 +190,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
127
190
  requirements:
128
191
  - - ">="
129
192
  - !ruby/object:Gem::Version
193
+ hash: 3
130
194
  segments:
131
195
  - 0
132
196
  version: "0"
@@ -135,6 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
199
  requirements:
136
200
  - - ">="
137
201
  - !ruby/object:Gem::Version
202
+ hash: 23
138
203
  segments:
139
204
  - 1
140
205
  - 3
@@ -1,26 +0,0 @@
1
- module BaconExtensions
2
- def self.included(mod)
3
- mod.module_eval do
4
- # nested context methods automatically inherit methods from parent contexts
5
- def describe(*args, &block)
6
- context = Bacon::Context.new(args.join(' '), &block)
7
- (parent_context = self).methods(false).each {|e|
8
- class<<context; self end.send(:define_method, e) {|*args| parent_context.send(e, *args)}
9
- }
10
- @before.each { |b| context.before(&b) }
11
- @after.each { |b| context.after(&b) }
12
- context.run
13
- end
14
- end
15
- end
16
-
17
- def xit(*args); end
18
- def xdescribe(*args); end
19
- def before_all; yield; end
20
- def after_all; yield; end
21
- def assert(description, &block)
22
- it(description) do
23
- block.call.should == true
24
- end
25
- end
26
- end