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,10 +1,10 @@
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) 2010, Sebastian Staudt
4
+ # Copyright (c) 2010-2011, Sebastian Staudt
5
5
 
6
6
  require 'rubikon/application/base'
7
- require 'rubikon/exceptions'
7
+ require 'rubikon/errors'
8
8
  require 'rubikon/has_arguments'
9
9
  require 'rubikon/parameter'
10
10
 
@@ -34,16 +34,14 @@ module Rubikon
34
34
  # @param [Application::Base] app The application this command belongs to
35
35
  # @param [Symbol, #to_sym] name The name of this command, used in application
36
36
  # arguments
37
- # @param [Range, Array, Numeric] arg_count The number of arguments this
38
- # command takes.
37
+ # @param options (see HasArguments#initialize)
39
38
  # @param [Proc] block The code block which should be executed by this
40
39
  # command
41
40
  # @raise [ArgumentError] if the given application object isn't a Rubikon
42
41
  # application
43
42
  # @raise [BlockMissingError] if no command code block is given and a
44
43
  # command file does not exist
45
- # @see HasArguments#arg_count=
46
- def initialize(app, name, arg_count = nil, &block)
44
+ def initialize(app, name, *options, &block)
47
45
  super
48
46
 
49
47
  @params = {}
@@ -58,8 +56,81 @@ module Rubikon
58
56
  end
59
57
  end
60
58
 
59
+ # Generate help for this command
60
+ #
61
+ # @param [Boolean] show_usage If +true+, the returned String will also
62
+ # include usage information
63
+ # @return [String] The contents of the help screen for this command
64
+ # @since 0.6.0
65
+ def help(show_usage = true)
66
+ help = ''
67
+
68
+ if show_usage
69
+ help << " #{name}" if name != :__default
70
+
71
+ @params.values.uniq.sort_by {|a| a.name.to_s }.each do |param|
72
+ help << ' ['
73
+ ([param.name] + param.aliases).each_with_index do |name, index|
74
+ name = name.to_s
75
+ help << '|' if index > 0
76
+ help << '-' if name.size > 1
77
+ help << "-#{name}"
78
+ end
79
+ help << ' ...' if param.is_a?(Option)
80
+ help << ']'
81
+ end
82
+ end
83
+
84
+ help << "\n\n#{description}" unless description.nil?
85
+
86
+ help_flags = {}
87
+ help_options = {}
88
+ params.each_value do |param|
89
+ if param.is_a? Flag
90
+ help_flags[param.name.to_s] = param
91
+ else
92
+ help_options[param.name.to_s] = param
93
+ end
94
+ end
95
+
96
+ param_name = lambda { |name| "#{name.size > 1 ? '-' : ' '}-#{name}" }
97
+ unless help_flags.empty? && help_options.empty?
98
+ max_param_length = (help_flags.keys + help_options.keys).
99
+ max_by { |a| a.size }.size + 2
100
+ end
101
+
102
+ unless help_flags.empty?
103
+ help << "\n\nFlags:"
104
+ help_flags.sort_by { |name, param| name }.each do |name, param|
105
+ help << "\n #{param_name.call(name).ljust(max_param_length)}"
106
+ help << " #{param.description}" unless param.description.nil?
107
+ end
108
+ end
109
+
110
+ unless help_options.empty?
111
+ help << "\n\nOptions:\n"
112
+ help_options.sort_by { |name, param| name }.each do |name, param|
113
+ help << " #{param_name.call(name).ljust(max_param_length)} ..."
114
+ help << " #{param.description}" unless param.description.nil?
115
+ help << "\n"
116
+ end
117
+ end
118
+
119
+ help
120
+ end
121
+
61
122
  private
62
123
 
124
+ # Returns all parameters of this command that are active, i.e. that have
125
+ # been supplied on the command-line
126
+ #
127
+ # @return [Array<Parameter>] All currently active parameters of this
128
+ # command
129
+ # @since 0.6.0
130
+ def active_params
131
+ @params.values.select { |param| param.active? }
132
+ end
133
+
63
134
  # Add a new parameter for this command
