#!/usr/bin/env python3 """ 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()