wvanbergen-request-log-analyzer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README +119 -0
- data/Rakefile +43 -0
- data/TODO +20 -0
- data/bin/request-log-analyzer +95 -0
- data/bin/request-log-database +79 -0
- data/lib/bashcolorizer.rb +60 -0
- data/lib/command_line/arguments.rb +129 -0
- data/lib/command_line/exceptions.rb +37 -0
- data/lib/command_line/flag.rb +51 -0
- data/lib/rails_analyzer/log_parser.rb +83 -0
- data/lib/rails_analyzer/record_inserter.rb +161 -0
- data/lib/rails_analyzer/summarizer.rb +121 -0
- data/lib/ruby-progressbar/progressbar.en.rd +103 -0
- data/lib/ruby-progressbar/progressbar.ja.rd +100 -0
- data/lib/ruby-progressbar/progressbar.rb +236 -0
- data/output/blockers.rb +11 -0
- data/output/errors.rb +9 -0
- data/output/hourly_spread.rb +28 -0
- data/output/mean_db_time.rb +7 -0
- data/output/mean_rendering_time.rb +7 -0
- data/output/mean_time.rb +7 -0
- data/output/most_requested.rb +6 -0
- data/output/timespan.rb +9 -0
- data/output/total_db_time.rb +6 -0
- data/output/total_time.rb +6 -0
- data/output/usage.rb +14 -0
- data/test/log_fragments/fragment_1.log +59 -0
- data/test/log_fragments/fragment_2.log +5 -0
- data/test/log_parser_test.rb +85 -0
- data/test/record_inserter_test.rb +42 -0
- data/test/tasks.rake +8 -0
- metadata +95 -0
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
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
Request log analyzer
|
2
|
+
--------------------------------
|
3
|
+
|
4
|
+
This is a simple command line tool to analyze request log files. At this moment,
|
5
|
+
it only supports Rails log files, but Merb log files are planned to be supported
|
6
|
+
as well. Its purpose is to find what actions are best candidates for optimization.
|
7
|
+
|
8
|
+
This tool will parse all requests in the logfile and aggregate the
|
9
|
+
information. Once it is finished parsing the log file, it will show the
|
10
|
+
requests that take op most server time. Different metrics are used (cumulative
|
11
|
+
time, average time, blockers, DB time, etc)
|
12
|
+
|
13
|
+
|
14
|
+
Installation
|
15
|
+
--------------------------------
|
16
|
+
gem sources -a http://gems.github.com
|
17
|
+
sudo gem install wvanbergen-request-log-analyzer
|
18
|
+
|
19
|
+
Usage
|
20
|
+
--------------------------------
|
21
|
+
|
22
|
+
Usage: request-log-analyzer [FILE] [OPTION]
|
23
|
+
Analyze the given log FILE with the given OPTION
|
24
|
+
Example: request-log-analyzer mongrel.log
|
25
|
+
|
26
|
+
--fast, -t: Only use completed requests
|
27
|
+
--guess-database-time, -g: Guesses the database duration of requests if they are not in the log
|
28
|
+
--output, -o: Comma-separated list of reports to show
|
29
|
+
--amount, -c: Displays the top <amount> elements in the reports
|
30
|
+
--colorize, -z: Fancy bash coloring
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
Example
|
35
|
+
--------------------------------
|
36
|
+
|
37
|
+
Note that this example was shortened for your viewing pleasure.
|
38
|
+
$ request-log-analyzer /var/log/my_app.log
|
39
|
+
|
40
|
+
Request log analyzer, by Willem van Bergen and Bart ten Brinke
|
41
|
+
|
42
|
+
Processing all log lines...
|
43
|
+
========================================================================
|
44
|
+
Successfully analyzed 58908 requests from log file
|
45
|
+
|
46
|
+
Timestamp first request: 2008-07-13T06:25:58+00:00
|
47
|
+
Timestamp last request: 2008-07-20T06:18:53+00:00
|
48
|
+
Total time analyzed: 7 days
|
49
|
+
|
50
|
+
Top 10 most requested actions
|
51
|
+
========================================================================
|
52
|
+
/overview/:date/ : 19359 requests
|
53
|
+
/overview/day/:date/ : 6365 requests
|
54
|
+
/overview/:date/set/ : 5589 requests
|
55
|
+
/overview/ : 3985 requests
|
56
|
+
/clients/:id/ : 1976 requests
|
57
|
+
........
|
58
|
+
|
59
|
+
Top 10 actions by time - cumulative
|
60
|
+
========================================================================
|
61
|
+
/overview/:date/ : 9044.582s [19359 requests]
|
62
|
+
/overview/ : 8478.767s [3985 requests]
|
63
|
+
/overview/:date/set/ : 3309.041s [5589 requests]
|
64
|
+
/clients/:id/products/:id/ : 1479.911s [924 requests]
|
65
|
+
/clients/:id/ : 750.080s [1976 requests]
|
66
|
+
........
|
67
|
+
|
68
|
+
Top 10 actions by time - per request mean
|
69
|
+
========================================================================
|
70
|
+
/overview/ : 2.128s [3985 requests]
|
71
|
+
/clients/:id/products/:id/ : 1.602s [924 requests]
|
72
|
+
/overview/:date/set/ : 0.592s [5589 requests]
|
73
|
+
/overview/:date/ : 0.467s [19359 requests]
|
74
|
+
/clients/:id/ : 0.380s [1976 requests]
|
75
|
+
........
|
76
|
+
|
77
|
+
Top 10 worst DB offenders - cumulative time
|
78
|
+
========================================================================
|
79
|
+
/overview/:date/ : 8773.993s [19359 requests]
|
80
|
+
/overview/ : 8394.754s [3985 requests]
|
81
|
+
/overview/:date/set/ : 3307.928s [5589 requests]
|
82
|
+
/clients/:id/products/:id/ : 1425.220s [924 requests]
|
83
|
+
/clients/:id/ : 535.229s [1976 requests]
|
84
|
+
........
|
85
|
+
|
86
|
+
Top 10 worst DB offenders - mean time
|
87
|
+
========================================================================
|
88
|
+
/overview/:id/:id/:id/print/ : 6.994s [448 requests]
|
89
|
+
/overview/ : 2.128s [3985 requests]
|
90
|
+
/clients/:id/products/:id/ : 1.602s [924 requests]
|
91
|
+
/overview/:date/set/ : 0.592s [5589 requests]
|
92
|
+
/overview/:date/ : 0.467s [19359 requests]
|
93
|
+
........
|
94
|
+
|
95
|
+
Mongrel process blockers (> 1.0 seconds) - frequency
|
96
|
+
========================================================================
|
97
|
+
/overview/:date/ : 7494.233s [3144 requests]
|
98
|
+
/overview/ : 8320.293s [1549 requests]
|
99
|
+
/overview/:date/set/ : 1149.235s [803 requests]
|
100
|
+
/overview/:id/:id/:id/print/new/ : 613.693s [341 requests]
|
101
|
+
/clients/:id/products/:id/ : 1370.693s [313 requests]
|
102
|
+
........
|
103
|
+
|
104
|
+
Requests graph - per hour
|
105
|
+
========================================================================
|
106
|
+
........
|
107
|
+
7:00 - 2731 : XXXXXXX
|
108
|
+
8:00 - 6139 : XXXXXXXXXXXXXXXX
|
109
|
+
9:00 - 7465 : XXXXXXXXXXXXXXXXXXXX
|
110
|
+
10:00 - 7118 : XXXXXXXXXXXXXXXXXXX
|
111
|
+
11:00 - 7409 : XXXXXXXXXXXXXXXXXXX
|
112
|
+
12:00 - 6450 : XXXXXXXXXXXXXXXXX
|
113
|
+
13:00 - 5377 : XXXXXXXXXXXXXX
|
114
|
+
14:00 - 6058 : XXXXXXXXXXXXXXXX
|
115
|
+
15:00 - 4156 : XXXXXXXXXXX
|
116
|
+
16:00 - 2767 : XXXXXXX
|
117
|
+
17:00 - 1598 : XXXX
|
118
|
+
18:00 - 792 : XX
|
119
|
+
........
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
load 'test/tasks.rake'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
|
9
|
+
namespace :gem do
|
10
|
+
|
11
|
+
desc "Builds a ruby gem for request-log-analyzer"
|
12
|
+
task :build => [:manifest] do
|
13
|
+
system %[gem build request-log-analyzer.gemspec]
|
14
|
+
end
|
15
|
+
|
16
|
+
desc %{Update ".manifest" with the latest list of project filenames. Respect\
|
17
|
+
.gitignore by excluding everything that git ignores. Update `files` and\
|
18
|
+
`test_files` arrays in "*.gemspec" file if it's present.}
|
19
|
+
task :manifest do
|
20
|
+
list = Dir['**/*'].sort
|
21
|
+
spec_file = Dir['*.gemspec'].first
|
22
|
+
list -= [spec_file] if spec_file
|
23
|
+
|
24
|
+
File.read('.gitignore').each_line do |glob|
|
25
|
+
glob = glob.chomp.sub(/^\//, '')
|
26
|
+
list -= Dir[glob]
|
27
|
+
list -= Dir["#{glob}/**/*"] if File.directory?(glob) and !File.symlink?(glob)
|
28
|
+
puts "excluding #{glob}"
|
29
|
+
end
|
30
|
+
|
31
|
+
if spec_file
|
32
|
+
spec = File.read spec_file
|
33
|
+
spec.gsub! /^(\s* s.(test_)?files \s* = \s* )( \[ [^\]]* \] | %w\( [^)]* \) )/mx do
|
34
|
+
assignment = $1
|
35
|
+
bunch = $2 ? list.grep(/^test.*_test\.rb$/) : list
|
36
|
+
'%s%%w(%s)' % [assignment, bunch.join(' ')]
|
37
|
+
end
|
38
|
+
|
39
|
+
File.open(spec_file, 'w') {|f| f << spec }
|
40
|
+
end
|
41
|
+
File.open('.manifest', 'w') {|f| f << list.join("\n") }
|
42
|
+
end
|
43
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
TODO items for Rails-log-analyzer
|
2
|
+
=================================
|
3
|
+
Contact willem AT vanbergen DOT org if you want to help out with the development.
|
4
|
+
|
5
|
+
Summarizer:
|
6
|
+
- Look at request types (GET / POST)
|
7
|
+
|
8
|
+
Database:
|
9
|
+
- Add query functionality for the resulting database file (interactive reports?)
|
10
|
+
- Link request processing line to request completed line
|
11
|
+
|
12
|
+
Rails integration:
|
13
|
+
- Create script that calls request-log-analyzer
|
14
|
+
- Optionally use local or specific routes.rb file to parse URLs
|
15
|
+
- Add rake tasks to Rails application when included
|
16
|
+
|
17
|
+
General:
|
18
|
+
- Add useful rake tasks
|
19
|
+
- Add more tests
|
20
|
+
- World domination
|
@@ -0,0 +1,95 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
require File.dirname(__FILE__) + '/../lib/command_line/arguments'
|
3
|
+
require File.dirname(__FILE__) + '/../lib/rails_analyzer/log_parser'
|
4
|
+
require File.dirname(__FILE__) + '/../lib/rails_analyzer/summarizer'
|
5
|
+
require File.dirname(__FILE__) + '/../lib/bashcolorizer'
|
6
|
+
require File.dirname(__FILE__) + '/../lib/ruby-progressbar/progressbar.rb'
|
7
|
+
|
8
|
+
puts "Request log analyzer, by Willem van Bergen and Bart ten Brinke\n\n"
|
9
|
+
|
10
|
+
# Substitutes variable elements in a url (like the id field) with a fixed string (like ":id")
|
11
|
+
# This is used to aggregate simular requests.
|
12
|
+
# <tt>request</tt> The request to evaluate.
|
13
|
+
# Returns uniformed url string.
|
14
|
+
# Raises on mailformed request.
|
15
|
+
def request_hasher(request)
|
16
|
+
if request[:url]
|
17
|
+
url = request[:url].downcase.split(/^http[s]?:\/\/[A-z0-9\.-]+/).last.split('?').first # only the relevant URL part
|
18
|
+
url << '/' if url[-1] != '/'[0] && url.length > 1 # pad a trailing slash for consistency
|
19
|
+
|
20
|
+
url.gsub!(/\/\d+-\d+-\d+/, '/:date') # Combine all (year-month-day) queries
|
21
|
+
url.gsub!(/\/\d+-\d+/, '/:month') # Combine all date (year-month) queries
|
22
|
+
url.gsub!(/\/\d+/, '/:id') # replace identifiers in URLs
|
23
|
+
|
24
|
+
return url
|
25
|
+
elsif request[:controller] && request[:action]
|
26
|
+
return "#{request[:controller]}##{request[:action]}"
|
27
|
+
else
|
28
|
+
raise 'Cannot hash this request! ' + request.inspect
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Print results using a ASCII table.
|
33
|
+
# <tt>summarizer</tt> The summarizer containg information to draw the table.
|
34
|
+
# <tt>field</tt> The field containing the data to be printed
|
35
|
+
# <tt>amount</tt> The length of the table (defaults to 20)
|
36
|
+
def print_table(summarizer, field, amount = 20)
|
37
|
+
summarizer.sort_actions_by(field).reverse[0, amount.to_i].each do |a|
|
38
|
+
# As we show count by default, show totaltime if we sort by count
|
39
|
+
field = :total_time if field == :count
|
40
|
+
|
41
|
+
puts "#{a[0].ljust(50)}: %10.03fs [#{green("%d requests")}]" % [a[1][field], a[1][:count]]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Parse the arguments given via commandline
|
46
|
+
begin
|
47
|
+
$arguments = CommandLine::Arguments.parse do |command_line|
|
48
|
+
command_line.switch(:guess_database_time, :g)
|
49
|
+
command_line.switch(:fast, :f)
|
50
|
+
command_line.switch(:colorize, :z)
|
51
|
+
command_line.flag(:output, :alias => :o)
|
52
|
+
command_line.flag(:amount, :alias => :c)
|
53
|
+
command_line.required_files = 1
|
54
|
+
end
|
55
|
+
|
56
|
+
rescue CommandLine::Error => e
|
57
|
+
puts "ARGUMENT ERROR: " + e.message
|
58
|
+
puts
|
59
|
+
load File.dirname(__FILE__) + "/../output/usage.rb"
|
60
|
+
exit(0)
|
61
|
+
end
|
62
|
+
|
63
|
+
$summarizer = RailsAnalyzer::Summarizer.new(:calculate_database => $arguments[:guess_database_time])
|
64
|
+
$summarizer.blocker_duration = 1.0
|
65
|
+
|
66
|
+
line_types = $arguments[:fast] ? [:completed] : [:started, :completed, :failed]
|
67
|
+
|
68
|
+
# Walk through al the files given via the arguments.
|
69
|
+
$arguments.files.each do |log_file|
|
70
|
+
puts "Processing #{line_types.join(', ')} log lines from #{log_file}..."
|
71
|
+
parser = RailsAnalyzer::LogParser.new(log_file)
|
72
|
+
|
73
|
+
# add progress bar
|
74
|
+
unless $arguments[:fast]
|
75
|
+
pbar = ProgressBar.new(green(log_file), File.size(log_file))
|
76
|
+
parser.progress { |pos, total| (pos == :finished) ? pbar.finish : pbar.set(pos) }
|
77
|
+
end
|
78
|
+
|
79
|
+
parser.each(*line_types) do |request|
|
80
|
+
$summarizer.group(request) { |r| request_hasher(r) }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Select the reports to output and generate them.
|
85
|
+
output_reports = $arguments[:output].split(',') rescue [:timespan, :most_requested, :total_time, :mean_time, :total_db_time, :mean_db_time, :mean_rendering_time, :blockers, :hourly_spread, :errors]
|
86
|
+
|
87
|
+
output_reports.each do |report|
|
88
|
+
report_location = "#{File.dirname(__FILE__)}/../output/#{report}.rb"
|
89
|
+
|
90
|
+
if File.exist?(report_location)
|
91
|
+
load report_location
|
92
|
+
else
|
93
|
+
puts "\nERROR: Output report #{report} not found!"
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/../lib/command_line/arguments'
|
4
|
+
require File.dirname(__FILE__) + '/../lib/rails_analyzer/log_parser'
|
5
|
+
require File.dirname(__FILE__) + '/../lib/rails_analyzer/record_inserter'
|
6
|
+
require File.dirname(__FILE__) + '/../lib/bashcolorizer'
|
7
|
+
require File.dirname(__FILE__) + '/../lib/ruby-progressbar/progressbar.rb'
|
8
|
+
|
9
|
+
|
10
|
+
puts "Rails log parser, by Willem van Bergen and Bart ten Brinke\n\n"
|
11
|
+
|
12
|
+
begin
|
13
|
+
|
14
|
+
$arguments = CommandLine::Arguments.parse do |command_line|
|
15
|
+
command_line.switch(:guess_database_time, :g)
|
16
|
+
command_line.switch(:reset_database, :r)
|
17
|
+
command_line.flag(:database, :alias => :d, :required => false)
|
18
|
+
command_line.required_files = 1
|
19
|
+
end
|
20
|
+
|
21
|
+
rescue CommandLine::Error => e
|
22
|
+
puts "ARGUMENT ERROR: " + e.message
|
23
|
+
puts
|
24
|
+
puts "Usage: ruby parsetodb.rb [LOGFILES*] <OPTIONS>"
|
25
|
+
puts
|
26
|
+
puts "Options:"
|
27
|
+
puts " --database, -t: The database file to use"
|
28
|
+
puts " --reset-database, -r: Resets the database before inserting new records"
|
29
|
+
puts " --guess-database-time, -g: Guesses the database duration of requests"
|
30
|
+
puts
|
31
|
+
puts "Examples:"
|
32
|
+
puts " ./parsetodb.rb development.log"
|
33
|
+
puts " ./parsetodb.rb mongrel.0.log mongrel.1.log mongrel.2.log -g -d mongrel.db"
|
34
|
+
puts
|
35
|
+
|
36
|
+
exit(0)
|
37
|
+
end
|
38
|
+
|
39
|
+
log_files = $arguments.files
|
40
|
+
db_file = $arguments[:database] || log_files.first + '.db'
|
41
|
+
|
42
|
+
if $arguments[:reset_database] && File.exist?(db_file)
|
43
|
+
File.delete(db_file)
|
44
|
+
puts "Database file cleared."
|
45
|
+
end
|
46
|
+
|
47
|
+
records_inserted = 0
|
48
|
+
inserter = RailsAnalyzer::RecordInserter.insert_batch_into(db_file) do |db|
|
49
|
+
log_files.each do |log_file|
|
50
|
+
|
51
|
+
puts "Processing all log lines from #{log_file}..."
|
52
|
+
parser = RailsAnalyzer::LogParser.new(log_file)
|
53
|
+
|
54
|
+
pbar = ProgressBar.new(green(log_file), File.size(log_file))
|
55
|
+
parser.progress { |pos, total| (pos == :finished) ? pbar.finish : pbar.set(pos) }
|
56
|
+
|
57
|
+
parser.each do |request|
|
58
|
+
db.insert(request)
|
59
|
+
records_inserted += 1
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
if $arguments[:guess_database_time]
|
64
|
+
puts "Calculating database times..."
|
65
|
+
db.calculate_db_durations!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
started = inserter.count(:started)
|
70
|
+
completed = inserter.count(:completed)
|
71
|
+
failed = inserter.count(:failed)
|
72
|
+
|
73
|
+
puts
|
74
|
+
puts "Inserted #{records_inserted} records from #{log_files.length} files."
|
75
|
+
puts "Parse warnings: #{inserter.warning_count}. Check the parse_warnings table in the database for details."
|
76
|
+
puts
|
77
|
+
puts "Requests started: #{started}"
|
78
|
+
puts "Requests completed: #{completed}"
|
79
|
+
puts "Requests failed: #{failed}"
|
@@ -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 ($arguments && $arguments[:colorize])
|
5
|
+
def colorize(text, color_code, color = $arguments && $arguments[:colorize])
|
6
|
+
color ? "#{color_code}#{text}\e[0m" : text
|
7
|
+
end
|
8
|
+
|
9
|
+
# Draw a red line of text
|
10
|
+
def red(text)
|
11
|
+
colorize(text, "\e[31m")
|
12
|
+
end
|
13
|
+
|
14
|
+
# Draw a Green line of text
|
15
|
+
def green(text)
|
16
|
+
colorize(text, "\e[32m")
|
17
|
+
end
|
18
|
+
|
19
|
+
# Draw a Yellow line of text
|
20
|
+
def yellow(text)
|
21
|
+
colorize(text, "\e[33m")
|
22
|
+
end
|
23
|
+
|
24
|
+
# Draw a Yellow line of text
|
25
|
+
def blue(text)
|
26
|
+
colorize(text, "\e[34m")
|
27
|
+
end
|
28
|
+
|
29
|
+
def white(text)
|
30
|
+
colorize(text, "\e[37m")
|
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,129 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/flag"
|
2
|
+
require "#{File.dirname(__FILE__)}/exceptions"
|
3
|
+
|
4
|
+
# Module used to parse commandline arguments
|
5
|
+
module CommandLine
|
6
|
+
|
7
|
+
# Parse argument lists and return an argument object containing all set flags and switches.
|
8
|
+
class Arguments
|
9
|
+
|
10
|
+
FLAG_REGEXP = /^--?[A-z0-9]/
|
11
|
+
|
12
|
+
attr_reader :flag_definitions
|
13
|
+
|
14
|
+
attr_reader :flags
|
15
|
+
attr_reader :files
|
16
|
+
attr_reader :command
|
17
|
+
|
18
|
+
attr_accessor :required_files
|
19
|
+
|
20
|
+
# Initializer.
|
21
|
+
# <tt>arguments</tt> The arguments which are going to be parsed (defaults to $*).
|
22
|
+
def initialize(arguments = $*, &block)
|
23
|
+
@arguments = arguments
|
24
|
+
@flag_definitions = {}
|
25
|
+
@begins_with_command = false
|
26
|
+
end
|
27
|
+
|
28
|
+
# Parse a list of arguments. Intatiates a Argument object with the given arguments and yeilds
|
29
|
+
# it so that flags and switches can be set by the application.
|
30
|
+
# <tt>arguments</tt> The arguments which are going to be parsed (defaults to $*).
|
31
|
+
# Returns the arguments object.parse!
|
32
|
+
def self.parse(arguments = $*, &block)
|
33
|
+
cla = Arguments.new(arguments)
|
34
|
+
yield(cla)
|
35
|
+
return cla.parse!
|
36
|
+
end
|
37
|
+
|
38
|
+
# Handle argument switches for the application
|
39
|
+
# <tt>switch</tt> A switch symbol like :fast
|
40
|
+
# <tt>switch_alias</tt> An short alias for the same switch (:f).
|
41
|
+
def switch(switch, switch_alias = nil)
|
42
|
+
return self.flag(switch, :alias => switch_alias, :expects => nil)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Handle argument flags for the application
|
46
|
+
# <tt>flag</tt> A flag symbol like :fast
|
47
|
+
# Options
|
48
|
+
# * <tt>:expects</tt> Expects a value after the flag
|
49
|
+
def flag(flag, options)
|
50
|
+
options[:expects] = String unless options.has_key?(:expects)
|
51
|
+
argument = Flag.new(flag, options)
|
52
|
+
@flag_definitions[argument.to_argument] = argument
|
53
|
+
@flag_definitions[argument.to_alias] = argument if argument.has_alias?
|
54
|
+
return argument
|
55
|
+
end
|
56
|
+
|
57
|
+
# If called argument list must begin with a command.
|
58
|
+
# <tt>begins_w_command</tt> Defaults to true.
|
59
|
+
def begins_with_command!(begins_w_command=true)
|
60
|
+
@begins_with_command = begins_w_command
|
61
|
+
end
|
62
|
+
|
63
|
+
# Unknown flags will be silently ignored.
|
64
|
+
# <tt>ignore</tt> Defaults to true.
|
65
|
+
def ignore_unknown_flags!(ignore = true)
|
66
|
+
@ignore_unknown_flags = ignore
|
67
|
+
end
|
68
|
+
|
69
|
+
def [](name)
|
70
|
+
return flags[name.to_s.gsub(/_/, '-').to_sym]
|
71
|
+
end
|
72
|
+
|
73
|
+
# Parse the flags and switches set by the application.
|
74
|
+
# Returns an arguments object containing the flags and switches found in the commandline.
|
75
|
+
def parse!
|
76
|
+
@flags = {}
|
77
|
+
@files = []
|
78
|
+
|
79
|
+
i = 0
|
80
|
+
while @arguments.length > i do
|
81
|
+
arg = @arguments[i]
|
82
|
+
if FLAG_REGEXP =~ arg
|
83
|
+
if @flag_definitions.has_key?(arg)
|
84
|
+
flag = @flag_definitions[arg]
|
85
|
+
if flag.expects_argument?
|
86
|
+
|
87
|
+
if @arguments.length > (i + 1) && @arguments[i + 1]
|
88
|
+
@flags[flag.name] = @arguments[i + 1]
|
89
|
+
i += 1
|
90
|
+
else
|
91
|
+
raise CommandLine::FlagExpectsArgument.new(arg)
|
92
|
+
end
|
93
|
+
|
94
|
+
else
|
95
|
+
@flags[flag.name] = true
|
96
|
+
end
|
97
|
+
else
|
98
|
+
raise CommandLine::UnknownFlag.new(arg) unless @ignore_unknown_flags
|
99
|
+
end
|
100
|
+
else
|
101
|
+
if @begins_with_command && @command.nil?
|
102
|
+
@command = arg
|
103
|
+
else
|
104
|
+
@files << arg
|
105
|
+
end
|
106
|
+
end
|
107
|
+
i += 1
|
108
|
+
end
|
109
|
+
|
110
|
+
check_parsed_arguments!
|
111
|
+
|
112
|
+
return self
|
113
|
+
end
|
114
|
+
|
115
|
+
# Check if the parsed arguments meet their requirements.
|
116
|
+
# Raises CommandLineexception on error.
|
117
|
+
def check_parsed_arguments!
|
118
|
+
if @begins_with_command && @command.nil?
|
119
|
+
raise CommandLine::CommandMissing.new
|
120
|
+
end
|
121
|
+
|
122
|
+
if @required_files && @files.length < @required_files
|
123
|
+
raise CommandLine::FileMissing.new("You need at least #{@required_files} files")
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module CommandLine
|
2
|
+
|
3
|
+
# Commandline parsing errors and exceptions
|
4
|
+
class Error < Exception
|
5
|
+
end
|
6
|
+
|
7
|
+
# Missing a required flag
|
8
|
+
class FlagMissing < CommandLine::Error
|
9
|
+
end
|
10
|
+
|
11
|
+
# Missing a required file
|
12
|
+
class FileMissing < CommandLine::Error
|
13
|
+
end
|
14
|
+
|
15
|
+
# Missing a required flag argument
|
16
|
+
class FlagExpectsArgument < CommandLine::Error
|
17
|
+
def initialize(flag)
|
18
|
+
super("#{flag} expects an argument!")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Missing a required command
|
23
|
+
class CommandMissing < CommandLine::Error
|
24
|
+
def initialize(msg = "A command is missing")
|
25
|
+
super(msg)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
# Encountered an unkown flag
|
31
|
+
class UnknownFlag < CommandLine::Error
|
32
|
+
def initialize(flag)
|
33
|
+
super("#{flag} not recognized as a valid flag!")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module CommandLine
|
2
|
+
|
3
|
+
# Argument flag handling.
|
4
|
+
class Flag
|
5
|
+
|
6
|
+
attr_reader :name
|
7
|
+
attr_reader :alias
|
8
|
+
attr_reader :argument
|
9
|
+
|
10
|
+
# Initialize new Flag
|
11
|
+
# <tt>name</tt> The name of the flag
|
12
|
+
# <tt>definition</tt> The definition of the flag.
|
13
|
+
def initialize(name, definition)
|
14
|
+
@name = name.to_s.gsub(/_/, '-').to_sym
|
15
|
+
@alias = definition[:alias].to_sym if definition[:alias]
|
16
|
+
@required = definition.has_key?(:required) && definition[:required] == true
|
17
|
+
@argument = definition[:expects] if definition[:expects]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Argument representation of the flag (--fast)
|
21
|
+
def to_argument
|
22
|
+
"--#{@name}"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Argument alias representation of the flag (-f)
|
26
|
+
def to_alias
|
27
|
+
"-#{@alias}"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Check if flag has an alias
|
31
|
+
def has_alias?
|
32
|
+
!@alias.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Check if flag is optional
|
36
|
+
def optional?
|
37
|
+
!@required
|
38
|
+
end
|
39
|
+
|
40
|
+
# Check if flag is required
|
41
|
+
def required?
|
42
|
+
@required
|
43
|
+
end
|
44
|
+
|
45
|
+
# Check if flag expects an argument (Are you talking to me?)
|
46
|
+
def expects_argument?
|
47
|
+
!@argument.nil?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|