rubikon 0.5.3 → 0.6.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.
@@ -1,16 +1,17 @@
1
1
  # This code is free software; you can redistribute it and/or modify it under
2
2
  # the terms of the new BSD License.
3
3
  #
4
- # Copyright (c) 2009-2010, Sebastian Staudt
4
+ # Copyright (c) 2009-2011, Sebastian Staudt
5
5
 
6
6
  require 'pathname'
7
7
  require 'stringio'
8
8
 
9
9
  require 'rubikon/application/sandbox'
10
+ require 'rubikon/argument_vector'
10
11
  require 'rubikon/colored_io'
11
12
  require 'rubikon/command'
12
13
  require 'rubikon/config/factory'
13
- require 'rubikon/exceptions'
14
+ require 'rubikon/errors'
14
15
  require 'rubikon/flag'
15
16
  require 'rubikon/option'
16
17
  require 'rubikon/progress_bar'
@@ -41,16 +42,17 @@ module Rubikon
41
42
  #
42
43
  # @see #set
43
44
  def initialize
44
- @commands = {}
45
- @current_command = nil
46
- @current_global_param = nil
47
- @current_param = nil
48
- @global_parameters = {}
49
- @hooks = {}
50
- @initialized = false
51
- @parameters = []
52
- @sandbox = Sandbox.new(self)
53
- @settings = {
45
+ @commands = {}
46
+ @current_command = nil
47
+ @current_param = nil
48
+ @default_config = {}
49
+ @global_parameters = {}
50
+ @hooks = {}
51
+ @initialized = false
52
+ @parameters = []
53
+ @sandbox = Sandbox.new(self)
54
+ @settings = {
55
+ :autohelp => true,
54
56
  :autorun => true,
55
57
  :colors => true,
56
58
  :config_file => "#{self.class.to_s.downcase}.yml",
@@ -60,15 +62,17 @@ module Rubikon
60
62
  :raise_errors => false
61
63
  }
62
64
 
63
- @settings[:config_paths] = []
64
65
  if RUBY_PLATFORM.downcase =~ /mswin(?!ce)|mingw|bccwin/
65
- @settings[:config_paths] << ENV['ALLUSERSPROFILE']
66
+ global_config_path = ENV['ALLUSERSPROFILE']
66
67
  else
67
- @settings[:config_paths] << '/etc'
68
+ global_config_path = '/etc'
68
69
  end
69
- @settings[:config_paths] << File.expand_path('~')
70
- @settings[:config_paths] << File.expand_path('.')
71
70
 
71
+ @settings[:config_paths] = [
72
+ global_config_path, File.expand_path('~'), File.expand_path('.')
73
+ ]
74
+
75
+ self.estream = $stderr
72
76
  self.ostream = $stdout
73
77
  end
74
78
 
@@ -86,32 +90,46 @@ module Rubikon
86
90
 
87
91
  begin
88
92
  InstanceMethods.instance_method(:init).bind(self).call
89
- command, parameters, args = InstanceMethods.
93
+ global_params, command, command_params = InstanceMethods.
90
94
  instance_method(:parse_arguments).bind(self).call(args)
91
95
 
92
- parameters.each do |parameter|
93
- @current_global_param = parameter
94
- parameter.send :check_args if parameter.is_a? Option
95
- parameter.send :active!
96
- @current_global_param = nil
97
- end
96
+ @config_factory = Config::Factory.new(@settings[:config_file],
97
+ @settings[:config_paths], @settings[:config_format])
98
+ @config = @default_config.merge @config_factory.config
98
99
 
99
- @config = Config::Factory.new(@settings[:config_file],
100
- @settings[:config_paths], @settings[:config_format]).config
100
+ global_params.each do |param|
101
+ @current_param = param
102
+ param.send :active!
103
+ @current_param = nil
104
+ end
101
105
 
102
106
  @current_command = command
103
107
  hook.call(:pre_execute)
104
- result = command.send(:run, *args)
108
+
109
+ command_params.each do |param|
110
+ @current_param = param
111
+ param.send :active!
112
+ @current_param = nil
113
+ end
114
+
115
+ result = command.send(:run)
105
116
  hook.call(:post_execute)
106
117
  @current_command = nil
107
118
 
108
119
  result
120
+ rescue Interrupt
121
+ error "\nInterrupted... exiting."
109
122
  rescue
110
123
  raise $! if @settings[:raise_errors]
111
124
 
112
- puts "r{Error:}\n #{$!.message}"
113
- puts " at #{$!.backtrace.join("\n at ")}" if $DEBUG
114
- exit 1
125
+ if @settings[:autohelp] && @commands.key?(:help) &&
126
+ $!.is_a?(UnknownCommandError)
127
+ call :help, $!.command
128
+ else
129
+ error "r{Error:}\n #{$!.message}"
130
+ error " at #{$!.backtrace.join("\n at ")}" if $DEBUG
131
+ exit 1
132
+ end
115
133
  ensure
116
134
  InstanceMethods.instance_method(:reset).bind(self).call
117
135
  end
@@ -141,7 +159,7 @@ module Rubikon
141
159
  # enable debug output.
142
160
  # Using it sets Ruby's global variable <tt>$DEBUG</tt> to +true+.
143
161
  #
144
- # @return [Flag]
162
+ # @return [Flag] The debug flag
145
163
  def debug_flag
146
164
  global_flag :debug do
147
165
  $DEBUG = true
@@ -149,6 +167,54 @@ module Rubikon
149
167
  global_flag :d => :debug
150
168
  end
151
169
 
170
+ # Sets the error output stream of the application
171
+ #
172
+ # If colors are enabled, this checks if the stream supports the
173
+ # +color_filter+ method and enables the +ColoredIO+ if not.
174
+ #
175
+ # @param [IO] estream The output stream to use
176
+ # @see ColoredIO.add_color_filter
177
+ # @since 0.6.0
178
+ def estream=(estream)
179
+ if !estream.respond_to?(:color_filter)
180
+ ColoredIO.add_color_filter(estream, @settings[:colors])
181
+ end
182
+ @settings[:estream] = estream
183
+ end
184
+
185
+ # Prints a help screen for this application
186
+ #
187
+ # @param [String] info A additional information string to be displayed
188
+ # right after usage information
189
+ # @since 0.6.0
190
+ def help(info = nil)
191
+ help = {}
192
+ @commands.each_value do |command|
193
+ help[command.name.to_s] = command.description
194
+ end
195
+ help.delete('__default')
196
+
197
+ if @commands.key? :__default
198
+ puts " [command] [args]\n\n"
199
+ else
200
+ puts " command [args]\n\n"
201
+ end
202
+
203
+ puts "#{info}\n\n" unless info.nil?
204
+
205
+ puts 'Commands:'
206
+ max_command_length = help.keys.max_by { |a| a.size }.size
207
+ help.sort_by { |name, description| name }.each do |name, description|
208
+ puts " #{name.ljust(max_command_length)} #{description}"
209
+ end
210
+
211
+ if @commands.key?(:__default) &&
212
+ @commands[:__default].description != '<hidden>'
213
+ put "\nYou can also call this application without a command:"
214
+ puts @commands[:__default].help(false) + "\n"
215
+ end
216
+ end
217
+
152
218
  # Defines a command for displaying a help screen
153
219
  #
154
220
  # This takes any defined commands and it's corresponding options and
@@ -158,16 +224,12 @@ module Rubikon
158
224
  global_parameters = @global_parameters
159
225
  settings = @settings
160
226
 
161
- command :help, nil, 'Display this help screen' do
227
+ command :help, 'Show help for the application or a single command',
228
+ :cmd => :optional do
162
229
  put settings[:help_banner]
163
230
 
164
- help = {}
165
- commands.each_value do |command|
166
- help[command.name.to_s] = command.description
167
- end
168
-
169
231
  global_params = ''
170
- global_parameters.values.uniq.sort {|a,b| a.name.to_s <=> b.name.to_s }.each do |param|
232
+ global_parameters.values.uniq.sort_by { |a| a.name.to_s }.each do |param|
171
233
  global_params << ' ['
172
234
  ([param.name] + param.aliases).each_with_index do |name, index|
173
235
  name = name.to_s
@@ -178,19 +240,19 @@ module Rubikon
178
240
  global_params << ' ...' if param.is_a?(Option)
179
241
  global_params << ']'
180
242
  end
243
+ put global_params
181
244
 
182
- default_description = help.delete('__default')
183
- if default_description.nil?
184
- puts "#{global_params} command [args]\n\n"
185
- else
186
- puts "#{global_params} [command] [args]\n\n"
187
- puts "Without command: #{default_description}\n\n"
188
- end
245
+ app_help = lambda { |info| @__app__.instance_eval { help(info) } }
189
246
 
190
- puts 'Commands:'
191
- max_command_length = help.keys.max { |a, b| a.size <=> b.size }.size
192
- help.sort_by { |name, description| name }.each do |name, description|
193
- puts " #{name.ljust(max_command_length)} #{description}"
247
+ unless cmd.nil?
248
+ name = cmd.to_sym
249
+ if commands.key? name
250
+ puts commands[name].help
251
+ else
252
+ app_help.call("The command \"#{cmd}\" is undefined. The following commands are available:")
253
+ end
254
+ else
255
+ app_help.call(nil)
194
256
  end
195
257
  end
196
258
  end
@@ -240,7 +302,8 @@ module Rubikon
240
302
  InstanceMethods.instance_method(:help_command).bind(self).call
241
303
  InstanceMethods.instance_method(:verbose_flag).bind(self).call
242
304
 
243
- if @settings[:help_as_default] && !@commands.keys.include?(:__default)
305
+ if @settings[:help_as_default] && @commands.key?(:help) &&
306
+ !@commands.key?(:__default)
244
307
  default :help
245
308
  end
246
309
 
@@ -267,7 +330,7 @@ module Rubikon
267
330
  # end
268
331
  # @since 0.4.0
269
332
  def method_missing(name, *args, &block)
270
- receiver = @current_param || @current_global_param || @current_command
333
+ receiver = @current_param || @current_command
271
334
  if receiver.nil? || (!receiver.respond_to?(name) &&
272
335
  !receiver.public_methods(false).include?(name))
273
336
  super
@@ -294,55 +357,40 @@ module Rubikon
294
357
  # user. This distinguishes between commands, global flags and command
295
358
  # flags
296
359
  #
297
- # @param [Array] args The command-line arguments
298
- # @return [Command, Array<Symbol>, Array] The command to execute, the
299
- # parameters of this command that have been supplied and any
300
- # additional command-line arguments supplied
301
- def parse_arguments(args)
302
- command_arg = args.find { |arg| arg == '--' || !arg.start_with?('-') }
303
- command_arg = nil if command_arg == '--'
304
-
305
- if command_arg.nil?
306
- command = @commands[:__default]
307
- raise NoDefaultCommandError if command.nil?
308
- else
309
- command = @commands[command_arg.to_sym]
310
- args.delete_at args.index(command_arg)
311
- raise UnknownCommandError.new(command_arg) if command.nil?
312
- end
313
-
314
- args.delete '--'
315
- args = args.map do |arg|
316
- if !arg.start_with?('--') && arg.start_with?('-') && arg.size > 2
317
- arg[1..-1].split('').map { |a| "-#{a}" }
318
- else
319
- arg
320
- end
321
- end.flatten
322
-
323
- parameter = nil
324
- parameters = []
325
- args.dup.each do |arg|
326
- if arg.start_with?('--')
327
- parameter = @global_parameters[arg[2..-1].to_sym]
328
- elsif arg.start_with?('-')
329
- parameter = @global_parameters[arg[1..-1].to_sym]
360
+ # @param [String] argv The command-line arguments
361
+ # @raise [NoDefaultCommandError] if no command can be found and no
362
+ # default command exists
363
+ # @raise [UnknownParameterError] if an unknown parameter is found
364
+ # @raise [UnknownParameterError] if an unknown command is found
365
+ # @return [Array<Parameter>] one All global parameters that have been
366
+ # supplied
367
+ # @return [Command] two The command to execute, the parameters of this
368
+ # command that have been supplied
369
+ # @return [Array<Parameter>] three All parameters of that command that have
370
+ # been supplied
371
+ def parse_arguments(argv)
372
+ argv.extend ArgumentVector
373
+
374
+ argv.expand!
375
+
376
+ command, command_index = argv.command! @commands
377
+ raise NoDefaultCommandError if command.nil?
378
+
379
+ command_params = argv.params! command.params, command_index
380
+ global_params = argv.params! @global_parameters
381
+
382
+ argv.scoped_args! command
383
+
384
+ unless argv.empty?
385
+ first = argv.first
386
+ if first.start_with? '-'
387
+ raise UnknownParameterError.new(first)
330
388
  else
331
- if !parameter.nil? && parameter.send(:more_args?)
332
- parameter.args << args.delete(arg)
333
- else
334
- parameter = nil
335
- end
336
- next
337
- end
338
-
339
- unless parameter.nil?
340
- parameters << parameter
341
- args.delete(arg)
389
+ raise UnknownCommandError.new(first)
342
390
  end
343
391
  end
344
392
 
345
- return command, parameters, args
393
+ return global_params, command, command_params
346
394
  end
347
395
 
348
396
  # Resets this application to its initial state
@@ -357,8 +405,10 @@ module Rubikon
357
405
  # @see Parameter#reset
358
406
  # @since 0.4.0
359
407
  def reset
360
- ostream.rewind if ostream.is_a? StringIO || !ostream.stat.chardev?
361
- ColoredIO.remove_color_filter(ostream)
408
+ [estream, ostream].each do |stream|
409
+ stream.rewind if stream.is_a? StringIO || !stream.stat.chardev?
410
+ ColoredIO.remove_color_filter(estream)
411
+ end
362
412
  (@commands.values + @global_parameters.values).uniq.each do |param|
363
413
  param.send :reset
364
414
  end
@@ -370,7 +420,7 @@ module Rubikon
370
420
  # verbose output.
371
421
  # Using it sets Ruby's global variable <tt>$VERBOSE</tt> to +true+.
372
422
  #
373
- # @return [Flag] The debug Flag object
423
+ # @return [Flag] The verbose Flag object
374
424
  def verbose_flag
375
425
  global_flag :verbose do
376
426
  $VERBOSE = true
File without changes
@@ -0,0 +1,119 @@
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) 2011, Sebastian Staudt
5
+
6
+ module Rubikon
7
+
8
+ # This module will extend the argument array passed to the application,
9
+ # usually ARGV. It provides functionality to parse Rubikon specific tokens
10
+ # from the strings contained in the argument list passed to the application.
11
+ #
12
+ # @author Sebastian Staudt
13
+ # @since 0.6.0
14
+ module ArgumentVector
15
+
16
+ # Gets the command to use from the list of arguments passed to the
17
+ # application. The first argument matching a command name or alias will
18
+ # cause the corresponding command to be selected.
19
+ #
20
+ # The command and all arguments equal to '--' will be removed from the
21
+ # array.
22
+ #
23
+ # @param [Hash<Symbol, Command>] commands A list of available commands
24
+ # @return [Command] The command found in the argument list
25
+ # @return [Fixnum] The position of the command in the argument list
26
+ def command!(commands)
27
+ command = nil
28
+ command_index = 0
29
+ each_with_index do |arg, i|
30
+ break if arg == '--'
31
+
32
+ command = commands[arg.to_sym]
33
+ unless command.nil?
34
+ command_index = i
35
+ delete_at i
36
+ break
37
+ end
38
+ end
39
+ delete '--'
40
+
41
+ command ||= commands[:__default]
42
+
43
+ return command, command_index
44
+ end
45
+
46
+ # Turns arguments using a special syntax into arguments that are parseable.
47
+ #
48
+ # Single character parameters may be joined together like '-dv'. This
49
+ # method will split them into separate parameters like '-d -v'.
50
+ #
51
+ # Additionally a parameter argument may be attached to the parameter itself
52
+ # using '=' like '--path=/tmp'. This method will also split them into
53
+ # '--path /tmp'.
54
+ def expand!
55
+ each_with_index do |arg, i|
56
+ next if !arg.start_with?('-')
57
+ self[i] = arg.split('=', 2)
58
+ next if arg.start_with?('--')
59
+ self[i] = arg[1..-1].split('').uniq.map { |a| '-' + a }
60
+ end
61
+ flatten!
62
+ end
63
+
64
+ # Selects active parameters from a list of available parameters
65
+ #
66
+ # For every option found in the argument list {#scoped_args!} is called to
67
+ # find the arguments for that option.
68
+ #
69
+ # All parameters found will be removed from the array.
70
+ #
71
+ # @param [Hash<Symbol, Parameter>] params A list of available parameters
72
+ # @param [Fixnum] pos The position of the first argument that should be
73
+ # checked. All arguments ahead of that position will be skipped.
74
+ # @return [Array<Parameter>] Parameters called from the given argument list
75
+ # @see #scoped_args
76
+ def params!(params, pos = 0)
77
+ active_params = []
78
+ to_delete = []
79
+ each_with_index do |arg, i|
80
+ next if i < pos || arg.nil? || !arg.start_with?('-')
81
+
82
+ param = params[(arg.start_with?('--') ? arg[2..-1] : arg[1..1]).to_sym]
83
+ unless param.nil?
84
+ to_delete << i
85
+ scoped_args! param, i + 1 if param.is_a? Option
86
+ active_params << param
87
+ end
88
+ end
89
+
90
+ to_delete.reverse.each { |i| delete_at i }
91
+
92
+ active_params
93
+ end
94
+
95
+ # Gets all arguments passed to a specific scope, i.e. a command or an
96
+ # option.
97
+ #
98
+ # All arguments in the scope will be removed from the array.
99
+ #
100
+ # @param [HasArguments] has_args
101
+ # @param [Fixnum] pos The position of the first argument that should be
102
+ # checked. All arguments ahead of that position will be skipped.
103
+ def scoped_args!(has_args, pos = 0)
104
+ to_delete = []
105
+
106
+ each_with_index do |arg, i|
107
+ next if i < pos
108
+ break if arg.start_with?('-') || !has_args.send(:more_args?)
109
+
110
+ to_delete << i
111
+ has_args.send(:<<, arg)
112
+ end
113
+
114
+ to_delete.reverse.each { |i| delete_at i }
115
+ end
116
+
117
+ end
118
+
119
+ end