rubikon 0.4.1 → 0.5.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/README.md CHANGED
@@ -93,14 +93,15 @@ section if you want to help to make Rubikon better.
93
93
  * Automatic checks for option arguments
94
94
  * Built-in methods to capture user input
95
95
  * Built-in methods to display progress bars and throbbers
96
+ * Built-in support for configuration files
97
+ * Built-in support for colored output
98
+ * Automatic generation of a application help screen
96
99
 
97
100
  ## Future plans
98
101
 
99
102
  * User defined type safety of option arguments
100
- * Automatic generation of help screens
101
103
  * Improved error handling
102
- * Built-in support for configuration files
103
- * Built-in support for colored output
104
+ * Automatic generation of command help screens
104
105
 
105
106
  ## Requirements
106
107
 
@@ -5,7 +5,8 @@
5
5
 
6
6
  unless Object.method_defined?(:respond_to_missing?)
7
7
 
8
- # Extends Ruby's own Object class with method #start_with? for Ruby < 1.9.2
8
+ # Extends Ruby's own Object class with method #respond_to_missing? for Ruby
9
+ # < 1.9.2
9
10
  #
10
11
  # @author Sebastian Staudt
11
12
  # @since 0.4.0
data/lib/rubikon.rb CHANGED
@@ -24,6 +24,6 @@ require 'rubikon/application/base'
24
24
  module Rubikon
25
25
 
26
26
  # This is the current version of the Rubikon gem
27
- VERSION = '0.4.1'
27
+ VERSION = '0.5.0'
28
28
 
29
29
  end
@@ -34,7 +34,11 @@ module Rubikon
34
34
  def inherited(subclass)
35
35
  subclass.class_eval { include Singleton }
36
36
  subclass.send(:base_file=, File.expand_path(caller.first.split(':').first))
37
- at_exit { subclass.run if subclass.send(:autorun?) }
37
+ at_exit do
38
+ if subclass.send(:autorun?)
39
+ InstanceMethods.instance_method(:run).bind(subclass.instance).call
40
+ end
41
+ end
38
42
  end
39
43
 
40
44
  # This is used for convinience. Method calls on the class itself are
@@ -19,11 +19,25 @@ module Rubikon
19
19
  # @return [String] The (first) definition file of the application
20
20
  attr_reader :base_file
21
21
 
22
+ attr_reader :config
23
+
22
24
  # @return [String] The absolute path of the application
23
25
  attr_reader :path
24
26
 
25
27
  private
26
28
 
29
+ # Call another named command with the given arguments
30
+ #
31
+ # @param [Symbol] command_name The name of the command to call
32
+ # @param [Array<String>] args The arguments to pass to the called command
33
+ # @see Command#run
34
+ def call(command_name, *args)
35
+ command = @current_command
36
+ @current_command = @commands[command_name]
37
+ @current_command.send(:run, *args)
38
+ @current_command = command
39
+ end
40
+
27
41
  # Define a new application Command or an alias to an existing one
28
42
  #
29
43
  # @param [String, Hash] name The name of the Command as used in
@@ -39,6 +53,8 @@ module Rubikon
39
53
  # @return [Command]
40
54
  # @since 0.2.0
41
55
  def command(name, arg_count = nil, description = nil, &block)
56
+ command = nil
57
+
42
58
  if name.is_a? Hash
43
59
  name.each do |alias_name, command_name|
44
60
  command = @commands[command_name]
@@ -63,7 +79,7 @@ module Rubikon
63
79
 
64
80
  unless command.nil? || @parameters.empty?
65
81
  @parameters.each do |parameter|
66
- command.add_param(parameter)
82
+ command.send(:add_param, parameter)
67
83
  end
68
84
  @parameters.clear
69
85
  end
@@ -139,7 +155,7 @@ module Rubikon
139
155
  parameter = @global_parameters[name]
140
156
  parameter = @current_command.parameters[name] if parameter.nil?
141
157
  return false if parameter.nil?
142
- parameter.active?
158
+ parameter.send(:active?)
143
159
  end
