request-log-analyzer 1.0.2

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.
Files changed (60) hide show
  1. data/DESIGN +14 -0
  2. data/HACKING +7 -0
  3. data/LICENSE +20 -0
  4. data/README.textile +36 -0
  5. data/Rakefile +5 -0
  6. data/bin/request-log-analyzer +123 -0
  7. data/lib/cli/bashcolorizer.rb +60 -0
  8. data/lib/cli/command_line_arguments.rb +301 -0
  9. data/lib/cli/progressbar.rb +236 -0
  10. data/lib/request_log_analyzer.rb +14 -0
  11. data/lib/request_log_analyzer/aggregator/base.rb +45 -0
  12. data/lib/request_log_analyzer/aggregator/database.rb +148 -0
  13. data/lib/request_log_analyzer/aggregator/echo.rb +25 -0
  14. data/lib/request_log_analyzer/aggregator/summarizer.rb +116 -0
  15. data/lib/request_log_analyzer/controller.rb +201 -0
  16. data/lib/request_log_analyzer/file_format.rb +81 -0
  17. data/lib/request_log_analyzer/file_format/merb.rb +33 -0
  18. data/lib/request_log_analyzer/file_format/rails.rb +90 -0
  19. data/lib/request_log_analyzer/filter/base.rb +29 -0
  20. data/lib/request_log_analyzer/filter/field.rb +36 -0
  21. data/lib/request_log_analyzer/filter/timespan.rb +32 -0
  22. data/lib/request_log_analyzer/line_definition.rb +159 -0
  23. data/lib/request_log_analyzer/log_parser.rb +173 -0
  24. data/lib/request_log_analyzer/log_processor.rb +121 -0
  25. data/lib/request_log_analyzer/request.rb +95 -0
  26. data/lib/request_log_analyzer/source/base.rb +42 -0
  27. data/lib/request_log_analyzer/source/log_file.rb +170 -0
  28. data/lib/request_log_analyzer/tracker/base.rb +54 -0
  29. data/lib/request_log_analyzer/tracker/category.rb +71 -0
  30. data/lib/request_log_analyzer/tracker/duration.rb +81 -0
  31. data/lib/request_log_analyzer/tracker/hourly_spread.rb +80 -0
  32. data/lib/request_log_analyzer/tracker/timespan.rb +54 -0
  33. data/spec/controller_spec.rb +40 -0
  34. data/spec/database_inserter_spec.rb +101 -0
  35. data/spec/file_format_spec.rb +78 -0
  36. data/spec/file_formats/spec_format.rb +26 -0
  37. data/spec/filter_spec.rb +137 -0
  38. data/spec/fixtures/merb.log +84 -0
  39. data/spec/fixtures/multiple_files_1.log +5 -0
  40. data/spec/fixtures/multiple_files_2.log +2 -0
  41. data/spec/fixtures/rails_1x.log +59 -0
  42. data/spec/fixtures/rails_22.log +12 -0
  43. data/spec/fixtures/rails_22_cached.log +10 -0
  44. data/spec/fixtures/rails_unordered.log +24 -0
  45. data/spec/fixtures/syslog_1x.log +5 -0
  46. data/spec/fixtures/test_file_format.log +13 -0
  47. data/spec/fixtures/test_language_combined.log +14 -0
  48. data/spec/fixtures/test_order.log +16 -0
  49. data/spec/line_definition_spec.rb +124 -0
  50. data/spec/log_parser_spec.rb +68 -0
  51. data/spec/log_processor_spec.rb +57 -0
  52. data/spec/merb_format_spec.rb +38 -0
  53. data/spec/rails_format_spec.rb +76 -0
  54. data/spec/request_spec.rb +72 -0
  55. data/spec/spec_helper.rb +67 -0
  56. data/spec/summarizer_spec.rb +9 -0
  57. data/tasks/github-gem.rake +177 -0
  58. data/tasks/request_log_analyzer.rake +10 -0
  59. data/tasks/rspec.rake +6 -0
  60. metadata +135 -0
