Initial Revision
[ohcount] / bin / ohcount
1 #!/usr/bin/env ruby
2 begin
3         # First assume we are installed as a gem
4         require 'rubygems'
5         require 'ohcount'
6 rescue LoadError
7         # Failing that, try to load our files from the source tree
8         $: << "#{File.dirname(__FILE__)}/../lib"
9         require 'ohcount'
10 end
11
12 class OhcountCommandLine
13         attr_accessor :paths
14
15         def initialize(args=[])
16                 args = args.clone # Because shift is destructive
17                 set_option(args.shift) while args.first =~ /^-/
18                 self.paths = args
19         end
20
21         def paths
22                 @paths || []
23         end
24
25         def paths=(p)
26                 @paths=p
27                 @files = nil # If we change the search paths, clear the cache filenames
28         end
29
30         def files
31                 return @files if @files
32
33                 @files = `find #{(self.paths.any? ? self.paths : ['.']).join(' ')} \! -path "*/.*" -type f -print`.split
34                 exit 1 unless @files.any?
35                 @files
36         end
37
38         def annotate
39                 files.each do |file|
40                         sfc = Ohcount::SimpleFileContext.new(file, files)
41                         polyglot = Ohcount::Detector.detect(sfc)
42                         if polyglot
43                                 Ohcount::parse(sfc.contents, polyglot) do |language, semantic, line|
44                                         puts "#{language}\t#{semantic}\t#{line}"
45                                 end
46                         end
47                 end
48         end
49
50         # Find all source code files
51         def detect
52                 files.each do |file|
53                         sfc = Ohcount::SimpleFileContext.new(file, files)
54                         polyglot = Ohcount::Detector.detect(sfc)
55                         puts "#{polyglot}\t#{file}" if polyglot
56                 end
57         end
58
59         def help
60                 puts <<HELP
61 Usage: ohcount [option] [paths...]
62
63 Ohloh source code line counter command line tool.
64    http://www.ohloh.net/
65
66 [option] can be one of the following:
67    -a, --annotate
68    -d, --detect
69    -h, --help
70    -s, --summary
71
72 -a, --annotate                  Show annotated source code
73
74    The contents of all source code files found within the given
75    paths will be emitted to stdout. Each line will be prefixed with
76    a tab-delimited language name and semantic categorization (code,
77    comment, or blank).
78
79 -d, --detect                    Find source code files
80
81    Recursively find all source code files within the given paths.
82    For each source code file found, the file name will be emitted to
83    stdout prefixed with a tab-delimited language name.
84
85 -h, --help                      Display this message
86
87 -s, --summary                   Count lines of code (default)
88
89    Count lines in all source code files within the given paths, and
90    emit a report of the total number of lines of code, comments,
91    and blanks in each language. This is the default action.
92
93 [paths] can refer to any number of individual files or directories.
94    Directories will be probed recursively. If no path is given,
95    the current directory will be used.
96
97 HELP
98         end
99
100         def summary
101                 STDOUT.write "Examining #{files.size} file(s)"
102
103                 counts = {}
104                 any_language_file_count = 0
105                 progress = 0
106
107                 files.each do |file|
108                         languages_found = []
109                         sfc = Ohcount::SimpleFileContext.new(file, files)
110                         polyglot = Ohcount::Detector.detect(sfc)
111                         if polyglot
112                                 Ohcount::parse(sfc.contents, polyglot) do |language_name, semantic, line|
113                                         counts[language_name] ||= {:code => 0, :comment => 0, :blank => 0}
114                                         counts[language_name][semantic] += 1
115                                         languages_found << language_name unless languages_found.include?(language_name)
116                                 end
117                         end
118
119                         # Keep a running total of the number of files that include a specific language
120                         languages_found.each { |l| counts[l][:files] = (counts[l][:files] || 0) + 1 }
121
122                         # Keep a running total of the number of files that include any language
123                         any_language_file_count += 1 if languages_found.any?
124
125                         progress += 1
126                         if (progress % 100 == 0)
127                                 STDOUT.write('.')
128                                 STDOUT.flush
129                         end
130                 end
131
132                 puts
133                 puts "Ohloh Line Count Summary".center(76)
134                 puts
135
136                 puts "Language        Files       Code    Comment  Comment %      Blank      Total"
137                 puts "--------------  -----  ---------  ---------  ---------  ---------  ---------"
138
139                 counts.keys.sort{ |a,b| counts[b][:code] <=> counts[a][:code] }.each do |key|
140                         write_summary_row(key, counts[key][:files], counts[key][:code], counts[key][:comment], counts[key][:blank])
141                 end
142
143                 puts "--------------  -----  ---------  ---------  ---------  ---------  ---------"
144                 write_summary_row('Total', any_language_file_count,
145                                                                                         counts.values.inject(0) { |sum, v| sum + v[:code] },
146                                                                                         counts.values.inject(0) { |sum, v| sum + v[:comment] },
147                                                                                         counts.values.inject(0) { |sum, v| sum + v[:blank] })
148         end
149
150         def write_summary_row(name, file_count, code, comment, blank)
151                 printf("%-14s", name)
152                 printf(" %6d", file_count)
153                 printf(" %10d", code)
154                 printf(" %10d", comment)
155                 if comment+code > 0
156                         printf(" %9.1f%%", comment.to_f / (comment+code).to_f * 100.0)
157                 end
158                 printf(" %10d", blank)
159                 printf(" %10d\n", code+comment+blank)
160         end
161
162         def subcommand=(s)
163                 if @subcommand
164                         STDERR.puts "Error: Multiple commands specified."
165                         exit 1
166                 else
167                         @subcommand=s
168                 end
169         end
170
171         def subcommand
172                 @subcommand
173         end
174
175         def set_option(option)
176                 case option
177                 when '-s', '--summary'
178                         self.subcommand = :summary
179                 when '-d', '--detect'
180                         self.subcommand = :detect
181                 when '-a', '--annotate'
182                         self.subcommand = :annotate
183                 when '-?', '-h', '--help'
184                         self.subcommand = :help
185                 else
186                         STDERR.puts "Type 'ohcount -?' for usage."
187                         exit 1
188                 end
189         end
190
191         def run!
192                 self.subcommand ||= :summary
193                 if self.respond_to?(self.subcommand)
194                         self.send(self.subcommand)
195                 else
196                         STDERR.puts "Type 'ohcount -?' for usage."
197                         exit 1
198                 end
199         end
200 end
201
202 OhcountCommandLine.new(ARGV).run!