Welcome, guest | Sign In | My Account | Store | Cart
#!/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

History