64
135
  #
65
136
  # @param [Parameter, Hash] parameter The parameter to add to this
@@ -96,7 +167,6 @@ module Rubikon
96
167
  # method will return the value of the parameter.
97
168
  #
98
169
  # @param (see ClassMethods#method_missing)
99
- # @see DSLMethods#params
100
170
  #
101
171
  # @example
102
172
  # option :user, [:who]
@@ -105,52 +175,17 @@ module Rubikon
105
175
  # puts "I feel #{mood}"
106
176
  # end
107
177
  def method_missing(name, *args, &block)
108
- if args.empty? && !block_given? && @params.key?(name)
109
- @params[name]
110
- else
111
- super
112
- end
113
- end
114
-
115
- # Parses the arguments of this command and sets each Parameter as active
116
- # if it has been supplied by the user on the command-line. Additional
117
- # arguments are passed to the individual parameters.
118
- #
119
- # @param [Array<String>] args The arguments that have been passed to this
120
- # command
121
- # @raise [UnknownParameterError] if an undefined parameter is passed to the
122
- # command
123
- # @see Flag
124
- # @see Option
125
- def parse_arguments(args)
126
- current_param = Application::InstanceMethods.
127
- instance_method(:current_param).bind(@app)
128
- set_current_param = Application::InstanceMethods.
129
- instance_method(:current_param=).bind(@app)
130
-
131
- @args = []
132
- args.each do |arg|
133
- if arg.start_with?('-')
134
- parameter_name = arg.start_with?('--') ? arg[2..-1] : arg[1..-1]
135
- parameter = @params[parameter_name.to_sym]
136
- raise UnknownParameterError.new(arg) if parameter.nil?
137
- end
138
-
139
- unless parameter.nil?
140
- current_param.call.send(:active!) unless current_param.call.nil?
141
- set_current_param.call(parameter)
142
- next
143
- end
144
-
145
- if current_param.call.nil? || !current_param.call.send(:more_args?)
146
- self << arg
178
+ if args.empty? && !block_given?
179
+ if @params.key?(name)
180
+ return @params[name]
147
181
  else
148
- current_param.call.send(:<<, arg)
182
+ active_params.each do |param|
183
+ return param.send(name) if param.respond_to_missing?(name)
184
+ end
149
185
  end
150
186
  end
151
187
 
152
- current_param.call.send(:active!) unless current_param.call.nil?
153
- set_current_param.call(nil)
188
+ super
154
189
  end
155
190
 
156
191
  # Resets this command to its initial state
@@ -172,15 +207,13 @@ module Rubikon
172
207
  # @return +true+ if named parameter with the specified name exists
173
208
  # @see #method_missing
174
209
  def respond_to_missing?(name, include_private = false)
175
- @params.key?(name) || super
210
+ @params.key?(name) ||
211
+ active_params.any? { |param| param.respond_to_missing?(name) } ||
212
+ super
176
213
  end
177
214
 
178
215
  # Run this command's code block
179
- #
180
- # @param [Array<String>] args The arguments that have been passed to this
181
- # command
182
- def run(*args)
183
- parse_arguments(args)
216
+ def run
184
217
  check_args
185
218
  Application::InstanceMethods.instance_method(:sandbox).bind(@app).call.
186
219
  instance_eval(&@block)
@@ -1,7 +1,7 @@
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) 2010, Sebastian Staudt
4
+ # Copyright (c) 2010-2011, Sebastian Staudt
5
5
 
6
6
  module Rubikon
7
7
 
@@ -19,14 +19,39 @@ module Rubikon
19
19
  #
20
20
  # @param [String] file The path of the config file to load
21
21
  # @return [Hash] The configuration values loaded from the file
22
+ # @see IniProvider
22
23
  # @see YamlProvider
23
24
  def self.load_config(file)
