142 lines
No EOL
4.9 KiB
Python
142 lines
No EOL
4.9 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""
|
|
Script to count code lines in a folder and its subfolders.
|
|
Reports lines per file for Python, JavaScript, and CSS files,
|
|
and calculates the total lines of code.
|
|
"""
|
|
|
|
import os
|
|
import argparse
|
|
from pathlib import Path
|
|
from typing import Dict, List, Tuple
|
|
|
|
# File extensions to count
|
|
FILE_TYPES = {
|
|
'python': ['.py'],
|
|
'javascript': ['.js', '.jsx', '.ts', '.tsx'],
|
|
'css': ['.css', '.scss', '.sass', '.less'],
|
|
'html': ['.htm','.html']
|
|
}
|
|
|
|
def count_lines(file_path: str) -> int:
|
|
"""Count the number of non-empty lines in a file."""
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as file:
|
|
# Count non-empty lines (strip whitespace)
|
|
return sum(1 for line in file if line.strip())
|
|
except UnicodeDecodeError:
|
|
# Skip files that can't be decoded as UTF-8
|
|
print(f"Warning: Could not read {file_path} as text. Skipping.")
|
|
return 0
|
|
except Exception as e:
|
|
print(f"Error reading {file_path}: {e}")
|
|
return 0
|
|
|
|
def get_file_type(file_path: str) -> str:
|
|
"""Determine the file type based on extension."""
|
|
extension = os.path.splitext(file_path)[1].lower()
|
|
|
|
for file_type, extensions in FILE_TYPES.items():
|
|
if extension in extensions:
|
|
return file_type
|
|
|
|
return "other"
|
|
|
|
def scan_directory(directory: str, exclude_dirs: List[str] = None) -> Tuple[Dict[str, List[Tuple[str, int]]], Dict[str, int]]:
|
|
"""
|
|
Scan a directory and its subdirectories for code files and count lines.
|
|
|
|
Args:
|
|
directory: The directory to scan
|
|
exclude_dirs: List of directory names to exclude (e.g., ['node_modules', '.git'])
|
|
|
|
Returns:
|
|
Tuple containing:
|
|
- Dictionary mapping file types to lists of (file_path, line_count) tuples
|
|
- Dictionary mapping file types to total line counts
|
|
"""
|
|
if exclude_dirs is None:
|
|
exclude_dirs = ['node_modules', '.git', 'venv', 'env', '__pycache__', '.vscode', '.idea']
|
|
|
|
# Initialize results
|
|
files_by_type = {file_type: [] for file_type in FILE_TYPES.keys()}
|
|
files_by_type["other"] = []
|
|
|
|
totals_by_type = {file_type: 0 for file_type in FILE_TYPES.keys()}
|
|
totals_by_type["other"] = 0
|
|
|
|
# Walk through directory
|
|
for root, dirs, files in os.walk(directory):
|
|
# Skip excluded directories
|
|
dirs[:] = [d for d in dirs if d not in exclude_dirs]
|
|
|
|
for file in files:
|
|
file_path = os.path.join(root, file)
|
|
file_type = get_file_type(file_path)
|
|
|
|
# Count lines if it's a type we're interested in
|
|
if file_type in files_by_type:
|
|
line_count = count_lines(file_path)
|
|
|
|
# Add to files list
|
|
files_by_type[file_type].append((file_path, line_count))
|
|
|
|
# Add to total
|
|
totals_by_type[file_type] += line_count
|
|
|
|
return files_by_type, totals_by_type
|
|
|
|
def main():
|
|
"""Main function."""
|
|
parser = argparse.ArgumentParser(description='Count code lines in a directory.')
|
|
parser.add_argument('directory', type=str, nargs='?', default='.',
|
|
help='Directory to scan (default: current directory)')
|
|
parser.add_argument('--exclude', type=str, nargs='+',
|
|
default=['node_modules', '.git', 'venv', 'env', '__pycache__', '.vscode', '.idea'],
|
|
help='Directories to exclude')
|
|
args = parser.parse_args()
|
|
|
|
directory = args.directory
|
|
exclude_dirs = args.exclude
|
|
|
|
print(f"Scanning directory: {os.path.abspath(directory)}")
|
|
print(f"Excluding directories: {', '.join(exclude_dirs)}")
|
|
print("\nCounting lines of code...\n")
|
|
|
|
# Scan directory
|
|
files_by_type, totals_by_type = scan_directory(directory, exclude_dirs)
|
|
|
|
# Calculate total lines
|
|
total_lines = sum(totals_by_type.values())
|
|
|
|
# Print results by file type
|
|
for file_type in sorted(files_by_type.keys()):
|
|
if file_type == "other" and not files_by_type["other"]:
|
|
continue
|
|
|
|
files = files_by_type[file_type]
|
|
if not files:
|
|
continue
|
|
|
|
print(f"\n{file_type.upper()} FILES ({totals_by_type[file_type]} total lines):")
|
|
print("-" * 80)
|
|
|
|
# Sort files by line count (descending)
|
|
for file_path, line_count in sorted(files, key=lambda x: x[1], reverse=True):
|
|
relative_path = os.path.relpath(file_path, directory)
|
|
print(f"{relative_path}: {line_count} lines")
|
|
|
|
# Print summary
|
|
print("\nSUMMARY:")
|
|
print("-" * 80)
|
|
for file_type in sorted(totals_by_type.keys()):
|
|
if totals_by_type[file_type] > 0:
|
|
print(f"{file_type.upper()}: {totals_by_type[file_type]} lines")
|
|
|
|
print("-" * 80)
|
|
print(f"TOTAL LINES OF CODE: {total_lines}")
|
|
|
|
if __name__ == '__main__':
|
|
main() |