data/DESIGN ADDED
@@ -0,0 +1,14 @@
1
+ Request-log-analyzer is set up like a simple pipe and filter system.
2
+
3
+ This allows you to easily add extra reports, filters and outputs.
4
+
5
+ 1) Build pipeline.
6
+ -> Aggregator (database)
7
+ Source -> Filter -> Filter -> Aggregator (summary report)
8
+ -> Aggregator (...)
9
+
10
+ 2) Start chunk producer and push chunks through pipeline.
11
+ Controller.start
12
+
13
+ 3) Gather output from pipeline.
14
+ Controller.report
data/HACKING ADDED
@@ -0,0 +1,7 @@
1
+ HACKING on r-l-a
2
+ ----------------
3
+
4
+ - See DESIGN for the basic internal design of r-l-a
5
+ - See http://wiki.github.com/wvanbergen/request-log-analyzer/development for
6
+ more information about developing
7
+ - Contact willem AT vanbergen DOT org for any questions
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Willem van Bergen / Bart ten Brinke
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,36 @@
1
+ h1. Request-log-analyzer
2
+
3
+ This is a simple command line tool to analyze request log files of both Rails and
4
+ Merb to produce a performance report. Its purpose is to find what actions are best candidates for optimization.
5
+
6
+ * Analyzes Rails log files (all versions)
7
+ * Can combine multiple files (handy if you are using logrotate)
8
+ * Uses several metrics, including cumulative request time, average request time, process blockers, database and rendering time, HTTP methods and states, Rails action cache statistics, etc.) ("Sample output":http://wiki.github.com/wvanbergen/request-log-analyzer/sample-output)
9
+ * Low memory footprint (server-safe)
10
+ * Fast
11
+ * MIT licensed
12
+
13
+ h2. Installation
14
+
15
+ <pre>
16
+ $ sudo gem install wvanbergen-request-log-analyzer --source http://gems.github.com
17
+ </pre>
18
+
19
+ To get the best results out of request-log-analyzer, make sure to
20
+ "set up logging correctly":http://wiki.github.com/wvanbergen/request-log-analyzer/configure-logging
21
+ for your application.
22
+
23
+ h2. Usage
24
+
25
+ To analyze a log file and produce a performance report, run request-log-analyzer like this:
26
+
27
+ <pre>
28
+ $ request-log-analyzer log/production.log
29
+ </pre>
30
+
31
+ For more details and available command line options, see the "project's wiki":http://wiki.github.com/wvanbergen/request-log-analyzer/basic-usage
32
+
33
+ h2. Additional information
34
+
35
+ * "Project wiki at GitHub":http://wiki.github.com/wvanbergen/request-log-analyzer
36
+ * "wvanbergen's blog posts":http://techblog.floorplanner.com/tag/request-log-analyzer/
@@ -0,0 +1,5 @@
1
+ Dir[File.dirname(__FILE__) + "/tasks/*.rake"].each { |file| load(file) }
2
+
3
+ desc 'Default: run RSpec for request-log-analyzer.'
4
+ task :default => :spec
5
+
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/ruby
2
+ require File.dirname(__FILE__) + '/../lib/request_log_analyzer'
3
+ require File.dirname(__FILE__) + '/../lib/cli/command_line_arguments'
4
+
5
+ def terminal_width(default = 81)
6
+ IO.popen('stty -a') do |pipe|
7
+ column_line = pipe.detect { |line| /(\d+) columns/ =~ line }
8
+ width = column_line ? $1.to_i : default
9
+ end
10
+ rescue
11
+ default
12
+ end
13
+
14
+ # Parse the arguments given via commandline
15
+ begin
16
+ arguments = CommandLine::Arguments.parse do |command_line|
17
+
18
+ command_line.command(:install) do |install|
19
+ install.parameters = 1
20
+ end
21
+
22
+ command_line.command(:strip) do |strip|
23
+ strip.minimum_parameters = 1
24
+ strip.option(:format, :alias => :f, :default => 'rails')
25
+ strip.option(:output, :alias => :o)
26
+ strip.switch(:discard_teaser_lines, :t)
27
+ strip.switch(:keep_junk_lines, :j)
28
+ end
29
+
30
+ command_line.command(:anonymize) do |anonymize|
31
+ anonymize.minimum_parameters = 1
32
+ anonymize.option(:format, :alias => :f, :default => 'rails')
33
+ anonymize.option(:output, :alias => :o)
34
+ anonymize.switch(:discard_teaser_lines, :t)
35
+ anonymize.switch(:keep_junk_lines, :j)
36
+ end
37
+
38
+ command_line.option(:format, :alias => :f, :default => 'rails')
39
+ command_line.option(:file, :alias => :e)
40
+ command_line.switch(:assume_correct_order)
41
+
42
+ command_line.option(:aggregator, :alias => :a, :multiple => true)
43
+ command_line.option(:database, :alias => :d)
44
+
45
+ # filtering options
46
+ command_line.option(:select, :multiple => true, :parameters => 2)
47
+ command_line.option(:reject, :multiple => true, :parameters => 2)
48
+ command_line.option(:after)
49
+ command_line.option(:before)
50
+
51
+ command_line.switch(:boring, :b)
52
+ command_line.option(:report_width, :default => terminal_width - 1)
53
+
54
+ command_line.switch(:debug)
55
+
56
+ command_line.minimum_parameters = 1
57
+ end
58
+
59
+ rescue CommandLine::Error => e
60
+ puts "ARGUMENT ERROR: " + e.message if e.message
61
+ puts
62
+ puts "Usage: request-log-analyzer [LOGFILES*] <OPTIONS>"
63
+ puts
64
+ puts "Input options:"
65
+ puts " --format <format>, -f: Uses the specified log file format. Defaults to rails."
66
+ puts " --after <date> Only consider requests from <date> or later."
67
+ puts " --before <date> Only consider requests before <date>."
68
+ puts " --select <field> <value> Only consider requests where <field> matches <value>."
69
+ puts " --reject <field> <value> Only consider requests where <field> does not match <value>."
70
+ puts
71
+ puts "Output options:"
72
+ puts " --boring, -b Output reports without ASCII colors."
73
+ puts " --database <filename>, -d: Creates an SQLite3 database of all the parsed request information."
74
+ puts " --debug Print debug information while parsing."
75
+ puts " --file <filename> Output to file."
76
+ puts
77
+ puts "Examples:"
78
+ puts " request-log-analyzer development.log"
79
+ puts " request-log-analyzer -b mongrel.0.log mongrel.1.log mongrel.2.log "
80
+ puts " request-log-analyzer --format merb -d requests.db production.log"
81
+ puts
82
+ puts "To install rake tasks in your Rails application, "
83
+ puts "run the following command in your application's root directory:"
84
+ puts
85
+ puts " request-log-analyzer install rails"
86
+ exit(0)
87
+ end
88
+
89
+ def install_rake_tasks(install_type)
90
+ if install_type == 'rails'
91
+ require 'ftools'
92
+ if File.directory?('./lib/tasks/')
93
+ File.copy(File.dirname(__FILE__) + '/../tasks/request_log_analyzer.rake', './lib/tasks/request_log_analyze.rake')
94
+ puts "Installed rake tasks."
95
+ puts "To use, run: rake log:analyze"
96
+ else
97
+ puts "Cannot find /lib/tasks folder. Are you in your Rails directory?"
98
+ puts "Installation aborted."
99
+ end
100
+ else
101
+ raise "Cannot perform this install type! (#{install_type})"
102
+ end
103
+ end
104
+
105
+
106
+ case arguments.command
107
+ when :install
108
+ install_rake_tasks(arguments.parameters[0])
109
+ when :strip
110
+ require File.dirname(__FILE__) + '/../lib/request_log_analyzer/log_processor'
111
+ RequestLogAnalyzer::LogProcessor.build(:strip, arguments).run!
112
+ when :anonymize
113
+ require File.dirname(__FILE__) + '/../lib/request_log_analyzer/log_processor'
114
+ RequestLogAnalyzer::LogProcessor.build(:anonymize, arguments).run!
115
+ else
116
+ puts "Request log analyzer, by Willem van Bergen and Bart ten Brinke - Version 1.0\n\n"
117
+
118
+ # Run the request_log_analyzer!
119
+ RequestLogAnalyzer::Controller.build(arguments, terminal_width).run!
120
+
121
+ puts
122
+ puts "Thanks for using request-log-analyzer"
123
+ end
@@ -0,0 +1,60 @@
1
+ # Colorize a text output with the given color if.
2
+ # <tt>text</tt> The text to colorize.
3
+ # <tt>color_code</tt> The color code string to set
4
+ # <tt>color</tt> Does not color if false. Defaults to false
5
+ def colorize(text, color_code, color = false)
6
+ color ? "#{color_code}#{text}\e[0m" : text
7
+ end
8
+
9
+ # Draw a red line of text
10
+ def red(text, color = false)
11
+ colorize(text, "\e[31m", color)
12
+ end
13
+
14
+ # Draw a Green line of text
15
+ def green(text, color = false)
16
+ colorize(text, "\e[32m", color)
17
+ end
18
+
19
+ # Draw a Yellow line of text
20
+ def yellow(text, color = false)
21
+ colorize(text, "\e[33m", color)
22
+ end
23
+
24
+ # Draw a Yellow line of text
25
+ def blue(text, color = false)
26
+ colorize(text, "\e[34m", color)
27
+ end
28
+
29
+ def white(text, color = false)
30
+ colorize(text, "\e[37m", color)
31
+ end
32
+
33
+
34
+ #STYLE = {
35
+ # :default => “33[0m”,
36
+ # # styles
37
+ # :bold => “33[1m”,
38
+ # :underline => “33[4m”,
39
+ # :blink => “33[5m”,
40
+ # :reverse => “33[7m”,
41
+ # :concealed => “33[8m”,
42
+ # # font colors
43
+ # :black => “33[30m”,
44
+ # :red => “33[31m”,
45
+ # :green => “33[32m”,
46
+ # :yellow => “33[33m”,
47
+ # :blue => “33[34m”,
48
+ # :magenta => “33[35m”,
49
+ # :cyan => “33[36m”,
50
+ # :white => “33[37m”,
51
+ # # background colors
52
+ # :on_black => “33[40m”,
53
+ # :on_red => “33[41m”,
54
+ # :on_green => “33[42m”,
55
+ # :on_yellow => “33[43m”,
56
+ # :on_blue => “33[44m”,
57
+ # :on_magenta => “33[45m”,
58
+ # :on_cyan => “33[46m”,
59
+ # :on_white => “33[47m” }
60
+ #
@@ -0,0 +1,301 @@
1
+ module CommandLine
2
+
3
+ class Option
4
+
5
+ attr_reader :name, :alias
6
+ attr_reader :parameter_count
7
+ attr_reader :default_value
8
+
9
+ # Rewrites a command line keyword by replacing the underscores with dashes
10
+ # <tt>sym</tt> The symbol to rewrite
11
+ def self.rewrite(sym)
12
+ sym.to_s.gsub(/_/, '-').to_sym
13
+ end
14
+
15
+ # Initialize new CommandLine::Option
16
+ # <tt>name</tt> The name of the flag
17
+ # <tt>definition</tt> The definition of the flag.
18
+ def initialize(name, definition = {})
19
+ @name = CommandLine::Option.rewrite(name)
20
+ @alias = definition[:alias].to_sym if definition[:alias]
21
+ @required = definition.has_key?(:required) && definition[:required] == true
22
+ @parameter_count = definition[:parameters] || 1
23
+ @multiple = definition[:multiple] || false
24
+ @default_value = definition[:default] || false
25
+ end
26
+
27
+ def parse(arguments_parser)
28
+ if @parameter_count == 0
29
+ return true
30
+ elsif @parameter_count == 1
31
+ parameter = arguments_parser.next_parameter
32
+ raise CommandLine::ParameterExpected, self if parameter.nil?
33
+ return parameter
34
+ elsif @parameter_count == :any
35
+ parameters = []
36
+ while parameter = arguments_parser.next_parameter && parameter != '--'
37
+ parameters << parameter
38
+ end
39
+ return parameters
40
+ else
41
+ parameters = []
42
+ @parameter_count.times do |n|
43
+ parameter = arguments_parser.next_parameter
44
+ raise CommandLine::ParameterExpected, self if parameter.nil?
45
+ parameters << parameter
46
+ end
47
+ return parameters
48
+ end
49
+ end
50
+
51
+ def =~(test)
52
+ [@name, @alias].include?(CommandLine::Option.rewrite(test))
53
+ end
54
+
55
+ # Argument representation of the flag (--fast)
56
+ def to_option
57
+ "--#{@name}"
58
+ end
59
+
60
+ # Argument alias representation of the flag (-f)
61
+ def to_alias
62
+ "-#{@alias}"
63
+ end
64
+
65
+ # Check if flag has an alias
66
+ def has_alias?
67
+ !@alias.nil?
68
+ end
69
+
70
+ # Check if flag is required
71
+ def required?
72
+ @required
73
+ end
74
+
75
+ # Check if flag is optional
76
+ def optional?
77
+ !@required
78
+ end
79
+
80
+ def multiple?
81
+ @multiple
82
+ end
83
+
84
+ def has_default?
85
+ !@default_value.nil?
86
+ end
87
+ end
88
+
89
+ class Arguments
90
+
91
+ class Definition
92
+
93
+ ENDLESS_PARAMETERS = 99999
94
+
95
+ attr_reader :commands, :options, :parameters
96
+
97
+ def initialize(parent)
98
+ @parent = parent
99
+ @options = {}
100
+ @commands = {}
101
+ @parameters = nil
102
+ end
103
+
104
+ def [](option_name)
105
+ option_symbol = CommandLine::Option.rewrite(option_name)
106
+ if the_option = @options.detect { |(name, odef)| odef =~ option_symbol }
107
+ the_option[1]
108
+ else
109
+ raise CommandLine::UnknownOption, option_name
110
+ end
111
+ end
112
+
113
+ def minimum_parameters=(count_specifier)
114
+ @parameters = count_specifier..ENDLESS_PARAMETERS
115
+ end
116
+
117
+ def parameters=(count_specifier)
118
+ @parameters = count_specifier
119
+ end
120
+
121
+ alias :files= :parameters=
122
+
123
+ def option(name, options = {})
124
+ clo = CommandLine::Option.new(name, options)
125
+ @options[clo.name] = clo
126
+ end
127
+
128
+ def switch(name, switch_alias = nil)
129
+ option(name, :alias => switch_alias, :parameters => 0)
130
+ end
131
+
132
+ def command(name, &block)
133
+ command_definition = Definition.new(self)
134
+ yield(command_definition) if block_given?
135
+ @commands[CommandLine::Option.rewrite(name)] = command_definition
136
+ end
137
+
138
+ def has_command?(command)
139
+ @commands[CommandLine::Option.rewrite(command)]
140
+ end
141
+ end
142
+
143
+ OPTION_REGEXP = /^\-\-([A-z0-9-]+)$/;
144
+ ALIASES_REGEXP = /^\-([A-z0-9]+)$/
145
+
146
+ attr_reader :definition
147
+ attr_reader :tokens
148
+ attr_reader :command, :options, :parameters
149
+
150
+ def self.parse(tokens = $*, &block)
151
+ cla = Arguments.new
152
+ cla.define(&block)
153
+ return cla.parse!(tokens)
154
+ end
155
+
156
+ def initialize
157
+ @tokens = []
158
+ @definition = Definition.new(self)
159
+ @current_definition = @definition
160
+ end
161
+
162
+ def define(&block)
163
+ yield(@definition)
164
+ end
165
+
166
+ def [](option)
167
+ if the_option = @options.detect { |(key, value)| key =~ option }
168
+ the_option[1]
169
+ else
170
+ @current_definition[option].default_value
171
+ end
172
+ end
173
+
174
+ def next_token
175
+ @current_token = @tokens.shift
176
+ return @current_token
177
+ end
178
+
179
+ def next_parameter
180
+ parameter_candidate = @tokens.first
181
+ parameter = (parameter_candidate.nil? || OPTION_REGEXP =~ parameter_candidate || ALIASES_REGEXP =~ parameter_candidate) ? nil : @tokens.shift
182
+ return parameter
183
+ end
184
+
185
+ def parse!(tokens)
186
+ @current_definition = @definition
187
+ @first_token = true
188
+ @tokens = tokens.clone
189
+
190
+ @options = {}
191
+ @parameters = []
192
+ @command = nil
193
+
194
+ prepare_result!
195
+
196
+ while next_token
197
+
198
+ if @first_token && command_definition = @definition.has_command?(@current_token)
199
+ @current_definition = command_definition
200
+ @command = CommandLine::Option.rewrite(@current_token)
201
+ else
202
+ case @current_token
203
+ when ALIASES_REGEXP; handle_alias_expansion($1)
204
+ when OPTION_REGEXP; handle_option($1)
205
+ else; handle_other_parameter(@current_token)
206
+ end
207
+ @first_token = false
208
+ end
209
+
210
+ end
211
+
212
+ validate_arguments!
213
+
214
+ return self
215
+ end
216
+
217
+ protected
218
+
219
+ def prepare_result!
220
+ multiple_options = Hash[*@current_definition.options.select { |name, o| o.multiple? }.flatten]
221
+ multiple_options.each { |name, definition| @options[definition] = [] }
222
+ end
223
+
224
+ def validate_arguments!
225
+ if @current_definition.parameters && !(@current_definition.parameters === @parameters.length)
226
+ raise CommandLine::ParametersOutOfRange.new(@current_definition.parameters, @parameters.length)
227
+ end
228
+
229
+ required_options = Hash[*@current_definition.options.select { |name, o| o.required? }.flatten]
230
+ required_options.each do |name, definition|
231
+ raise CommandLine::RequiredOptionMissing, definition unless self[name]
232
+ end
233
+ end
234
+
235
+ def handle_alias_expansion(aliases)
236
+ aliases.reverse.scan(/./) do |alias_char|
237
+ if option_definition = @current_definition[alias_char]
238
+ @tokens.unshift(option_definition.to_option)
239
+ else
240
+ raise CommandLine::UnknownOption, alias_char
241
+ end
242
+ end
243
+ end
244
+
245
+ def handle_other_parameter(parameter)
246
+ @parameters << parameter
247
+ end
248
+
249
+ def handle_option(option_name)
250
+ option_definition = @current_definition[option_name]
251
+ raise CommandLine::UnknownOption, option_name if option_definition.nil?
252
+
253
+ if option_definition.multiple?
254
+ @options[option_definition] << option_definition.parse(self)
255
+ else
256
+ @options[option_definition] = option_definition.parse(self)
257
+ end
258
+ end
259
+
260
+ end
261
+
262
+ # Commandline parsing errors and exceptions
263
+ class Error < Exception
264
+ end
265
+
266
+ # Missing a required flag
267
+ class RequiredOptionMissing < CommandLine::Error
268
+ def initialize(option)
269
+ super("You have to provide the #{option.name} option!")
270
+ end
271
+ end
272
+
273
+ # Missing a required file
274
+ class ParametersOutOfRange < CommandLine::Error
275
+ def initialize(expected, actual)
276
+ if expected.kind_of?(Range)
277
+ if expected.end == CommandLine::Arguments::Definition::ENDLESS_PARAMETERS
278
+ super("The command expected at least #{expected.begin} parameters, but found #{actual}!")
279
+ else
280
+ super("The command expected between #{expected.begin} and #{expected.end} parameters, but found #{actual}!")
281
+ end
282
+ else
283
+ super("The command expected #{expected} parameters, but found #{actual}!")
284
+ end
285
+ end
286
+ end
287
+
288
+ # Missing a required flag argument
289
+ class ParameterExpected < CommandLine::Error
290
+ def initialize(option)
291
+ super("The option #{option.inspect} expects a parameter!")
292
+ end
293
+ end
294
+
295
+ # Encountered an unkown flag
296
+ class UnknownOption < CommandLine::Error
297
+ def initialize(option_identifier)
298
+ super("#{option_identifier.inspect} not recognized as a valid option!")
299
+ end
300
+ end
301
+ end