rubikon 0.2.1 → 0.3.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,8 +1,12 @@
1
- # This code is free software; you can redistribute it and/or modify it under the
2
- # terms of the new BSD License.
1
+ # This code is free software; you can redistribute it and/or modify it under
2
+ # the terms of the new BSD License.
3
3
  #
4
- # Copyright (c) 2009, Sebastian Staudt
4
+ # Copyright (c) 2009-2010, Sebastian Staudt
5
5
 
6
+ require 'rubikon/command'
7
+ require 'rubikon/exceptions'
8
+ require 'rubikon/flag'
9
+ require 'rubikon/option'
6
10
  require 'rubikon/progress_bar'
7
11
  require 'rubikon/throbber'
8
12
 
@@ -10,183 +14,70 @@ module Rubikon
10
14
 
11
15
  module Application
12
16
 
17
+ # This module contains internal instance methods of +Application::Base+ and
18
+ # its subclasses.
19
+ #
20
+ # @author Sebastian Staudt
21
+ # @see Application::Base
22
+ # @since 0.2.0
13
23
  module InstanceMethods
14
24
 
15
- # Initialize with default settings (see set for more detail)
16
- #
17
- # If you really need to override this in your application class, be sure to
18
- # call +super+
19
- def initialize
20
- @actions = {}
21
- @aliases = {}
22
- @default = nil
23
- @initialized = false
24
- @settings = {
25
- :autorun => true,
26
- :auto_shortopts => true,
27
- :dashed_options => true,
28
- :help_banner => "Usage: #{$0}",
29
- :istream => $stdin,
30
- :name => self.class.to_s,
31
- :ostream => $stdout,
32
- :raise_errors => false
33
- }
34
- end
35
-
36
- # Define an Application Action
37
- #
38
- # +name+:: The name of the action. Used as an option parameter.
39
- # +options+:: A Hash of options to be used on the created Action
40
- # (default: <tt>{}</tt>)
41
- # +block+:: A block containing the code that should be executed when this
42
- # Action is called, i.e. when the Application is called with
43
- # the associated option parameter
44
- def action(name, options = {}, &block)
45
- raise "No block given" unless block_given?
46
-
47
- action = Action.new(options, &block)
48
-
49
- key = name.to_s
50
- if @settings[:dashed_options]
51
- if @settings[:auto_shortopts]
52
- short_key = "-#{key[0..0]}"
53
- @actions[short_key.to_sym] = action unless @actions.key? short_key
54
- end
55
- key = "--#{key}"
56
- end
57
-
58
- @actions[key.to_sym] = action
59
- end
60
-
61
- # Define an alias to an Action
62
- #
63
- # +name+:: The name of the alias
64
- # +action+:: The name of the Action that should be aliased
65
- #
66
- # Example:
67
- #
68
- # action_alias :doit, :dosomething
69
- def action_alias(name, action)
70
- @aliases[name.to_sym] = action.to_sym
71
- end
72
-
73
- # Define the default Action of the Application
74
- #
75
- # +options+:: A Hash of options to be used on the created Action
76
- # (default: <tt>{}</tt>)
77
- # +block+:: A block containing the code that should be executed when this
78
- # Action is called, i.e. when no option is given to the
79
- # Application
80
- def default(options = {}, &block)
81
- @default = Action.new(options, &block)
82
- end
83
-
84
- # Prompts the user for input
85
- #
86
- # If +prompt+ is not empty this will display a prompt using
87
- # <tt>prompt.to_s</tt>.
88
- #
89
- # +prompt+:: A String or other Object responding to +to_s+ used for
90
- # displaying a prompt to the user (default: <tt>''</tt>)
91
- #
92
- # Example:
93
- #
94
- # action 'interactive' do
95
- # # Display a prompt "Please type something: "
96
- # user_provided_value = input 'Please type something'
97
- #
98
- # # Do something with the data
99
- # ...
100
- # end
101
- def input(prompt = '')
102
- unless prompt.to_s.empty?
103
- ostream << "#{prompt}: "
104
- end
105
- @settings[:istream].gets[0..-2]
106
- end
107
-
108
- # Convenience method for accessing the user-defined output stream
109
- #
110
- # Use this if you want to work directly with the output stream
111
- #
112
- # Example:
113
- #
114
- # ostream.flush
115
- def ostream
116
- @settings[:ostream]
117
- end
118
-
119
- # Displays a progress bar while the given block is executed
120
- #
121
- # Inside the block you have access to a instance of ProgressBar. So you
122
- # can update the progress using <tt>ProgressBar#+</tt>.
123
- #
124
- # +options+:: A Hash of options that should be passed to the ProgressBar
125
- # object. For available options see ProgressBar
126
- # +block+:: The block to execute
127
- #
128
- # Example:
129
- #
130
- # progress_bar(:maximum => 5) do |progress|
131
- # 5.times do |file|
132
- # File.read("any#{file}.txt")
133
- # progress.+
134
- # end
135
- # end
136
- def progress_bar(*options, &block)
137
- hidden_output do |ostream|
138
- options = options[0]
139
- options[:ostream] = ostream
140
-
141
- progress = ProgressBar.new(options)
142
-
143
- block.call(progress)
144
- end
145
- end
25
+ # @return [String] The absolute path of the application
26
+ attr_reader :path
146
27
 