25
+ provider_for(file).load_config(file)
26
+ end
27
+
28
+ # Saves a configuration Hash with the corresponding provider detected
29
+ # from the file extension
30
+ #
31
+ # @param [Hash] config The configuration to write
32
+ # @param [String] file The path of the file to write
33
+ # @see IniProvider
34
+ # @see YamlProvider
35
+ # @since 0.6.0
36
+ def self.save_config(config, file)
37
+ provider_for(file).save_config(config, file)
38
+ end
39
+
40
+ private
41
+
42
+ # Returns the correct provider for the given file
43
+ #
44
+ # The file format is guessed from the file extension.
45
+ #
46
+ # @return Object A provider for the given file format
47
+ # @since 0.6.0
48
+ def provider_for(file)
24
49
  ext = File.extname(file)
25
50
  case ext
26
51
  when '.ini'
27
- IniProvider.load_config file
52
+ IniProvider
28
53
  when '.yaml', '.yml'
29
- YamlProvider.load_config file
54
+ YamlProvider
30
55
  else
31
56
  raise UnsupportedConfigFormatError.new(ext)
32
57
  end
@@ -1,7 +1,7 @@
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) 2010, Sebastian Staudt
4
+ # Copyright (c) 2010-2011, Sebastian Staudt
5
5
 
6
6
  require 'rubikon/config/auto_provider'
7
7
  require 'rubikon/config/ini_provider'
@@ -45,19 +45,32 @@ module Rubikon
45
45
  # configuration data from the files found
46
46
  def initialize(name, search_paths, provider = :yaml)
47
47
  provider = :auto unless PROVIDERS.include?(provider)
48
- provider = Config.const_get("#{provider.to_s.capitalize}Provider")
48
+ @provider = Config.const_get("#{provider.to_s.capitalize}Provider")
49
49
 
50
50
  @files = []
51
51
  @config = {}
52
52
  search_paths.each do |path|
53
53
  config_file = File.join path, name
54
54
  if File.exists? config_file
55
- @config.merge! provider.load_config(config_file)
55
+ @config.merge! @provider.load_config(config_file)
56
56
  @files << config_file
57
57
  end
58
58
  end
59
59
  end
60
60
 
61
+ # Save the given configuration into the specified file
62
+ #
63
+ # @param [Hash] The configuration to save
64
+ # @param [String] The file path where the configuration should be saved
65
+ # @since 0.6.0
66
+ def save_config(config, file)
67
+ unless config.is_a? Hash
68
+ raise ArgumentError.new('Configuration has to be a Hash')
69
+ end
70
+
71
+ @provider.save_config config, file
72
+ end
73
+
61
74
  end
62
75
 
63
76
  end
@@ -1,7 +1,7 @@
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) 2010, Sebastian Staudt
4
+ # Copyright (c) 2010-2011, Sebastian Staudt
5
5
 
6
6
  module Rubikon
7
7
 
@@ -53,6 +53,34 @@ module Rubikon
53
53
  config
54
54
  end
55
55
 
56
+ # Saves a configuration Hash into a INI file
57
+ #
58
+ # @param [Hash] config The configuration to write
59
+ # @param [String] file The path of the file to write
60
+ # @since 0.6.0
61
+ def self.save_config(config, file)
62
+ unless config.is_a? Hash
63
+ raise ArgumentError.new('Configuration has to be a Hash')
64
+ end
65
+
66
+ file = File.new file, 'w'
67
+
68
+ config.each do |key, value|
69
+ if value.is_a? Hash
70
+ file << "\n" if file.pos > 0
71
+ file << "[#{key.to_s}]\n"
72
+
73
+ value.each do |k, v|
74
+ file << " #{k.to_s} = #{v.to_s unless v.nil?}\n"
75
+ end
76
+ else
77
+ file << "#{key.to_s} = #{value.to_s unless value.nil?}\n"
78
+ end
79
+ end
80
+
81
+ file.close
82
+ end
83
+
56
84
  end
57
85
 
58
86
  end
@@ -1,7 +1,7 @@
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) 2010, Sebastian Staudt
4
+ # Copyright (c) 2010-2011, Sebastian Staudt
5
5
 