144
160
  alias_method :given?, :active?
145
161
 
@@ -365,8 +381,8 @@ module Rubikon
365
381
  # @param [String] text The text to write into the output stream
366
382
  # @since 0.2.0
367
383
  def put(text)
368
- @settings[:ostream] << text
369
- @settings[:ostream].flush
384
+ ostream << text
385
+ ostream.flush
370
386
  end
371
387
 
372
388
  # Output a character using +IO#putc+ of the output stream
@@ -375,7 +391,7 @@ module Rubikon
375
391
  # stream
376
392
  # @since 0.2.0
377
393
  def putc(char)
378
- @settings[:ostream].putc char
394
+ ostream.putc char
379
395
  end
380
396
 
381
397
  # Output a line of text using +IO#puts+ of the output stream
@@ -383,7 +399,7 @@ module Rubikon
383
399
  # @param [String] text The text to write into the output stream
384
400
  # @since 0.2.0
385
401
  def puts(text)
386
- @settings[:ostream].puts text
402
+ ostream.puts text
387
403
  end
388
404
 
389
405
  # Sets an application setting
@@ -393,20 +409,28 @@ module Rubikon
393
409
  # @since 0.2.0
394
410
  #
395
411
  # Available settings
396
- # +autorun+:: If true, let the application run as soon as its
397
- # class is defined
398
- # +help_banner+:: Defines a banner for the help message
399
- # (<em>unused</em>)
400
- # +istream+:: Defines an input stream to use
401
- # +name+:: Defines the name of the application
402
- # +ostream+:: Defines an output stream to use
403
- # +raise_errors+:: If true, raise errors, otherwise fail gracefully
412
+ # +autorun+:: If +true+, let the application run as soon as its
413
+ # class is defined. This is generally useful for simple
414
+ # "code and run" applications.
415
+ # +colors+:: If +true+, enables colored output using ColoredIO
416
+ # +config_file+:: The name of the config file to search
417
+ # +config_paths+:: The paths to search for config files
418
+ # +help_banner+:: Defines a banner for the help message
419
+ # +istream+:: Defines an input stream to use
420
+ # +name+:: Defines the name of the application
421
+ # +ostream+:: Defines an output stream to use
422
+ # +raise_errors+:: If +true+, raise errors, otherwise fail gracefully
404
423
  #
405
424
  # @example
406
425
  # set :name, 'My App'
407
426
  # set :autorun, false
408
427
  def set(setting, value)
409
- @settings[setting.to_sym] = value
428
+ setting = setting.to_sym
429
+ unless setting == :ostream
430
+ @settings[setting.to_sym] = value
431
+ else
432
+ self.ostream = value
433
+ end
410
434
  end
411
435
 
412
436
  # Displays a throbber while the given block is executed
@@ -7,7 +7,9 @@ require 'pathname'
7
7
  require 'stringio'
8
8
 
9
9
  require 'rubikon/application/sandbox'
10
+ require 'rubikon/colored_io'
10
11
  require 'rubikon/command'
12
+ require 'rubikon/config/factory'
11
13
  require 'rubikon/exceptions'
12
14
  require 'rubikon/flag'
13
15
  require 'rubikon/option'
@@ -50,12 +52,24 @@ module Rubikon
50
52
  @sandbox = Sandbox.new(self)
51
53
  @settings = {
52
54
  :autorun => true,
55
+ :colors => true,
56
+ :config_file => "#{self.class.to_s.downcase}.yml",
53
57
  :help_as_default => true,
54
58
  :istream => $stdin,
55
59
  :name => self.class.to_s,
56
- :ostream => $stdout,
57
60
  :raise_errors => false
58
61
  }
62
+
63
+ @settings[:config_paths] = []
64
+ if RUBY_PLATFORM.downcase =~ /mswin(?!ce)|mingw|bccwin/
65
+ @settings[:config_paths] << ENV['ALLUSERSPROFILE']
66
+ else
67
+ @settings[:config_paths] << '/etc'
68
+ end
69
+ @settings[:config_paths] << File.expand_path('~')
70
+ @settings[:config_paths] << File.expand_path('.')
71
+
72
+ self.ostream = $stdout
59
73
  end
