#!/usr/bin/python
# -*- mode: python; coding: utf-8 -*-
#
# Copyright 2011 (C) by RĂ©mi Thebault <remi.thebault - at - gmail - dot - com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import sys
import os
from optparse import OptionParser
from fnmatch import fnmatch
usage = '''
linecount.py [Options] Targets
Targets must be list of valid files or directories
Ex for a C++ project :
linecount.py --exts="h cc" --excludes="*build*" myprojectroot'''
sh_exts = 'sh py pl rb'.split()
c_exts = 'h hpp c cc cpp cxx java cs'.split()
m_exts = 'm'.split()
# the few following lines will try to fetch terminal width for better output
# 1st try for POSIX systems (Linux, MacOSX)
# 2nd try for MS systems
# if failure, defaults to 80
termwidth = 80
try:
import fcntl, termios, struct
cr = struct.unpack('hh', fcntl.ioctl(sys.stdout.fileno(),
termios.TIOCGWINSZ, '1234'))
(h, termwidth) = cr
except:
try:
from ctypes import windll, create_string_buffer
# stdin handle is -10
# stdout handle is -11
# stderr handle is -12
h = windll.kernel32.GetStdHandle(-11)
csb = create_string_buffer(22)
res = windll.kernel32.GetConsoleScreenBufferInfo(h, csb)
if res:
import struct
(bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx,
maxy) = struct.unpack("hhhhHhhhhhh", csb.raw)
termwidth = right - left + 1
except:
pass
# some output formatting utility
defaultFmtTag = '\033[0m'
boldFmtTag = '\033[1;1m'
redFmtTag = '\033[91m'
greenFmtTag = '\033[92m'
def boldFmt(str, closeTag=defaultFmtTag):
return boldFmtTag + str + closeTag
def greenFmt(str, closeTag=defaultFmtTag):
return greenFmtTag + str + closeTag
def redFmt(str, closeTag=defaultFmtTag):
return redFmtTag + str + closeTag
def errorMsg(mes):
sys.stderr.write(boldFmt(redFmt('Error: ' + mes)) + '\n')
def getExt(file):
(root, ext) = os.path.splitext(file)
if len(ext) > 0:
return ext[1:]
return ''
def fileMatches(file, options):
if options.excludes:
for exc in options.excludes:
if fnmatch(file, exc):
return False
if options.syntax:
return True
ext = getExt(file)
if options.exts:
if ext in options.exts:
if ext in sh_exts or ext in c_exts or ext in m_exts:
return True
else:
return True
return False
def doCFile(file):
count = 0
f = open(file, 'r')
incomment = False
for line in f:
line = line.strip()
if incomment:
end = line.find('*/')
if end < 0:
continue
else:
incomment = False
line = line[end+2:]
if len(line) == 0:
continue
if line.startswith('//'):
continue
ind = line.find('/*')
if ind >= 0:
incomment = True
ind2 = line[ind+2:].find('*/') >= 0
if ind2 >= 0:
incomment = False
if ind > 0 or ind2 < len(line)-2:
count += 1
continue
count += 1
return count
def doRegularFile(file, cmtStr):
count = 0
f = open(file, 'r')
for line in f:
line = line.strip()
if len(line) > 0 and not line.startswith(cmtStr):
count += 1
return count
def doShFile(file):
return doRegularFile(file, '#')
def doMFile(file):
return doRegularFile(file, '%')
formatstr = '{0:.<' + str(termwidth-11) + '}' + boldFmt(greenFmt('{1:>5d} lines'))
filecount = 0
def doFile(file, options):
global formatstr
global filecount
if options.syntax:
if options.syntax == 'S':
count = doShFile(file)
elif options.syntax == 'C':
count = doCFile(file)
elif options.syntax == 'M':
count = doMFile(file)
else:
ext = getExt(file)
count = 0
if ext in sh_exts:
count = doShFile(file)
elif ext in c_exts:
count = doCFile(file)
elif ext in m_exts:
count = doMFile(file)
print formatstr.format(file, count)
filecount += 1
return count
def doDir(dir, options):
files = sorted(os.listdir(dir))
count = 0
for file in files:
fname = os.path.join(dir, file)
if os.path.islink(fname):
continue
if os.path.isdir(fname) and options.recurs:
count += doDir(fname, options)
elif fileMatches(fname, options):
count += doFile(fname, options)
return count
if __name__ == '__main__':
parser = OptionParser(usage)
parser.add_option('-e', '--exts', dest='exts', action='store',
help='list of extensions of files to be parsed (mandatory if a dir '
+ ' is in targets')
parser.add_option('-x', '--excludes', dest='excludes', action='store',
help='Blob syntax list of files to be excluded from count '
'(only useful when parsing dirs)')
parser.add_option('-s', '--syntax', dest='syntax', action='store',
help='Force parsing mode to the given syntax ' +
'(S: Shell-style, C: C-style, M: Matlab-style). If not specified, '
'syntax is based on file extension')
parser.add_option('-r', '--non-recursive', dest='recurs',
action='store_false', default=True,
help='Do not enter subdirectories recursively')
(options, args) = parser.parse_args()
if len(args) == 0:
parser.print_help()
errorMsg('you must specify a destination')
sys.exit(1)
args = sorted(args)
for dest in args:
if os.path.exists(dest) and os.path.isdir(dest):
if not options.exts:
parser.print_help()
errorMsg('option ' + greenFmt('-e', redFmtTag) + ' or ' +
greenFmt('--exts', redFmtTag) + ' is needed')
sys.exit(1)
break
if options.exts:
options.exts = options.exts.split()
if options.excludes:
options.excludes = options.excludes.split()
if options.syntax:
if not options.syntax in 'S C M'.split():
parser.print_help()
errorMsg('accepted values for ' + greenFmt('--syntax', redFmtTag) + ' are:\n' +
' S for shell-style\n' +
' C for C-style\n' +
' M for matlab-style')
sys.exit(1)
count = 0
err = 0
printresume = len(args) > 1
for dest in args:
if os.path.exists(dest):
if os.path.isdir(dest):
c = doDir(dest, options)
if len(args)>1:
print repr(c) + ' lines of code in ' + dest
count += c
printresume = True
elif os.path.isfile(dest):
if fileMatches(dest, options):
count += doFile(dest, options)
else:
errorMsg('file ' + dest + ' doesn\'t match your options')
err += 1
else:
errorMsg('target ' + dest + ' is not valid')
err += 1
if err == 0 or count > 0:
if printresume:
resume = 'total count : ' + repr(count) + ' line'
if count > 1:
resume += 's'
resume += ' of code in ' + repr(filecount)+ ' file'
if filecount > 1:
resume += 's'
print boldFmt(greenFmt(resume))
else:
parser.print_help()
errorMsg('Aborting because of errors')
sys.exit(1)
sys.exit(0)
Diff to Previous Revision
--- revision 1 2011-01-17 22:35:02
+++ revision 2 2011-01-17 22:37:32
@@ -42,7 +42,7 @@
c_exts = 'h hpp c cc cpp cxx java cs'.split()
m_exts = 'm'.split()
-# the few following lines will try to fetch terminal width for fancier output
+# the few following lines will try to fetch terminal width for better output
# 1st try for POSIX systems (Linux, MacOSX)
# 2nd try for MS systems
# if failure, defaults to 80