wvanbergen-request-log-analyzer 0.1.0
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/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
|