147
- # Output text using +IO#<<+ of the output stream
28
+ # Initialize with default settings
148
29
  #
149
- # +text+:: The text to write into the output stream
150
- def put(text)
151
- @settings[:ostream] << text
152
- @settings[:ostream].flush
153
- end
154
-
155
- # Output a character using +IO#putc+ of the output stream
30
+ # If you really need to override this in your application class, be sure
31
+ # to call +super+
156
32
  #
157
- # +char+:: The character to write into the output stream
158
- def putc(char)
159
- @settings[:ostream].putc char
160
- end
161
-
162
- # Output a line of text using +IO#puts+ of the output stream
163
- #
164
- # +text+:: The text to write into the output stream
165
- def puts(text)
166
- @settings[:ostream].puts text
33
+ # @see #set
34
+ def initialize
35
+ @commands = {}
36
+ @current_command = nil
37
+ @current_global_option = nil
38
+ @global_parameters = {}
39
+ @initialized = false
40
+ @parameters = []
41
+ @path = File.dirname($0)
42
+ @settings = {
43
+ :autorun => true,
44
+ :help_as_default => true,
45
+ :help_banner => "Usage: #{$0}",
46
+ :istream => $stdin,
47
+ :name => self.class.to_s,
48
+ :ostream => $stdout,
49
+ :raise_errors => false
50
+ }
167
51
  end
168
52
 
169
53
  # Run this application
170
54
  #
171
- # +args+:: The command line arguments that should be given to the
172
- # application as options
55
+ # Calling this method explicitly is not required when you want to create
56
+ # a simple application (having one main class inheriting from
57
+ # Rubikon::Application). But it's useful for testing or if you want to
58
+ # have some sort of sub-applications.
173
59
  #
174
- # Calling this method explicitly is not required when you want to create a
175
- # simple application (having one main class inheriting from
176
- # Rubikon::Application). But it's useful for testing or if you want to have
177
- # some sort of sub-applications.
60
+ # @param [Array<String>] args The command line arguments that should be
61
+ # given to the application as options
178
62
  def run(args = ARGV)
179
- init unless @initialized
180
- action_results = []
181
-
182
63
  begin
183
- if !@default.nil? and args.empty?
184
- action_results << @default.run
185
- else
186
- parse_options(args).each do |action, args|
187
- action_results << @actions[action].run(*args)
64
+ init unless @initialized
65
+
66
+ command, parameters, args = parse_arguments(args)
67
+
68
+ parameters.each do |parameter|
69
+ if parameter.is_a? Option
70
+ parameter.check_args
71
+ @current_global_option = parameter
188
72
  end
73
+ parameter.active!
74
+ @current_global_option = nil
189
75
  end
76
+
77
+ @current_command = command
78
+ result = command.run(*args)
79
+ @current_command = nil
80
+ result
190
81
  rescue
191
82
  raise $! if @settings[:raise_errors]
192
83
 
@@ -194,96 +85,62 @@ module Rubikon
194
85
  puts " #{$!.backtrace.join("\n ")}" if $DEBUG
195
86
  exit 1
196
87
  end
