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,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