diff --git a/submissions/cloc/BINARY_IS_IN_BIN_DIR.txt b/submissions/cloc/BINARY_IS_IN_BIN_DIR.txt new file mode 100644 index 00000000..e69de29b diff --git a/submissions/cloc/LICENSE b/submissions/cloc/LICENSE new file mode 100644 index 00000000..aad4148e --- /dev/null +++ b/submissions/cloc/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2025] [Joshua Hall] + +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. \ No newline at end of file diff --git a/submissions/cloc/README.md b/submissions/cloc/README.md new file mode 100644 index 00000000..13d447e5 --- /dev/null +++ b/submissions/cloc/README.md @@ -0,0 +1,101 @@ +# cloc + +## Description +cloc (Count LOC) is a simple terminal utility to easily count lines of code in a file / project. + +## Why I Made This? +I created cloc to provide a lightweight and easy to use terminal-based LOC counter with flexible ignore patterns. +Unlike existing tools, it supports recursive ignore patterns and simple wildcard filtering to streamline codebase analysis without making it too complex. + +## Features + - Counts lines of code in files and directories recursively + - Allows easy exclusion of files, directories, and patterns with wildcards + - Simple help menu and thorough documentation + - .clocignore file to handle many ignore patterns in large projects + +## Installation +### Windows, Linux & Mac +#### Python +1. Make sure you have a recent version of Python installed. +2. Run the command `pip install plazma-cloc` to install. +3. Done. + +#### Binary (Usefull on Raspberry Pi) +1. Download the binary from the releases page on GitHub +2. Use `./cloc` to run the binary. +> Note: You may want to add the binary's directory to your system `PATH` so you can run cloc from anywhere without needing the `./`. + +## Running The Binary +You can run the pre-built Linux binary directly from the terminal without installing Python as such: + +```bash +./cloc -h +./cloc /path/to/project --ignore "*.test" +``` + +> Note: See **Usage** below for more information on how to use... + +## Usage +> Note: If using the binary, replace 'cloc' with './cloc'. If using the pip installation, replace 'cloc' with 'python -m cloc'. + +1. Open a terminal and run the command `cloc ` +2. For help run the command `cloc -h` + +To narrow the file types down use the `--ignore` flag. +This flag allows you to ignore certain files, directories and file extensions. + +### Ignoring singular files +To ignore one file use `cloc your_project_folder --ignore your_file.txt` + +You can also ignore multiple files with this same syntax. + +E.G. `cloc your_project_folder --ignore "your_file.txt" "your_other_file.txt"` + +> Note: It is good practice to surround each file / directory / extension in quotation marks to avoid your terminal getting confused + +### Ignoring singular directories +To ignore one directory use `cloc your_project_folder --ignore your_directory/` + +You can also ignore multiple directories with this same syntax. + +E.G. `cloc your_project_folder --ignore "your_directory/" "your_other_directory/"` + +> Note: Ensure to add a '/' character to the end of your directory name so the program knows it is a directory + +### Ignoring files with certain file extensions +To ignore all files with a certain file extension use: `cloc your_project_folder --ignore "*.txt"` + +> Note: The '*' character is a wildcard character and tells the program it means every file with the file extension after the wildcard. + +Then after the wildcard you enter the file extension. E.G. ".txt" or ".exe"... + +You can also ignore multiple file extensions with the same syntax as before. +`cloc your_project_folder --ignore "*.txt" "*.exe" "*.json"` + +### Ignoring all directories with the same name +To ignore all directories with the same name use a similar ignore pattern as before: +`cloc your_project_folder --ignore your_directory/*` + +> Note: You must append `/*` to the end of the directory name so that the program knows it is a recursive directory ignore. + +You can ignore multiple directories using a similar command: +`cloc your_project_folder --ignore "your_directory/*" "your_other_directory/*"` + +### Using .clocignore to ingore many patterns +In large projects (or just for convenience) you may wish to use a .clocignore file to handle many patterns. + +A .clocignore file should look something like this: +```.clocignore +a_file.txt +a_directory/ +*.test +a_repetitive_directory/* +``` + +The .clocignore just contains all of your `--ignore` arguments in a file format. Each ignore pattern is placed on a new line. + +To open a .clocignore simply run cloc with the `--clocignore ` flag. +An example of this is: `cloc your_project_folder --clocignore .clocignore` + +> Note: The `.clocignore` file you provide as an argument should be relative to where you are running the `cloc` executable from (where your terminal is). +> The `--clocignore` flag will override the `--ignore` flag. \ No newline at end of file diff --git a/submissions/cloc/bin/cloc b/submissions/cloc/bin/cloc new file mode 100755 index 00000000..9fbeaf5b Binary files /dev/null and b/submissions/cloc/bin/cloc differ diff --git a/submissions/cloc/cloc/__init__.py b/submissions/cloc/cloc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/submissions/cloc/cloc/__main__.py b/submissions/cloc/cloc/__main__.py new file mode 100644 index 00000000..945114f4 --- /dev/null +++ b/submissions/cloc/cloc/__main__.py @@ -0,0 +1,167 @@ +import argparse +import os +import json +import importlib.resources + +from pathlib import Path +from typing import List, Dict + +class CLOC: + BCOLORS = { + "HEADER": '\033[95m', + "blue": '\033[94m', + "cyan": '\033[96m', + "green": '\033[92m', + "orange": '\033[93m', + "red": '\033[91m', + "ENDC": '\033[0m', + "BOLD": '\033[1m', + "UNDERLINE": '\033[4m' + } + + def __init__(self) -> None: + self.parser = argparse.ArgumentParser(description="cloc (Count LOC) is a terminal utility to easily count lines of code in a file / project.", formatter_class=argparse.RawTextHelpFormatter) + self.parser.add_argument("path_arg", type=str, help="The path to the file / directory cloc should scan.") + self.parser.add_argument( + "--ignore", + type=str, + action="extend", + nargs='+', + help=( + "Ignore files or directories matching the given patterns.\n" + "You can provide multiple patterns at once or repeat the --ignore option.\n" + "When ignoring specific directories their paths should be relative to the path you ran this with.\n" + " i.e. You ran: 'cloc ~/Documents/my_project', then if you wish to ignore a specific directory you should provide it's path relative to the ~/Documents/my_project directory." + "\nExamples:\n" + " --ignore '*.py' 'main.cpp' # Ignore all .py files and main.cpp\n" + " --ignore '*.json' # Ignore all .json files\n" + " --ignore 'my_directory/' # Ignore a specific directory\n" + " --ignore 'my_directory/*' # Ignore all directories with this name\n" + ) + ) + self.parser.add_argument("--clocignore", type=str, help="The path to a '.clocignore' file relative to where this (cloc) is ran from which contains a list of files / dirs / extensions to ignore.\nWill override --ignore flag!") + + self.args = self.parser.parse_args() + + if not os.path.exists(self.args.path_arg): + print(f"Error: Please enter a valid path!\n") + self.parser.print_help() + exit() + + self.path = Path(self.args.path_arg) + + try: + with importlib.resources.open_text("cloc", "languages.json") as f: + self.languages = json.loads(f.read()) + except Exception as e: + print(f"\nError while loading 'languages.json': {e}\n") + self.parser.print_help() + exit() + + self.calculate_ignore_types() + self.print_loc() + + def calculate_ignore_types(self) -> None: + ignore = None + + if self.args.clocignore is not None: + try: + with open(self.args.clocignore, "r") as f: + ignore = f.read().split('\n') + + except Exception as e: + print(f"Error while parsing clocignore: {e}!\n") + self.parser.print_help() + exit() + + self.ignore_exts = [] + self.ignore_dirs = [] + self.ignore_strict_files = [] + self.ignore_strict_dirs = [] + + if self.args.ignore is None and ignore is None: return + + if self.args.ignore is not None and ignore is None: + ignore = self.args.ignore + + for pattern in ignore: + if pattern.strip() == "": continue + + if pattern[:2] == '*.': + self.ignore_exts.append(pattern[1:]) + elif pattern[-2:] == '/*': + self.ignore_dirs.append(pattern[:-2]) + elif pattern[-1] == '/': + self.ignore_strict_dirs.append(self.path.joinpath(pattern[:-1])) + else: + self.ignore_strict_files.append(pattern) + + def get_file_loc(self, file_path: Path) -> int: + path = file_path + + if path.name in self.ignore_strict_files or path.suffix in self.ignore_exts or path.suffix == "": + return -1 + + with open(path, 'rb') as f: + return len(f.readlines()) + + def get_dir_files_loc(self, path: Path) -> Dict[Path, int]: + if path in self.ignore_strict_dirs or path.name in self.ignore_dirs: + return [] + + #print(f"Exploring: {path}...") + + dir_loc = {} + + for new_path in path.iterdir(): + if new_path.is_file(): + file_loc = self.get_file_loc(new_path) + + if file_loc >= 0: + dir_loc[new_path] = file_loc + else: + dir_files_loc = self.get_dir_files_loc(new_path) + + if dir_files_loc is not None: + dir_loc.update(dir_files_loc) + + return dir_loc + + def get_ext_usage(self, files_loc: Dict[Path, int]) -> Dict[str, int]: + ext_usage = {} + + for file, loc in files_loc.items(): + if file.suffix in ext_usage: + ext_usage[file.suffix] += loc + else: + ext_usage[file.suffix] = loc + + return ext_usage + + def fmt_ext_usage(self, ext_loc: Dict[str, int]) -> str: + fmt_text = "" + + for ext, loc in ext_loc.items(): + if ext in self.languages: + start_char = self.BCOLORS.get(self.languages[ext]["color"], "") + end_char = self.BCOLORS["ENDC"] + if start_char == "": end_char = "" + + fmt_text += f"{start_char}{self.languages[ext]["name"]} ({ext}): {loc}{end_char}\n" + else: + fmt_text += f"{ext}: {loc}\n" + + return fmt_text + + def print_loc(self) -> None: + if self.path.is_file(): + print(self.get_file_loc(self.path)) + else: + files_loc = self.get_dir_files_loc(self.path) + + #print(f"File extentions breakdown: {self.get_ext_usage(files_loc)}") + print(self.fmt_ext_usage(self.get_ext_usage(files_loc))) + print(f"Total LOC: {sum(files_loc.values())}") + +if __name__ == "__main__": + cloc = CLOC() \ No newline at end of file diff --git a/submissions/cloc/cloc/languages.json b/submissions/cloc/cloc/languages.json new file mode 100644 index 00000000..133ccaac --- /dev/null +++ b/submissions/cloc/cloc/languages.json @@ -0,0 +1,211 @@ +{ + ".py": { + "name": "Python", + "color": "green", + "comments": "#" + }, + ".cpp": { + "name": "C++", + "color": "blue", + "comments": "//" + }, + ".c++": { + "name": "C++", + "color": "blue", + "comments": "//" + }, + ".cc": { + "name": "C++", + "color": "blue", + "comments": "//" + }, + ".cxx": { + "name": "C++", + "color": "blue", + "comments": "//" + }, + ".c": { + "name": "C", + "color": "blue", + "comments": "//" + }, + ".hpp": { + "name": "C++ Header", + "color": "blue", + "comments": "//" + }, + ".hh": { + "name": "C++ Header", + "color": "blue", + "comments": "//" + }, + ".hxx": { + "name": "C++ Header", + "color": "blue", + "comments": "//" + }, + ".h": { + "name": "Header", + "color": "blue", + "comments": "//" + }, + ".go": { + "name": "Go", + "color": "blue", + "comments": "//" + }, + ".js": { + "name": "Javascript", + "color": "orange", + "comments": "//" + }, + ".json": { + "name": "JSON", + "color": "red", + "comments": null + }, + ".java": { + "name": "Java", + "color": "orange", + "comments": "//" + }, + ".cs": { + "name": "C Sharp", + "color": "blue", + "comments": "//" + }, + ".rs": { + "name": "Rust", + "color": "red", + "comments": "//" + }, + ".toml": { + "name": "TOML", + "color": "purple", + "comments": "#" + }, + ".yaml": { + "name": "YAML", + "color": "yellow", + "comments": "#" + }, + ".yml": { + "name": "YAML", + "color": "yellow", + "comments": "#" + }, + ".md": { + "name": "Markdown", + "color": "green", + "comments": null + }, + ".markdown": { + "name": "Markdown", + "color": "green", + "comments": "" + }, + ".rb": { + "name": "Ruby", + "color": "red", + "comments": "#" + }, + ".php": { + "name": "PHP", + "color": "purple", + "comments": "#", + "extra_comments": "//" + }, + ".swift": { + "name": "Swift", + "color": "orange", + "comments": "//" + }, + ".kt": { + "name": "Kotlin", + "color": "purple", + "comments": "//" + }, + ".kts": { + "name": "Kotlin", + "color": "purple", + "comments": "//" + }, + ".scala": { + "name": "Scala", + "color": "red", + "comments": "//" + }, + ".pl": { + "name": "Perl", + "color": "blue", + "comments": "#" + }, + ".pm": { + "name": "Perl", + "color": "blue", + "comments": "#" + }, + ".sh": { + "name": "Shell", + "color": "blue", + "comments": "#" + }, + ".sql": { + "name": "SQL", + "color": "yellow", + "comments": "--" + }, + ".html": { + "name": "HTML", + "color": "red", + "comments": "