197
-
198
- action_results
199
- end
200
-
201
- # Sets an application setting
202
- #
203
- # +setting+:: The name of the setting to change, will be symbolized first.
204
- # +value+:: The value the setting should be changed to
205
- #
206
- # Available settings
207
- # +autorun+:: If true, let the application run as soon as its class
208
- # is defined
209
- # +dashed_options+:: If true, each option is prepended with a double-dash
210
- # (<tt>-</tt><tt>-</tt>)
211
- # +help_banner+:: Defines a banner for the help message (<em>unused</em>)
212
- # +istream+:: Defines an input stream to use
213
- # +name+:: Defines the name of the application
214
- # +ostream+:: Defines an output stream to use
215
- # +raise_errors+:: If true, raise errors, otherwise fail gracefully
216
- #
217
- # Example:
218
- #
219
- # set :name, 'My App'
220
- # set :autorun, false
221
- def set(setting, value)
222
- @settings[setting.to_sym] = value
223
- end
224
-
225
- # Displays a throbber while the given block is executed
226
- #
227
- # Example:
228
- #
229
- # action 'slow' do
230
- # throbber do
231
- # # Add some long running code here
232
- # ...
233
- # end
234
- # end
235
- def throbber(&block)
236
- hidden_output do |ostream|
237
- code_thread = Thread.new { block.call }
238
- throbber_thread = Throbber.new(ostream, code_thread)
239
-
240
- code_thread.join
241
- throbber_thread.join
242
- end
243
88
  end
244
89
 
245
90
  private
246
91
 
247
- # Assigns aliases to the actions that have been defined using action_alias
92
+ # Defines a global Flag for enabling debug output
248
93
  #
249
- # Clears the aliases Hash afterwards
250
- def assign_aliases
251
- @aliases.each do |key, action|
252
- if @settings[:dashed_options]
253
- action = "--#{action}".to_sym
254
- key = "--#{key}".to_sym
255
- end
256
-
257
- unless @actions.key? key
258
- @actions[key] = @actions[action]
259
- else
260
- warn "There's already an action called \"#{key}\"."
261
- end
94
+ # This will define a Flag <tt>--debug</tt> (with alias <tt>-d</tt>) to
95
+ # enable debug output.
96
+ # Using it sets Ruby's global variable <tt>$DEBUG</tt> to +true+.
97
+ #
98
+ # @return [Flag]
99
+ def debug_flag
100
+ global_flag :debug do
101
+ $DEBUG = true
262
102
  end
103
+ global_flag :d => :debug
263
104
  end
264
105
 
265
- # Defines an action for displaying a help screen
106
+ # Defines a command for displaying a help screen
266
107
  #
267
- # This takes any defined action and it's corresponding options and
108
+ # This takes any defined commands and it's corresponding options and
268
109
  # descriptions and displays them in a user-friendly manner.
269
- def help_action
270
- action 'help', { :description => 'Display this help screen' } do
110
+ def help_command
111
+ command :help, 'Display this help screen' do
112
+ put @settings[:help_banner]
113
+
271
114
  help = {}
272
- @actions.each do |option, action|
273
- help[action] = [] if help[action].nil?
274
- help[action] << option.to_s
115
+ @commands.each_value do |command|
116
+ help[command.name.to_s] = command.description
275
117
  end
276
118
 
277
- put @settings[:help_banner]
278
- puts " [options]" unless @default.nil?
279
- puts ''
119
+ global_params = ''
120
+ @global_parameters.values.uniq.sort {|a,b| a.name.to_s <=> b.name.to_s }.each do |param|
121
+ global_params << ' ['
122
+ ([param.name] + param.aliases).each_with_index do |name, index|
123
+ name = name.to_s
124
+ global_params << '|' if index > 0
125
+ global_params << '-' if name.size > 1
126
+ global_params << "-#{name}"
127
+ end
128
+ global_params << ' ...' if param.is_a?(Option)
129
+ global_params << ']'
130
+ end
280
131
 
281
- help.each do |action, options|
282
- help[action] = options.sort.join(', ')
132
+ default_description = help.delete('__default')
133
+ if default_description.nil?
134
+ puts "#{global_params} command [args]\n\n"
135
+ else
136
+ puts "#{global_params} [command] [args]\n\n"
137
+ puts "Without command: #{default_description}\n\n"
283
138
  end