6
6
  require 'yaml'
7
7
 
@@ -23,6 +23,21 @@ module Rubikon
23
23
  YAML.load_file file
24
24
  end
25
25
 
26
+ # Saves a configuration Hash into a YAML formatted file
27
+ #
28
+ # @param [Hash] config The configuration to write
29
+ # @param [String] file The path of the file to write
30
+ # @since 0.6.0
31
+ def self.save_config(config, file)
32
+ unless config.is_a? Hash
33
+ raise ArgumentError.new('Configuration has to be a Hash')
34
+ end
35
+
36
+ file = File.new file, 'w'
37
+ YAML.dump config, file
38
+ file.close
39
+ end
40
+
26
41
  end
27
42
 
28
43
  end
@@ -1,7 +1,7 @@
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
  module Rubikon
7
7
 
@@ -62,8 +62,32 @@ module Rubikon
62
62
  # @since 0.3.0
63
63
  class UnknownCommandError < ArgumentError
64
64
 
65
+ # @return [Symbol] The name of the command that has been tried to access
66
+ attr_reader :command
67
+
68
+ # Creates a new error and stores the name of the command that could not be
69
+ # found
70
+ #
71
+ # @param [Symbol] name The name of the unknown command
65
72
  def initialize(name)
66
73
  super "Unknown command: #{name}"
74
+ @command = name
75
+ end
76
+
77
+ end
78
+
79
+ # Raised if an argument is passed, that does not match a validation rule
80
+ #
81
+ # @author Sebastian Staudt
82
+ # @see HasArguments#check_args
83
+ # @since 0.6.0
84
+ class UnexpectedArgumentError < ArgumentError
85
+
86
+ # Creates a new error and stores the given argument value
87
+ #
88
+ # @param [Symbol] arg The given argument value
89
+ def initialize(arg)
90
+ super "Unexpected argument: #{arg}"
67
91
  end
68
92
 
69
93
  end
@@ -1,7 +1,7 @@
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) 2010, Sebastian Staudt
4
+ # Copyright (c) 2010-2011, Sebastian Staudt
5
5
 
6
6
  require 'rubikon/parameter'
7
7
 
@@ -19,47 +19,114 @@ module Rubikon
19
19
 
20
20
  include Parameter
21
21
 
22
- # @return [Array<String>] The arguments given to this parameter
23
- attr_reader :args
24
- alias_method :arguments, :args
22
+ # Provides a number of predefined regular expressions to check arguments
23
+ # against
24
+ #
25
+ # @see #initialize
26
+ # @since 0.6.0
27
+ ARGUMENT_MATCHERS = {
28
+ # Allow only alphanumeric characters
29
+ :alnum => /[[:alnum:]]+/,
30
+ # Allow only floating point numbers as arguments
31
+ :float => /-?[0-9]+(?:\.[0-9]+)?/,
32
+ # Allow only alphabetic characters
33
+ :letters => /[a-zA-Z]+/,
34
+ # Allow only numeric arguments
35
+ :numeric => /-?[0-9]+/
36
+ }
25
37
 
26
38
  # Creates a new parameter with arguments with the given name and an
27
39
  # optional code block
28
40
  #
29
41
  # @param [Application::Base] app The application this parameter belongs to
30
- # @param [Symbol, #to_sym] name The name of the option
31
- # @param [Fixnum, Range, Array] arg_count A range or array allows any
32
- # number of arguments inside the limits between the first and the
33
- # last element of the range or array (-1 stands for an arbitrary
34
- # number of arguments). A positive number indicates the exact amount
35
- # of required arguments while a negative argument count indicates
36
- # the amount of required arguments, but allows additional, optional
42
+ # @param [Symbol, #to_sym] name The name of the parameter
43
+ # @param [Array] options A range allows any number of arguments inside the
44
+ # limits of the range or array (-1 stands for an arbitrary number of
45
+ # arguments). A positive number indicates the exact amount of
46
+ # required arguments while a negative argument count indicates the
47
+ # amount of required arguments, but allows additional, optional
37
48
  # arguments. A argument count of 0 means there are no required