60
74
 
61
75
  # Run this application
@@ -68,33 +82,38 @@ module Rubikon
68
82
  # @param [Array<String>] args The command line arguments that should be
69
83
  # given to the application as options
70
84
  def run(args = ARGV)
85
+ hook = InstanceMethods.instance_method(:hook).bind(self)
86
+
71
87
  begin
72
- init
73
- command, parameters, args = parse_arguments(args)
88
+ InstanceMethods.instance_method(:init).bind(self).call
89
+ command, parameters, args = InstanceMethods.
90
+ instance_method(:parse_arguments).bind(self).call(args)
74
91
 
75
92
  parameters.each do |parameter|
76
93
  @current_global_param = parameter
77
- if parameter.is_a? Option
78
- parameter.check_args
79
- end
80
- parameter.active!
94
+ parameter.send :check_args if parameter.is_a? Option
95
+ parameter.send :active!
81
96
  @current_global_param = nil
82
97
  end
83
98
 
99
+ @config = Config::Factory.new(@settings[:config_file],
100
+ @settings[:config_paths], @settings[:config_format]).config
101
+
84
102
  @current_command = command
85
- hook :pre_execute
86
- result = command.run(*args)
87
- hook :post_execute
103
+ hook.call(:pre_execute)
104
+ result = command.send(:run, *args)
105
+ hook.call(:post_execute)
88
106
  @current_command = nil
89
107
 
90
- reset
91
108
  result
92
109
  rescue
93
110
  raise $! if @settings[:raise_errors]
94
111
 
95
- puts "Error:\n #{$!.message}"
112
+ puts "r{Error:}\n #{$!.message}"
96
113
  puts " #{$!.backtrace.join("\n ")}" if $DEBUG
97
114
  exit 1
115
+ ensure
116
+ InstanceMethods.instance_method(:reset).bind(self).call
98
117
  end
99
118
  end
100
119
 
@@ -185,13 +204,13 @@ module Rubikon
185
204
  # If the block needs to print to the real IO stream, it can access it
186
205
  # using its first parameter.
187
206
  def hidden_output(&block)
188
- current_ostream = @settings[:ostream]
189
- @settings[:ostream] = StringIO.new
207
+ current_ostream = ostream
208
+ self.ostream = StringIO.new
190
209
 
191
210
  block.call(current_ostream)
192
211
 
193
- current_ostream << @settings[:ostream].string
194
- @settings[:ostream] = current_ostream
212
+ current_ostream << ostream.string
213
+ self.ostream = current_ostream
195
214
  end
196
215
 
197
216
  # Executes the hook with the secified name
@@ -199,7 +218,7 @@ module Rubikon
199
218
  # @param [Symbol] name The name of the hook to execute
200
219
  # @since 0.4.0
201
220
  def hook(name)
202
- @sandbox.instance_eval &@hooks[name] unless @hooks[name].nil?
221
+ @sandbox.instance_eval(&@hooks[name]) unless @hooks[name].nil?
203
222
  end
204
223
 
205
224
  # This method is called once for each application and is used to
@@ -209,15 +228,17 @@ module Rubikon
209
228
  def init
210
229
  return if @initialized
211
230
 
231
+ hook = InstanceMethods.instance_method(:hook).bind(self)
232
+
212
233
  @current_command = nil
213
234
  @current_param = nil
214
235
  @current_global_param = nil
215
236
 
216
- hook :pre_init
237
+ hook.call(:pre_init)
217
238
 
218
- debug_flag
219
- help_command
220
- verbose_flag
239
+ InstanceMethods.instance_method(:debug_flag).bind(self).call
240
+ InstanceMethods.instance_method(:help_command).bind(self).call
241
+ InstanceMethods.instance_method(:verbose_flag).bind(self).call
221
242
 
222
243
  if @settings[:help_as_default] && !@commands.keys.include?(:__default)
223
244
  default :help
@@ -225,7 +246,7 @@ module Rubikon
225
246
 
