rubikon 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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