38
- # arguments, but it allows optional arguments.
39
- # Finally an array of symbols enables named arguments where the
40
- # argument count is the size of the array and each argument is named
41
- # after the corresponding symbol.
49
+ # arguments, but it allows optional arguments. An array of
50
+ # symbols enables named arguments where the argument count is the
51
+ # size of the array and each argument is named after the
52
+ # corresponding symbol. Finally a hash may be used to specify
53
+ # options for named arguments. The keys of the hash will be the
54
+ # names of the arguments and the values are options for this
55
+ # argument. You may specify multiple options as an array. Possible
56
+ # options are:
57
+ # - +:optional+ makes the argument optional
58
+ # - +:remainder+ makes the argument take all remaining arguments as
59
+ # an array
60
+ # - One or more strings will cause the argument to be checked to be
61
+ # equal to one of the strings
62
+ # - One or more regular expressions will cause the argument to be
63
+ # checked to match one of the expressions
64
+ # - Other symbols may reference to a predefined regular expression
65
+ # from {ARGUMENT_MATCHERS}
42
66
  # @param [Proc] block An optional code block to be executed if this
43
67
  # option is used
44
- def initialize(app, name, arg_count = 0, &block)
68
+ def initialize(app, name, *options, &block)
45
69
  super(app, name, &block)
46
70
 
47
- @args = []
48
- @arg_names = nil
49
- if arg_count.is_a? Fixnum
50
- if arg_count > 0
51
- @min_arg_count = arg_count
52
- @max_arg_count = arg_count
53
- elsif arg_count <= 0
54
- @min_arg_count = -arg_count
71
+ @arg_names = []
72
+ @arg_values = {}
73
+ @args = {}
74
+
75
+ @description = options.shift if options.first.is_a? String
76
+
77
+ if options.size == 1 && (options.first.nil? ||
78
+ options.first.is_a?(Fixnum) || options.first.is_a?(Range))
79
+ options = options.first
80
+ end
81
+
82
+ if options.is_a? Fixnum
83
+ if options > 0
84
+ @min_arg_count = options
85
+ @max_arg_count = options
86
+ elsif options <= 0
87
+ @min_arg_count = -options
55
88
  @max_arg_count = -1
56
89
  end
57
- elsif arg_count.is_a?(Array) && arg_count.all? { |a| a.is_a? Symbol }
58
- @max_arg_count = @min_arg_count = arg_count.size
59
- @arg_names = arg_count
60
- elsif arg_count.is_a?(Range) || arg_count.is_a?(Array)
61
- @min_arg_count = arg_count.first
62
- @max_arg_count = arg_count.last
90
+ elsif options.is_a? Range
91
+ @min_arg_count = options.first
92
+ @max_arg_count = options.last
93
+ elsif options.is_a? Array
94
+ @arg_names = []
95
+ @max_arg_count = 0
96
+ @min_arg_count = 0
97
+ options.each do |arg|
98
+ if arg.is_a? Hash
99
+ arg = arg.map do |arg_name, opt|
100
+ [arg_name, opt.is_a?(Array) ? opt : [opt]]
101
+ end
102
+ arg = arg.sort_by do |arg_name, opt|
103
+ opt.include?(:optional) ? 1 : 0
104
+ end
105
+ arg.each do |arg_name, opt|
106
+ matchers = opt.reject { |o| [:optional, :remainder].include? o }
107
+ opt -= matchers
108
+ @arg_names << arg_name.to_sym
109
+ if !matchers.empty?
110
+ matchers.map! do |m|
111
+ ARGUMENT_MATCHERS[m] || (m.is_a?(Regexp) ? m : m.to_s)
112
+ end
113
+ @arg_values[arg_name] = /^#{Regexp.union *matchers}$/
114
+ end
115
+ unless opt.include? :optional
116
+ @min_arg_count += 1
117
+ end
118
+ if opt.include? :remainder
119
+ @max_arg_count = -1
120
+ break
121
+ end
122
+ @max_arg_count += 1
123
+ end
124
+ else
125
+ @arg_names << arg.to_sym
126
+ @min_arg_count += 1
127
+ @max_arg_count += 1
128
+ end
129
+ end
63
130
  else
