ngmoco-request-log-analyzer 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. data/.gitignore +10 -0
  2. data/DESIGN.rdoc +41 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +39 -0
  5. data/Rakefile +8 -0
  6. data/bin/request-log-analyzer +114 -0
  7. data/lib/cli/command_line_arguments.rb +301 -0
  8. data/lib/cli/database_console.rb +26 -0
  9. data/lib/cli/database_console_init.rb +43 -0
  10. data/lib/cli/progressbar.rb +213 -0
  11. data/lib/cli/tools.rb +46 -0
  12. data/lib/request_log_analyzer.rb +44 -0
  13. data/lib/request_log_analyzer/aggregator.rb +49 -0
  14. data/lib/request_log_analyzer/aggregator/database_inserter.rb +83 -0
  15. data/lib/request_log_analyzer/aggregator/echo.rb +29 -0
  16. data/lib/request_log_analyzer/aggregator/summarizer.rb +175 -0
  17. data/lib/request_log_analyzer/controller.rb +332 -0
  18. data/lib/request_log_analyzer/database.rb +102 -0
  19. data/lib/request_log_analyzer/database/base.rb +115 -0
  20. data/lib/request_log_analyzer/database/connection.rb +38 -0
  21. data/lib/request_log_analyzer/database/request.rb +22 -0
  22. data/lib/request_log_analyzer/database/source.rb +13 -0
  23. data/lib/request_log_analyzer/database/warning.rb +14 -0
  24. data/lib/request_log_analyzer/file_format.rb +160 -0
  25. data/lib/request_log_analyzer/file_format/amazon_s3.rb +71 -0
  26. data/lib/request_log_analyzer/file_format/apache.rb +141 -0
  27. data/lib/request_log_analyzer/file_format/merb.rb +67 -0
  28. data/lib/request_log_analyzer/file_format/rack.rb +11 -0
  29. data/lib/request_log_analyzer/file_format/rails.rb +176 -0
  30. data/lib/request_log_analyzer/file_format/rails_development.rb +12 -0
  31. data/lib/request_log_analyzer/filter.rb +30 -0
  32. data/lib/request_log_analyzer/filter/anonymize.rb +39 -0
  33. data/lib/request_log_analyzer/filter/field.rb +42 -0
  34. data/lib/request_log_analyzer/filter/timespan.rb +45 -0
  35. data/lib/request_log_analyzer/line_definition.rb +111 -0
  36. data/lib/request_log_analyzer/log_processor.rb +99 -0
  37. data/lib/request_log_analyzer/mailer.rb +62 -0
  38. data/lib/request_log_analyzer/output.rb +113 -0
  39. data/lib/request_log_analyzer/output/fixed_width.rb +220 -0
  40. data/lib/request_log_analyzer/output/html.rb +184 -0
  41. data/lib/request_log_analyzer/request.rb +175 -0
  42. data/lib/request_log_analyzer/source.rb +72 -0
  43. data/lib/request_log_analyzer/source/database_loader.rb +87 -0
  44. data/lib/request_log_analyzer/source/log_parser.rb +274 -0
  45. data/lib/request_log_analyzer/tracker.rb +206 -0
  46. data/lib/request_log_analyzer/tracker/duration.rb +104 -0
  47. data/lib/request_log_analyzer/tracker/frequency.rb +95 -0
  48. data/lib/request_log_analyzer/tracker/hourly_spread.rb +107 -0
  49. data/lib/request_log_analyzer/tracker/timespan.rb +81 -0
  50. data/lib/request_log_analyzer/tracker/traffic.rb +106 -0
  51. data/request-log-analyzer.gemspec +40 -0
  52. data/spec/database.yml +23 -0
  53. data/spec/fixtures/apache_combined.log +5 -0
  54. data/spec/fixtures/apache_common.log +10 -0
  55. data/spec/fixtures/decompression.log +12 -0
  56. data/spec/fixtures/decompression.log.bz2 +0 -0
  57. data/spec/fixtures/decompression.log.gz +0 -0
  58. data/spec/fixtures/decompression.log.zip +0 -0
  59. data/spec/fixtures/decompression.tar.gz +0 -0
  60. data/spec/fixtures/decompression.tgz +0 -0
  61. data/spec/fixtures/header_and_footer.log +6 -0
  62. data/spec/fixtures/merb.log +84 -0
  63. data/spec/fixtures/merb_prefixed.log +9 -0
  64. data/spec/fixtures/multiple_files_1.log +5 -0
  65. data/spec/fixtures/multiple_files_2.log +2 -0
  66. data/spec/fixtures/rails.db +0 -0
  67. data/spec/fixtures/rails_1x.log +59 -0
  68. data/spec/fixtures/rails_22.log +12 -0
  69. data/spec/fixtures/rails_22_cached.log +10 -0
  70. data/spec/fixtures/rails_unordered.log +24 -0
  71. data/spec/fixtures/syslog_1x.log +5 -0
  72. data/spec/fixtures/test_file_format.log +13 -0
  73. data/spec/fixtures/test_language_combined.log +14 -0
  74. data/spec/fixtures/test_order.log +16 -0
  75. data/spec/integration/command_line_usage_spec.rb +84 -0
  76. data/spec/integration/munin_plugins_rails_spec.rb +58 -0
  77. data/spec/integration/scout_spec.rb +151 -0
  78. data/spec/lib/helpers.rb +52 -0
  79. data/spec/lib/macros.rb +18 -0
  80. data/spec/lib/matchers.rb +77 -0
  81. data/spec/lib/mocks.rb +76 -0
  82. data/spec/lib/testing_format.rb +46 -0
  83. data/spec/spec_helper.rb +24 -0
  84. data/spec/unit/aggregator/database_inserter_spec.rb +93 -0
  85. data/spec/unit/aggregator/summarizer_spec.rb +26 -0
  86. data/spec/unit/controller/controller_spec.rb +41 -0
  87. data/spec/unit/controller/log_processor_spec.rb +18 -0
  88. data/spec/unit/database/base_class_spec.rb +183 -0
  89. data/spec/unit/database/connection_spec.rb +34 -0
  90. data/spec/unit/database/database_spec.rb +133 -0
  91. data/spec/unit/file_format/amazon_s3_format_spec.rb +49 -0
  92. data/spec/unit/file_format/apache_format_spec.rb +203 -0
  93. data/spec/unit/file_format/file_format_api_spec.rb +69 -0
  94. data/spec/unit/file_format/line_definition_spec.rb +75 -0
  95. data/spec/unit/file_format/merb_format_spec.rb +52 -0
  96. data/spec/unit/file_format/rails_format_spec.rb +164 -0
  97. data/spec/unit/filter/anonymize_filter_spec.rb +21 -0
  98. data/spec/unit/filter/field_filter_spec.rb +66 -0
  99. data/spec/unit/filter/filter_spec.rb +17 -0
  100. data/spec/unit/filter/timespan_filter_spec.rb +58 -0
  101. data/spec/unit/mailer_spec.rb +30 -0
  102. data/spec/unit/request_spec.rb +111 -0
  103. data/spec/unit/source/log_parser_spec.rb +119 -0
  104. data/spec/unit/tracker/duration_tracker_spec.rb +130 -0
  105. data/spec/unit/tracker/frequency_tracker_spec.rb +88 -0
  106. data/spec/unit/tracker/hourly_spread_spec.rb +79 -0
  107. data/spec/unit/tracker/timespan_tracker_spec.rb +73 -0
  108. data/spec/unit/tracker/tracker_api_spec.rb +124 -0
  109. data/spec/unit/tracker/traffic_tracker_spec.rb +107 -0
  110. data/tasks/github-gem.rake +323 -0
  111. data/tasks/request_log_analyzer.rake +26 -0
  112. metadata +220 -0
