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.
- data/.gitignore +10 -0
- data/DESIGN.rdoc +41 -0
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/Rakefile +8 -0
- data/bin/request-log-analyzer +114 -0
- data/lib/cli/command_line_arguments.rb +301 -0
- data/lib/cli/database_console.rb +26 -0
- data/lib/cli/database_console_init.rb +43 -0
- data/lib/cli/progressbar.rb +213 -0
- data/lib/cli/tools.rb +46 -0
- data/lib/request_log_analyzer.rb +44 -0
- data/lib/request_log_analyzer/aggregator.rb +49 -0
- data/lib/request_log_analyzer/aggregator/database_inserter.rb +83 -0
- data/lib/request_log_analyzer/aggregator/echo.rb +29 -0
- data/lib/request_log_analyzer/aggregator/summarizer.rb +175 -0
- data/lib/request_log_analyzer/controller.rb +332 -0
- data/lib/request_log_analyzer/database.rb +102 -0
- data/lib/request_log_analyzer/database/base.rb +115 -0
- data/lib/request_log_analyzer/database/connection.rb +38 -0
- data/lib/request_log_analyzer/database/request.rb +22 -0
- data/lib/request_log_analyzer/database/source.rb +13 -0
- data/lib/request_log_analyzer/database/warning.rb +14 -0
- data/lib/request_log_analyzer/file_format.rb +160 -0
- data/lib/request_log_analyzer/file_format/amazon_s3.rb +71 -0
- data/lib/request_log_analyzer/file_format/apache.rb +141 -0
- data/lib/request_log_analyzer/file_format/merb.rb +67 -0
- data/lib/request_log_analyzer/file_format/rack.rb +11 -0
- data/lib/request_log_analyzer/file_format/rails.rb +176 -0
- data/lib/request_log_analyzer/file_format/rails_development.rb +12 -0
- data/lib/request_log_analyzer/filter.rb +30 -0
- data/lib/request_log_analyzer/filter/anonymize.rb +39 -0
- data/lib/request_log_analyzer/filter/field.rb +42 -0
- data/lib/request_log_analyzer/filter/timespan.rb +45 -0
- data/lib/request_log_analyzer/line_definition.rb +111 -0
- data/lib/request_log_analyzer/log_processor.rb +99 -0
- data/lib/request_log_analyzer/mailer.rb +62 -0
- data/lib/request_log_analyzer/output.rb +113 -0
- data/lib/request_log_analyzer/output/fixed_width.rb +220 -0
- data/lib/request_log_analyzer/output/html.rb +184 -0
- data/lib/request_log_analyzer/request.rb +175 -0
- data/lib/request_log_analyzer/source.rb +72 -0
- data/lib/request_log_analyzer/source/database_loader.rb +87 -0
- data/lib/request_log_analyzer/source/log_parser.rb +274 -0
- data/lib/request_log_analyzer/tracker.rb +206 -0
- data/lib/request_log_analyzer/tracker/duration.rb +104 -0
- data/lib/request_log_analyzer/tracker/frequency.rb +95 -0
- data/lib/request_log_analyzer/tracker/hourly_spread.rb +107 -0
- data/lib/request_log_analyzer/tracker/timespan.rb +81 -0
- data/lib/request_log_analyzer/tracker/traffic.rb +106 -0
- data/request-log-analyzer.gemspec +40 -0
- data/spec/database.yml +23 -0
- data/spec/fixtures/apache_combined.log +5 -0
- data/spec/fixtures/apache_common.log +10 -0
- data/spec/fixtures/decompression.log +12 -0
- data/spec/fixtures/decompression.log.bz2 +0 -0
- data/spec/fixtures/decompression.log.gz +0 -0
- data/spec/fixtures/decompression.log.zip +0 -0
- data/spec/fixtures/decompression.tar.gz +0 -0
- data/spec/fixtures/decompression.tgz +0 -0
- data/spec/fixtures/header_and_footer.log +6 -0
- data/spec/fixtures/merb.log +84 -0
- data/spec/fixtures/merb_prefixed.log +9 -0
- data/spec/fixtures/multiple_files_1.log +5 -0
- data/spec/fixtures/multiple_files_2.log +2 -0
- data/spec/fixtures/rails.db +0 -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/integration/command_line_usage_spec.rb +84 -0
- data/spec/integration/munin_plugins_rails_spec.rb +58 -0
- data/spec/integration/scout_spec.rb +151 -0
- data/spec/lib/helpers.rb +52 -0
- data/spec/lib/macros.rb +18 -0
- data/spec/lib/matchers.rb +77 -0
- data/spec/lib/mocks.rb +76 -0
- data/spec/lib/testing_format.rb +46 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/unit/aggregator/database_inserter_spec.rb +93 -0
- data/spec/unit/aggregator/summarizer_spec.rb +26 -0
- data/spec/unit/controller/controller_spec.rb +41 -0
- data/spec/unit/controller/log_processor_spec.rb +18 -0
- data/spec/unit/database/base_class_spec.rb +183 -0
- data/spec/unit/database/connection_spec.rb +34 -0
- data/spec/unit/database/database_spec.rb +133 -0
- data/spec/unit/file_format/amazon_s3_format_spec.rb +49 -0
- data/spec/unit/file_format/apache_format_spec.rb +203 -0
- data/spec/unit/file_format/file_format_api_spec.rb +69 -0
- data/spec/unit/file_format/line_definition_spec.rb +75 -0
- data/spec/unit/file_format/merb_format_spec.rb +52 -0
- data/spec/unit/file_format/rails_format_spec.rb +164 -0
- data/spec/unit/filter/anonymize_filter_spec.rb +21 -0
- data/spec/unit/filter/field_filter_spec.rb +66 -0
- data/spec/unit/filter/filter_spec.rb +17 -0
- data/spec/unit/filter/timespan_filter_spec.rb +58 -0
- data/spec/unit/mailer_spec.rb +30 -0
- data/spec/unit/request_spec.rb +111 -0
- data/spec/unit/source/log_parser_spec.rb +119 -0
- data/spec/unit/tracker/duration_tracker_spec.rb +130 -0
- data/spec/unit/tracker/frequency_tracker_spec.rb +88 -0
- data/spec/unit/tracker/hourly_spread_spec.rb +79 -0
- data/spec/unit/tracker/timespan_tracker_spec.rb +73 -0
- data/spec/unit/tracker/tracker_api_spec.rb +124 -0
- data/spec/unit/tracker/traffic_tracker_spec.rb +107 -0
- data/tasks/github-gem.rake +323 -0
- data/tasks/request_log_analyzer.rake +26 -0
- metadata +220 -0
data/.gitignore
ADDED
data/DESIGN.rdoc
ADDED
|
@@ -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.
|
data/README.rdoc
ADDED
|
@@ -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
|
data/Rakefile
ADDED
|
@@ -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
|