226
247
  @initialized = true
227
248
 
228
- hook :post_init
249
+ hook.call(:post_init)
229
250
  end
230
251
 
231
252
  # This is used to determine the receiver of a method call inside the
@@ -247,13 +268,28 @@ module Rubikon
247
268
  # @since 0.4.0
248
269
  def method_missing(name, *args, &block)
249
270
  receiver = @current_param || @current_global_param || @current_command
250
- if receiver.nil? || !receiver.respond_to?(name)
271
+ if receiver.nil? || (!receiver.respond_to?(name) &&
272
+ !receiver.public_methods(false).include?(name))
251
273
  super
252
274
  else
253
275
  receiver.send(name, *args, &block)
254
276
  end
255
277
  end
256
278
 
279
+ # Sets the output stream of the application
280
+ #
281
+ # If colors are enabled, this checks if the stream supports the
282
+ # +color_filter+ method and enables the +ColoredIO+ if not.
283
+ #
284
+ # @param [IO] ostream The output stream to use
285
+ # @see ColoredIO.add_color_filter
286
+ def ostream=(ostream)
287
+ if !ostream.respond_to?(:color_filter)
288
+ ColoredIO.add_color_filter(ostream, @settings[:colors])
289
+ end
290
+ @settings[:ostream] = ostream
291
+ end
292
+
257
293
  # Parses the command-line arguments given to the application by the
258
294
  # user. This distinguishes between commands, global flags and command
259
295
  # flags
@@ -263,16 +299,20 @@ module Rubikon
263
299
  # parameters of this command that have been supplied and any
264
300
  # additional command-line arguments supplied
265
301
  def parse_arguments(args)
266
- command_arg = args.shift
267
- if command_arg.nil? || command_arg.start_with?('-')
302
+ command_arg = args.find { |arg| arg == '--' || !arg.start_with?('-') }
303
+ command_arg = nil if command_arg == '--'
304
+
305
+ if command_arg.nil?
268
306
  command = @commands[:__default]
269
- args.unshift(command_arg) unless command_arg.nil?
270
307
  raise NoDefaultCommandError if command.nil?
271
308
  else
272
309
  command = @commands[command_arg.to_sym]
310
+ args.delete_at args.find_index(command_arg)
273
311
  raise UnknownCommandError.new(command_arg) if command.nil?
274
312
  end
275
313
 
314
+ args.delete '--'
315
+
276
316
  parameter = nil
277
317
  parameters = []
278
318
  args.dup.each do |arg|
@@ -281,7 +321,7 @@ module Rubikon
281
321
  elsif arg.start_with?('-')
282
322
  parameter = @global_parameters[arg[1..-1].to_sym]
283
323
  else
284
- if !parameter.nil? && parameter.more_args?
324
+ if !parameter.nil? && parameter.send(:more_args?)
285
325
  parameter.args << args.delete(arg)
286
326
  else
287
327
  parameter = nil
@@ -300,13 +340,20 @@ module Rubikon
300
340
 
301
341
  # Resets this application to its initial state
302
342
  #
343
+ # This rewinds the output stream, removes the color features from the and
344
+ # resets all commands and global parameters.
345
+ #
346
+ # @see ColoredIO.remove_color_filter
303
347
  # @see Command#reset
304
348
  # @see HasArguments#reset
349
+ # @see IO#rewind
305
350
  # @see Parameter#reset
306
351
  # @since 0.4.0
307
352
  def reset
353
+ ostream.rewind
354
+ ColoredIO.remove_color_filter(ostream)
308
355
  (@commands.values + @global_parameters.values).uniq.each do |param|
309
- param.reset
356
+ param.send :reset
310
357
  end
311
358
  end
312
359
 
@@ -35,11 +35,13 @@ module Rubikon
35
35
  # InstanceMethods and should therefore be protected
36
36
  # @see InstanceMethods
37
37
  def method_missing(name, *args, &block)