@@ -0,0 +1,10 @@
1
+ .svn/
2
+ .DS_Store
3
+ request-log-analyzer-*.gem
4
+ requests.db
5
+ /pkg
6
+ /doc
7
+ /tmp
8
+ /classes
9
+ /files
10
+ /coverage
@@ -0,0 +1,41 @@
1
+ === Request-log-analyzer
2
+ RLA is set up like a simple pipe and filter system.
3
+
4
+ This allows you to easily add extra reports, filters and outputs.
5
+ -> Aggregator (database)
6
+ Source -> Filter -> Filter -> Aggregator (summary report) -> Output
7
+ -> Aggregator (...)
8
+
9
+ When the pipeline has been constructed, we Start chunk producer (source) and push requests through pipeline.
10
+
11
+ Controller.start
12
+
13
+ === Source
14
+ RequestLogAnalyzer::Source is an Object that pushes requests into the chain.
15
+ At the moment you can only use the log-parser as a source.
16
+ It accepts files or stdin and can parse then into request objects using a RequestLogAnalyzer::FileFormat definition.
17
+ In the future we want to be able to have a generated request database as source as this will make interactive
18
+ down drilling possible.
19
+
20
+ === Filter
21
+ The filters are all subclasses of the RequestLogAnalyzer::Filter class.
22
+ They accept a request object, manipulate or drop it, and then pass the request object on to the next filter
23
+ in the chain.
24
+ At the moment there are three types of filters available: Anonymize, Field and Timespan.
25
+
26
+ === Aggregator
27
+ The Aggregators all inherit from the RequestLogAnalyzer::Aggregator class.
28
+ All the requests that come out of the Filterchain are fed into all the aggregators in parallel.
29
+ These aggregators can do anything what they want with the given request.
30
+ For example: the Database aggregator will just store all the requests into a SQLite database while the Summarizer will
31
+ generate a wide range of statistical reports from them.
32
+
33
+ === Running the pipeline
34
+ All Aggregators are asked to report what they have done. For example the database will report: I stuffed x requests
35
+ into SQLite database Y. The Summarizer will output its reports.
36
+
37
+ Controller.report
38
+
39
+ The output is pushed to a RequestLogAnalyzer::Output object, which takes care of the output.
40
+ It can generate either ASCII, UTF8 or even HTML output.
41
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008-2009 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,39 @@
1
+ = Request-log-analyzer
2
+
3
+ This is a simple command line tool to analyze request log files in various formats to produce a performance report. Its purpose is to find what actions are best candidates for optimization.
4
+
5
+ * Analyzes Rails request logs, Merb request logs, Apache access logs and more, or parses any other log format you specify.
6
+ * Combines multiple files and decompresses compressed files, which comes in handy if you are using logrotate.
7
+ * Uses several metrics, including cumulative request time, mean request time, process blockers, database and rendering time, HTTP methods and statuses, Rails action cache statistics, etc.) (Sample output: http://wiki.github.com/wvanbergen/request-log-analyzer/sample-output)
8
+ * Low memory footprint and reasonably fast, so it is safe to run on a production server.
9
+ * MIT licensed
10
+
11
+ See the project wiki at http://wiki.github.com/wvanbergen/request-log-analyzer for documentation and additional information.
12
+
13
+ == Installation & basic usage
14
+
15
+ Install request-log-analyzer as a Ruby gem (you might need to run this command
16
+ as root by prepending +sudo+ to it):
17
+
18
+ $ gem install request-log-analyzer
19
+
20
+ To analyze a Rails log file and produce a performance report, run
21
+ request-log-analyzer like this:
22
+
23
+ $ request-log-analyzer log/production.log
24
+
25
+ For more details, other file formats, and available command line options, see the project's wiki at http://wiki.github.com/wvanbergen/request-log-analyzer
26
+
27
+ == Additional information
28
+
29
+ Request-log-analyzer was designed and built by Willem van Bergen and Bart ten
30
+ Brinke.
31
+
32
+ Do you have a rails application that is not performing as it should? If you need
33
+ an expert to analyze your application, feel free to contact either Willem van
34
+ Bergen (willem@railsdoctors.com) or Bart ten Brinke (bart@railsdoctors.com).
35
+
36
+ * Project wiki at GitHub: http://wiki.github.com/wvanbergen/request-log-analyzer
37
+ * railsdoctors homepage: http://railsdoctors.com
38
+ * wvanbergen's blog posts: http://techblog.floorplanner.com/tag/request-log-analyzer
39
+ * barttenbrinke's blog posts: http://movesonrails.com/articles/tag/analyzer
@@ -0,0 +1,8 @@
1
+ Dir[File.dirname(__FILE__) + "/tasks/*.rake"].each { |file| load(file) }
2
+
3
+ # Create rake tasks for a gem manages by github. The tasks are created in the
4
+ # gem namespace
5
+ GithubGem::RakeTasks.new(:gem)
6
+
7
+ # Set the RSpec runner with specdoc output as default task.
8
+ task :default => "spec:specdoc"
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
5
+ require 'request_log_analyzer'
6
+ require 'cli/command_line_arguments'
7
+ require 'cli/progressbar'
8
+ require 'cli/tools'
9
+
10
+ # Parse the arguments given via commandline
11
+ begin
12
+ arguments = CommandLine::Arguments.parse do |command_line|
13
+
14
+ command_line.command(:install) do |install|
15
+ install.parameters = 1
16
+ end
17
+
18
+ command_line.command(:console) do |cons|
19
+ cons.option(:database, :alias => :d, :required => true)
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.option(:format, :alias => :f, :default => 'rails')
31
+ command_line.option(:apache_format)
32
+ command_line.option(:rails_format)
33
+
34
+ command_line.option(:file, :alias => :e)
35
+ command_line.option(:mail, :alias => :m)
36
+ command_line.option(:parse_strategy, :default => 'assume-correct')
37
+ command_line.option(:dump)
38
+
39
+ command_line.option(:aggregator, :alias => :a, :multiple => true)
40
+
41
+ command_line.option(:database, :alias => :d)
42
+ command_line.switch(:reset_database)
43
+
44
+ # filtering options
45
+ command_line.option(:select, :multiple => true, :parameters => 2)
46
+ command_line.option(:reject, :multiple => true, :parameters => 2)
47
+ command_line.option(:after)
48
+ command_line.option(:before)
49
+
50
+ command_line.switch(:boring, :b)
51
+ command_line.option(:output, :alias => :o, :default => 'FixedWidth')
52
+ command_line.option(:report_width, :default => terminal_width - 1)
53
+ command_line.option(:report_amount, :default => 20)
54
+ command_line.option(:report_sort, :default => 'sum,mean')
55
+
56
+ command_line.switch(:debug)
57
+ command_line.switch(:no_progress)
58
+
59
+ command_line.minimum_parameters = 1
60
+ end
61
+
62
+ rescue CommandLine::Error => e
63
+ puts "Request-log-analyzer, by Willem van Bergen and Bart ten Brinke - version #{RequestLogAnalyzer::VERSION}"
64
+ puts "Website: http://railsdoctors.com"
65
+ puts
66
+ puts "ARGUMENT ERROR: " + e.message if e.message
67
+ puts
68
+ puts "Usage: request-log-analyzer [LOGFILES*] <OPTIONS>"
69
+ puts
70
+ puts "Input options:"
71
+ puts " --format <format>, -f: Uses the specified log file format. Defaults to rails."
72
+ puts " --after <date> Only consider requests from <date> or later."
73
+ puts " --before <date> Only consider requests before <date>."
74
+ puts " --select <field> <value> Only consider requests where <field> matches <value>."
75
+ puts " --reject <field> <value> Only consider requests where <field> does not match <value>."
76
+ puts
77
+ puts "Output options:"
78
+ puts " --boring, -b Output reports without ASCII colors."
79
+ puts " --database <filename>, -d: Creates an SQLite3 database of all the parsed request information."
80
+ puts " --debug Print debug information while parsing."
81
+ puts " --file <filename> Output to file."
82
+ puts " --mail <emailaddress> Send report to an email address."
83
+ puts " --output <format> Output format. Supports 'HTML' and 'FixedWidth' (default)"
84
+ puts " --dump <filename> Dump the YAML formatted results in the given file"
85
+ puts
86
+ puts "Examples:"
87
+ puts " request-log-analyzer development.log"
88
+ puts " request-log-analyzer -b mongrel.0.log mongrel.1.log mongrel.2.log "
89
+ puts " request-log-analyzer --format merb -d requests.db production.log"
90
+ puts
91
+ puts "To install rake tasks in your Rails application, "
92
+ puts "run the following command in your application's root directory:"
93
+ puts
94
+ puts " request-log-analyzer install rails"
95
+ exit(0)
96
+ end
97
+
98
+ case arguments.command
99
+ when :install
100
+ install_rake_tasks(arguments.parameters[0])
101
+ when :console
102
+ require 'cli/database_console'
103
+ DatabaseConsole.new(arguments).run!
104
+ when :strip
105
+ require File.dirname(__FILE__) + '/../lib/request_log_analyzer/log_processor'
106
+ RequestLogAnalyzer::LogProcessor.build(:strip, arguments).run!
107
+ else
108
+ puts "Request-log-analyzer, by Willem van Bergen and Bart ten Brinke - version #{RequestLogAnalyzer::VERSION}"
109
+ puts "Website: http://railsdoctors.com"
110
+ puts
111
+
112
+ # Run the request_log_analyzer!
113
+ RequestLogAnalyzer::Controller.build_from_arguments(arguments).run!
114
+ end
@@ -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-Za-z0-9-]+)$/;
144
+ ALIASES_REGEXP = /^\-([A-Aa-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