ngmoco-request-log-analyzer 1.4.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 (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