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.
- data/DESIGN +14 -0
- data/HACKING +7 -0
- data/LICENSE +20 -0
- data/README.textile +36 -0
- data/Rakefile +5 -0
- data/bin/request-log-analyzer +123 -0
- data/lib/cli/bashcolorizer.rb +60 -0
- data/lib/cli/command_line_arguments.rb +301 -0
- data/lib/cli/progressbar.rb +236 -0
- data/lib/request_log_analyzer.rb +14 -0
- data/lib/request_log_analyzer/aggregator/base.rb +45 -0
- data/lib/request_log_analyzer/aggregator/database.rb +148 -0
- data/lib/request_log_analyzer/aggregator/echo.rb +25 -0
- data/lib/request_log_analyzer/aggregator/summarizer.rb +116 -0
- data/lib/request_log_analyzer/controller.rb +201 -0
- data/lib/request_log_analyzer/file_format.rb +81 -0
- data/lib/request_log_analyzer/file_format/merb.rb +33 -0
- data/lib/request_log_analyzer/file_format/rails.rb +90 -0
- data/lib/request_log_analyzer/filter/base.rb +29 -0
- data/lib/request_log_analyzer/filter/field.rb +36 -0
- data/lib/request_log_analyzer/filter/timespan.rb +32 -0
- data/lib/request_log_analyzer/line_definition.rb +159 -0
- data/lib/request_log_analyzer/log_parser.rb +173 -0
- data/lib/request_log_analyzer/log_processor.rb +121 -0
- data/lib/request_log_analyzer/request.rb +95 -0
- data/lib/request_log_analyzer/source/base.rb +42 -0
- data/lib/request_log_analyzer/source/log_file.rb +170 -0
- data/lib/request_log_analyzer/tracker/base.rb +54 -0
- data/lib/request_log_analyzer/tracker/category.rb +71 -0
- data/lib/request_log_analyzer/tracker/duration.rb +81 -0
- data/lib/request_log_analyzer/tracker/hourly_spread.rb +80 -0
- data/lib/request_log_analyzer/tracker/timespan.rb +54 -0
- data/spec/controller_spec.rb +40 -0
- data/spec/database_inserter_spec.rb +101 -0
- data/spec/file_format_spec.rb +78 -0
- data/spec/file_formats/spec_format.rb +26 -0
- data/spec/filter_spec.rb +137 -0
- data/spec/fixtures/merb.log +84 -0
- data/spec/fixtures/multiple_files_1.log +5 -0
- data/spec/fixtures/multiple_files_2.log +2 -0
- data/spec/fixtures/rails_1x.log +59 -0
- data/spec/fixtures/rails_22.log +12 -0
- data/spec/fixtures/rails_22_cached.log +10 -0
- data/spec/fixtures/rails_unordered.log +24 -0
- data/spec/fixtures/syslog_1x.log +5 -0
- data/spec/fixtures/test_file_format.log +13 -0
- data/spec/fixtures/test_language_combined.log +14 -0
- data/spec/fixtures/test_order.log +16 -0
- data/spec/line_definition_spec.rb +124 -0
- data/spec/log_parser_spec.rb +68 -0
- data/spec/log_processor_spec.rb +57 -0
- data/spec/merb_format_spec.rb +38 -0
- data/spec/rails_format_spec.rb +76 -0
- data/spec/request_spec.rb +72 -0
- data/spec/spec_helper.rb +67 -0
- data/spec/summarizer_spec.rb +9 -0
- data/tasks/github-gem.rake +177 -0
- data/tasks/request_log_analyzer.rake +10 -0
- data/tasks/rspec.rake +6 -0
- 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
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.
|
data/README.textile
ADDED
@@ -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/
|
data/Rakefile
ADDED
@@ -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
|