request-log-analyzer 1.0.2

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