284
- max_options_length = help.values.max { |a,b| a.size <=> b.size }.size
285
- help.sort_by { |action, options| options }.each do |action, options|
286
- puts options.ljust(max_options_length) << " " << action.description
139
+
140
+ puts "Commands:"
141
+ max_command_length = help.keys.max { |a, b| a.size <=> b.size }.size
142
+ help.sort_by { |name, description| name }.each do |name, description|
143
+ puts " #{name.ljust(max_command_length)} #{description}"
287
144
  end
288
145
  end
289
146
  end
@@ -291,7 +148,8 @@ module Rubikon
291
148
  # Hide output inside the given block and print it after the block has
292
149
  # finished
293
150
  #
294
- # +block+:: The block that should not print output while it's running
151
+ # @param [Proc] block The block that should not print output while it's
152
+ # running
295
153
  #
296
154
  # If the block needs to print to the real IO stream, it can access it
297
155
  # using its first parameter.
@@ -310,32 +168,73 @@ module Rubikon
310
168
  # run, but <em>after</em> the application is setup, i.e. after the user
311
169
  # has defined the application class.
312
170
  def init
313
- help_action
314
- assign_aliases
171
+ debug_flag
172
+ help_command
173
+ verbose_flag
174
+
175
+ if @settings[:help_as_default] && !@commands.keys.include?(:__default)
176
+ default :help
177
+ end
178
+
315
179
  @initialized = true
316
180
  end
317
181
 
318
- # Parses the options used when starting the application
319
- #
320
- # +options+:: An Array of Strings that should be used as application
321
- # options. Usually +ARGV+ is used for this.
322
- def parse_options(options)
323
- actions_to_call = {}
324
- last_action = nil
325
-
326
- options.each do |option|
327
- option_sym = option.to_s.to_sym
328
- if @actions.keys.include? option_sym
329
- actions_to_call[option_sym] = []
330
- last_action = option_sym
331
- elsif last_action.nil? || (option.is_a?(String) && @settings[:dashed_options] && option[0..1] == '--')
332
- raise UnknownOptionError.new(option)
182
+ # Parses the command-line arguments given to the application by the
183
+ # user. This distinguishes between commands, global flags and command
184
+ # flags
185
+ #
186
+ # @param [Array] args The command-line arguments
187
+ # @return [Command, Array<Symbol>, Array] The command to execute, the
188
+ # parameters of this command that have been supplied and any
189
+ # additional command-line arguments supplied
190
+ def parse_arguments(args)
191
+ command_arg = args.shift
192
+ if command_arg.nil? || command_arg.start_with?('-')
193
+ command = @commands[:__default]
194
+ args.unshift(command_arg) unless command_arg.nil?
195
+ raise NoDefaultCommandError if command.nil?
196
+ else
197
+ command = @commands[command_arg.to_sym]
198
+ raise UnknownCommandError.new(command_arg) if command.nil?
199
+ end
200
+
201
+ parameter = nil
202
+ parameters = []
203
+ args.dup.each do |arg|
204
+ if arg.start_with?('--')
205
+ parameter = @global_parameters[arg[2..-1].to_sym]
206
+ elsif arg.start_with?('-')
207
+ parameter = @global_parameters[arg[1..-1].to_sym]
333
208
  else
334
- actions_to_call[last_action] << option
209
+ if !parameter.nil? && parameter.more_args?
210
+ parameter.args << args.delete(arg)
211
+ else
212
+ parameter = nil
213
+ end
214
+ next
215
+ end
216
+
217
+ unless parameter.nil?
218
+ parameters << parameter
219
+ args.delete(arg)
335
220
  end
336
221
  end
337
222
 
338
- actions_to_call
223
+ return command, parameters, args
224
+ end
225
+
226
+ # Defines a global Flag for enabling verbose output
227
+ #
228
+ # This will define a Flag <tt>--verbose</tt> and <tt>-v</tt> to enable
229
+ # verbose output.
230
+ # Using it sets Ruby's global variable <tt>$VERBOSE</tt> to +true+.
231
+ #
232
+ # @return [Flag] The debug Flag object
233
+ def verbose_flag
234
+ global_flag :verbose do
235
+ $VERBOSE = true
236
+ end
237
+ global_flag :v => :verbose
339
238
  end
340
239
 
341
240
  end