38
- if InstanceMethods.method_defined?(name) ||
39
- InstanceMethods.private_method_defined?(name)
38
+ if @__app__.class.instance_methods(false).include?(name.to_s) ||
39
+ !(InstanceMethods.method_defined?(name) ||
40
+ InstanceMethods.private_method_defined?(name))
41
+ @__app__.send(name, *args, &block)
42
+ else
40
43
  raise NoMethodError.new("Method `#{name}' is protected by the application sandbox", name)
41
44
  end
42
- @__app__.send(name, *args, &block)
43
45
  end
44
46
 
45
47
  # Relay putc to the instance method
@@ -0,0 +1,105 @@
1
+ # This code is free software; you can redistribute it and/or modify it under
2
+ # the terms of the new BSD License.
3
+ #
4
+ # Copyright (c) 2010, Sebastian Staudt
5
+
6
+ module Rubikon
7
+
8
+ # This module is used to enhance an IO stream to generate terminal
9
+ # color codes from simple text tags.
10
+ #
11
+ # @author Sebastian Staudt
12
+ # @since 0.5.0
13
+ module ColoredIO
14
+
15
+ # Color codes that can be used inside a IO enhanced with ColoredIO
16
+ COLORS = {
17
+ :black => 30,
18
+ :bl => 30,
19
+ :red => 31,
20
+ :r => 31,
21
+ :green => 32,
22
+ :g => 32,
23
+ :yellow => 33,
24
+ :y => 33,
25
+ :blue => 34,
26
+ :b => 34,
27
+ :purple => 35,
28
+ :p => 35,
29
+ :cyan => 37,
30
+ :c => 36,
31
+ :white => 37,
32
+ :w => 37,
33
+ }
34
+
35
+ # The keys of the color codes joined for the regular expression
36
+ COLOR_MATCHER = COLORS.keys.join('|')
37
+
38
+ # Enables color filtering on the given output stream (or another object
39
+ # responding to +puts+)
40
+ #
41
+ # This wraps the IO's +puts+ method into a call to +color_filter+. The
42
+ # +color_filter+ method is added dynamically to the singleton class of the
43
+ # object and does either turn color tags given to the output stream into
44
+ # their corresponding color code or it simply removes the color tags, if
45
+ # coloring is disabled.
46
+ #
47
+ # @param [IO] io The IO object to add color filtering to
48
+ # @raise TypeError if the given object does not respond to +puts+
49
+ # @see .remove_color_filter
50
+ def self.add_color_filter(io, enabled = true)
51
+ raise TypeError unless io.respond_to? :puts
52
+ return if io.respond_to?(:color_filter)
53
+
54
+ enabled = enabled && ENV['TERM'] != 'dumb'
55
+ if enabled && RUBY_PLATFORM.downcase =~ /mswin(?!ce)|mingw|bccwin/
56
+ begin
57
+ require 'Win32/Console/ANSI'
58
+ rescue LoadError
59
+ enabled = false
60
+ end
61
+ end
62
+
63
+ class << io
64
+ const_set :COLORS, COLORS
65
+
66
+ def puts(text = '')
67
+ super color_filter(text.to_s)
68
+ end
69
+ end
70
+
71
+ if enabled
72
+ class << io
73
+ def color_filter(text)
74
+ text.gsub(/(#{COLOR_MATCHER})\{(.*?)\}/i) do
75
+ "\e[0;#{COLORS[$1.downcase.to_sym]}m#{$2}\e[0m"
76
+ end
77
+ end
78
+ end
79
+ else
80
+ class << io
81
+ def color_filter(text)
82
+ text.gsub(/(#{COLOR_MATCHER})\{(.*?)\}/i, '\2')
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # Disables color filtering on the given output stream
89
+ #
90
+ # This reverts the actions of +add_color_filter+
91
+ #
92
+ # @param [IO] io The IO object to remove color filtering from
93
+ # @see .add_color_filter
94
+ def self.remove_color_filter(io)
95
+ return unless io.respond_to?(:color_filter)
96
+ class << io
97
+ remove_const :COLORS
98
+ remove_method :color_filter
99
+ remove_method :puts
100
+ end
101
+ end
102
+
103
+ end
104
+
105
+ end