Line Counter

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Line Counter
~~~~~~~~~~~~

Count the lines in given files.

Multiple patterns with shell-style wildcards (``*`` for
everything, ``?`` for any single character) are accepted.

Example usage and output::

    $> python linecounter.py /some/path *.php *.html *.css

    *.css:      982 lines
    *.py:     4.739 lines
    *.xhtml:  2.218 lines
    ---------------------
    total:    7.939 lines

It returns the total for each pattern and an overall total.

Be aware that files will be included multiple times if you
specify overlapping patterns and so the result might not be
what you expected.

Python 2.5 is required.

:Copyright: 2005-2007 Jochen Kupperschmidt
:Date: 12-Jul-2007
:License: MIT
"""

from __future__ import with_statement
from glob import iglob
import locale
locale.setlocale(locale.LC_ALL, '')
from optparse import OptionParser
import os


def count_lines(filename):
    """Count lines in file."""
    with open(filename, 'rb') as f:
        return sum(1 for line in f)

def walk(top):
    """Walk file system tree and return directory names."""
    yield top
    for name in os.listdir(top):
        name = os.path.join(top, name)
        if os.path.isdir(name) and not os.path.islink(name):
            for dir in walk(name):
                yield dir

def match_filenames(path, patterns, callback):
    """Find files matching the pattern and count their lines."""
    for dir in walk(path):
        for pattern in patterns:
            for filename in iglob(os.path.join(dir, pattern)):
                line_count = count_lines(filename)
                callback(filename, line_count)
                yield pattern, line_count

def process_files(path, patterns, callback):
    """Collect line count statistics."""
    stats = dict.fromkeys(patterns, 0)
    for pattern, line_count in match_filenames(
            path, patterns, callback):
        stats[pattern] += line_count
    return stats

def format_thousands(number):
    """Format number with thousands separated."""
    return locale.format('%d', number, True)

def display_results(stats):
    total = format_thousands(sum(stats.values()))
    key_width = max(len(k) for k in stats.keys() + ['total'])
    value_width = len(total)
    format = '%%-%ds  %%%ds lines' % (key_width + 1, value_width)
    print
    for key in sorted(stats.iterkeys()):
        print format % (key + ':', format_thousands(stats[key]))
    total_line = format % ('total:', total)
    print '-' * len(total_line)
    print total_line

if __name__ == '__main__':
    parser = OptionParser(
        usage='%prog [options] <path> [patterns]')
    parser.add_option('-a', '--absolute', dest='absolute',
        action='store_true',
        help='show absolute paths in details (overrides `-r`)')
    parser.add_option('-d', '--details', dest='details',
        action='store_true',
        help='show details for each file')
    parser.add_option('-r', '--relative', dest='relative',
        action='store_true',
        help='show relative paths in details')

    opts, args = parser.parse_args()
    if not args:
        parser.print_help()
        parser.exit()
    path, patterns = args[0], args[1:]

    def callback(filename, line_count):
        if opts.details:
            path_len = len(path)
            if opts.absolute:
                filename = os.path.abspath(filename)
            elif opts.relative:
                filename = '.' + filename[path_len:]
            print '%5d %s' % (line_count, filename)

    stats = process_files(path, patterns, callback)
    display_results(stats)