@@ -0,0 +1,135 @@
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
+ require 'rubikon/application/base'
7
+ require 'rubikon/exceptions'
8
+ require 'rubikon/parameter'
9
+
10
+ module Rubikon
11
+
12
+ # Instances of the Command class are used to define the real code that should
13
+ # be executed when running the Application.
14
+ #
15
+ # @author Sebastian Staudt
16
+ # @since 0.3.0
17
+ class Command
18
+
19
+ include Parameter
20
+
21
+ attr_accessor :description
22
+ attr_reader :args, :params
23
+ alias_method :arguments, :args
24
+ alias_method :parameters, :params
25
+
26
+ # Create a new application command with the given name with a reference to
27
+ # the app it belongs to
28
+ #
29
+ # @param [Application::Base] app A reference to the application
30
+ # instance this command belongs to
31
+ # @param [#to_sym] name The name of this command, used in application
32
+ # arguments
33
+ # @param [Proc] block The code block which should be executed by this
34
+ # command
35
+ # @raise [ArgumentError] if the given application object isn't a Rubikon
36
+ # application
37
+ # @raise [BlockMissingError] if no command code block is given and a
38
+ # command file does not exist
39
+ def initialize(app, name, &block)
40
+ raise ArgumentError unless app.is_a? Application::Base
41
+ super(name, nil)
42
+
43
+ @app = app
44
+ @params = {}
45
+
46
+ if block_given?
47
+ @block = block
48
+ else
49
+ @file_name = "#{@app.path}/commands/#{name}.rb"
50
+ raise BlockMissingError unless File.exists?(@file_name)
51
+ code = open(@file_name).read
52
+ @block = Proc.new { instance_eval(code) }
53
+ end
54
+ end
55
+
56
+ # Add a new Parameter for this command
57
+ #
58
+ # @param [Parameter, Hash] parameter The parameter to add to this
59
+ # command. This might also be a Hash where every key will be an
60
+ # alias to the corresponding value, e.g. <tt>{ :alias => :parameter
61
+ # }</tt>.
62
+ # @see Parameter
63
+ def <<(parameter)
64
+ if parameter.is_a? Hash
65
+ parameter.each do |alias_name, name|
66
+ alias_name = alias_name.to_sym
67
+ name = name.to_sym
68
+ parameter = @params[name]
69
+ if parameter.nil?
70
+ @params[alias_name] = name
71
+ else
72
+ parameter.aliases << alias_name
73
+ @params[alias_name] = parameter
74
+ end
75
+ end
76
+ else
77
+ raise ArgumentError unless parameter.is_a? Parameter
78
+ @params.each do |name, param|
79
+ if param == parameter.name
80
+ parameter.aliases << name
81
+ end
82
+ end
83
+ @params[parameter.name] = parameter
84
+ end
85
+ end
86
+
87
+ # Parses the arguments of this command and sets each Parameter as active
88
+ # if it has been supplied by the user on the command-line. Additional
89
+ # arguments are passed to the individual parameters.
90
+ #
91
+ # @param [Array<String>] args The arguments that have been passed to this
92
+ # command
93
+ # @raise [UnknownParameterError] if an undefined parameter is passed to the
94
+ # command
95
+ # @see Flag
96
+ # @see Option
97
+ def parse_arguments(args)
98
+ @args = []
99
+ parameter = nil
100
+ args.each do |arg|
101
+ if arg.start_with?('-')
102
+ parameter_name = arg.start_with?('--') ? arg[2..-1] : arg[1..-1]
103
+ parameter = @params[parameter_name.to_sym]
104
+ raise UnknownParameterError.new(arg) if parameter.nil?
105
+ end
106
+
107
+ unless parameter.nil? || parameter.active?
108
+ parameter.active!
109
+ next
110
+ end
111
+
112
+ if parameter.nil? || !parameter.more_args?
113
+ @args << arg
114
+ else
115
+ parameter << arg
116
+ end
117
+ end
118
+
119
+ @params.values.each do |param|
120
+ param.check_args if param.is_a?(Option) && param.active?
121
+ end
122
+ end
123
+
124
+ # Run this command's code block
125
+ #
126
+ # @param [Array<String>] args The arguments that have been passed to this
127
+ # command
128
+ def run(*args)
129
+ parse_arguments(args)
130
+ @app.instance_eval(&@block)
131
+ end
132
+
133
+ end
134
+
135
+ end