64
131
  @min_arg_count = 0
65
132
  @max_arg_count = 0
@@ -68,17 +135,28 @@ module Rubikon
68
135
 
69
136
  # Access the arguments of this parameter using a numeric or symbolic index
70
137
  #
71
- # @param [Numeric, Symbol] The index of the argument to return. Numeric
72
- # indices can be used always while symbolic arguments are only
73
- # available for named arguments.
138
+ # @param [Numeric, Symbol] arg The name or index of the argument to return.
139
+ # Numeric indices can be used always while symbolic arguments are
140
+ # only available for named arguments.
74
141
  # @return The argument with the specified index
75
142
  # @see #args
76
143
  # @since 0.4.0
77
144
  def [](arg)
78
- arg = @arg_names.index(arg) if arg.is_a? Symbol
79
145
  @args[arg]
80
146
  end
81
147
 
148
+ # Returns the arguments given to this parameter. They are given as a Hash
149
+ # when there are named arguments or as an Array when there are no named
150
+ # arguments
151
+ #
152
+ # @return [Array<String>, Hash<Symbol, String>] The arguments given to this
153
+ # parameter
154
+ # @since 0.6.0
155
+ def args
156
+ @arg_names.empty? ? @args.values : @args
157
+ end
158
+ alias_method :arguments, :args
159
+
82
160
  protected
83
161
 
84
162
  # Adds an argument to this parameter. Arguments can be accessed inside the
@@ -93,10 +171,20 @@ module Rubikon
93
171
  # @see #args
94
172
  # @since 0.3.0
95
173
  def <<(arg)
96
- if args_full? && @args.size == @max_arg_count
97
- raise ExtraArgumentError.new(@name)
174
+ raise ExtraArgumentError.new(@name) unless more_args?
175
+
176
+ if @arg_names.size > @args.size
177
+ name = @arg_names[@args.size]
178
+ if @max_arg_count == -1 && @arg_names.size == @args.size + 1
179
+ @args[name] = [arg]
180
+ else
181
+ @args[name] = arg
182
+ end
183
+ elsif !@arg_names.empty? && @max_arg_count == -1
184
+ @args[@arg_names.last] << arg
185
+ else
186
+ @args[@args.size] = arg
98
187
  end
99
- @args << arg
100
188
  end
101
189
 
102
190
  # Marks this parameter as active when it has been supplied by the user on
@@ -131,6 +219,18 @@ module Rubikon
131
219
  # @since 0.3.0
132
220
  def check_args
133
221
  raise MissingArgumentError.new(@name) unless args_full?
222
+ unless @arg_values.empty?
223
+ @args.each do |name, arg|
224
+ if @arg_values.key? name
225
+ arg = [arg] unless arg.is_a? Array
226
+ arg.each do |a|
227
+ unless a =~ @arg_values[name]
228
+ raise UnexpectedArgumentError.new(a)
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
134
234
  end
135
235
 
136
236
  # If a named argument with the specified method name exists, a call to that
@@ -145,8 +245,8 @@ module Rubikon
145
245
  # @user = name
146
246
  # end
147
247
  def method_missing(name, *args, &block)
148
- if args.empty? && !block_given? && !@arg_names.nil? && @arg_names.include?(name)
149
- @args[@arg_names.index(name)]
248
+ if args.empty? && !block_given? && @arg_names.include?(name)
249
+ @args[name]
150
250
  else
151
251
  super
152
252
  end
@@ -177,7 +277,7 @@ module Rubikon
177
277
  # @return +true+ if named argument with the specified name exists
178
278
  # @see #method_missing
179
279
  def respond_to_missing?(name, include_private = false)
180
- !@arg_names.nil? && @arg_names.include?(name)
280
+ @arg_names.include? name
181
281
  end
182
282
 
183
283
  end