Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
21 changes: 21 additions & 0 deletions submissions/cloc/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
101 changes: 101 additions & 0 deletions submissions/cloc/README.md
Original file line number Diff line number Diff line change
@@ -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 <PATH_TO_FILE_OR_PROJECT>`
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 <YOUR_CLOCIGNORE_FILE>` 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.
Binary file added submissions/cloc/bin/cloc
Binary file not shown.
Empty file.
167 changes: 167 additions & 0 deletions submissions/cloc/cloc/__main__.py
Original file line number Diff line number Diff line change
@@ -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()
Loading