rubikon 0.5.3 → 0.6.0

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