Skip to content

Commit d87940d

Browse files
Enhance README and CLI: Add support for ignoring folders, output formats, and improve error handling
1 parent c3e04f3 commit d87940d

File tree

5 files changed

+205
-109
lines changed

5 files changed

+205
-109
lines changed

README.md

Lines changed: 86 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,35 @@
1-
## 📦 extliner
1+
# 📦 extliner
22

3-
**extliner** is a lightweight Python package that counts lines in files (with and without empty lines) grouped by file extension perfect for analyzing codebases or text-heavy directories.
3+
**extliner** is a lightweight Python package that recursively counts lines in files — distinguishing between total lines and non-empty linesgrouped by file extension. It's perfect for analyzing codebases, writing statistics, or cleaning up documentation-heavy directories.
44

55
---
66

7-
### 🚀 Features
8-
9-
* 📂 Recursive directory traversal
10-
* 🔍 Counts:
11-
12-
* Total lines **with** whitespace
13-
* Total lines **excluding** empty lines
14-
* 🎯 Extension-based grouping (`.py`, `.txt`, `NO_EXT`, etc.)
15-
* 🚫 Option to **ignore specific file extensions**
16-
* 📊 Beautiful **tabulated output**
17-
* 🧩 Easily extensible class-based design
18-
* 🧪 CLI support
7+
## 🚀 Features
8+
9+
- 📂 Recursive directory scanning
10+
- 🧮 Counts:
11+
- Total lines (with whitespace)
12+
- Non-empty lines (ignores blank lines)
13+
- 🔠 Groups results by file extension (`.py`, `.js`, `NO_EXT`, etc.)
14+
- 🚫 Support for ignoring specific extensions or folders
15+
- 📊 Output formats:
16+
- Pretty CLI table
17+
- JSON / CSV / Markdown exports
18+
- 🧩 Clean, extensible class-based design
19+
- 🧪 Fully tested with `unittest`
20+
- 🔧 CLI and Python API support
1921

2022
---
2123

22-
### 📥 Installation
24+
## 📥 Installation
25+
26+
Install via pip:
2327

2428
```bash
2529
pip install extliner
2630
```
2731

28-
(Or if using locally during development:)
32+
Or install locally for development:
2933

3034
```bash
3135
git clone https://github.com/extliner/extliner.git
@@ -35,62 +39,107 @@ pip install -e .
3539

3640
---
3741

38-
### 🧑‍💻 Usage
42+
## ⚙️ CLI Usage
3943

40-
#### CLI
44+
### Basic
4145

4246
```bash
43-
extliner -d <directory_path> --ignore .log .json
47+
extliner -d <directory_path>
4448
```
4549

46-
#### Example
50+
### 🔍 Ignoring Extensions
4751

4852
```bash
4953
extliner -d ./myproject --ignore .md .log
5054
```
5155

52-
#### Output
56+
### Ignoring Folders
57+
58+
```bash
59+
extliner -d ./myproject --folders .venv __pycache__
60+
```
61+
62+
### 🧾 Output Example
5363

5464
```
55-
+------------+---------------+-------------------+--------------+
56-
| Extension | With Spaces | Without Spaces | % of Total |
57-
+------------+---------------+-------------------+--------------+
58-
| .py | 320 | 280 | 65.31% |
59-
| .txt | 170 | 150 | 34.69% |
60-
+------------+---------------+-------------------+--------------+
65+
+-------------+---------------+------------------+---------+--------------+
66+
| Extension | With Spaces | Without Spaces | Files | % of Total |
67+
+=============+===============+==================+=========+==============+
68+
| .py | 443 | 362 | 7 | 32.15% |
69+
+-------------+---------------+------------------+---------+--------------+
70+
| no_ext | 361 | 287 | 8 | 26.20% |
71+
+-------------+---------------+------------------+---------+--------------+
72+
| .pyc | 151 | 125 | 3 | 10.96% |
73+
+-------------+---------------+------------------+---------+--------------+
6174
```
6275

6376
---
77+
## 🧱 Python API
6478

65-
### 🧱 Python API
79+
### ✅ Count Lines Programmatically
6680

6781
```python
68-
from linecountx.main import LineCounter
82+
from extliner.main import LineCounter
6983
from pathlib import Path
7084

7185
counter = LineCounter(ignore_extensions=[".log", ".json"])
7286
result = counter.count_lines(Path("./your_directory"))
7387

88+
# Output as JSON
7489
print(counter.to_json(result))
90+
91+
# Output as Markdown
92+
print(counter.to_markdown(result))
93+
94+
# Output as CSV
95+
print(counter.to_csv(result))
7596
```
7697

7798
---
7899

79-
### ⚙️ Options
100+
## 🛠️ Configuration Options
101+
102+
| Flag | Description | Example | Optioal/Required |
103+
| ----------- | --------------------------------- | ----------------------------- | ---------------- |
104+
| `-d` | Directory to scan | `-d ./src` | Required |
105+
| `--ignore` | File extensions to ignore | `--ignore .log .md .json` | Optional |
106+
| `--folders` | Folder names to ignore | `--folders .venv __pycache__` | Optional |
107+
108+
---
109+
110+
## 📂 Supported Formats
80111

81-
| Flag | Description | Example |
82-
| ---------- | ---------------------------- | ------------------------- |
83-
| `-d` | Directory to scan (required) | `-d ./src` |
84-
| `--ignore` | File extensions to ignore | `--ignore .log .md .json` |
112+
| Output Method | Description |
113+
| ------------------- | ---------------------- |
114+
| `to_json(data)` | Returns JSON string |
115+
| `to_csv(data)` | Returns CSV string |
116+
| `to_markdown(data)` | Returns Markdown table |
85117

118+
---
119+
120+
## ✅ Testing
121+
122+
To run tests:
123+
124+
```bash
125+
python -m unittest discover tests
126+
```
127+
128+
Or using `pytest` (if installed):
129+
130+
```bash
131+
pytest
132+
```
133+
134+
---
86135

87-
### 📄 License
136+
## 📄 License
88137

89-
MIT License
138+
This project is licensed under the MIT License.
90139

91140
---
92141

93-
### 👨‍💻 Author
142+
## 👨‍💻 Author
94143

95144
Made with ❤️ by [Deepak Raj](https://github.com/extliner)
96145

extliner/cli.py

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,38 @@
44

55
from extliner.main import LineCounter
66

7+
78
def main():
8-
parser = argparse.ArgumentParser(description="Count lines in files by extension.")
9+
parser = argparse.ArgumentParser(
10+
description="📦 Count lines in files grouped by extension (with/without empty lines)."
11+
)
912
parser.add_argument(
1013
"-d", "--directory", type=Path, required=True,
11-
help="Directory to count lines in."
14+
help="Input directory to count lines in (default: current directory)"
15+
)
16+
parser.add_argument(
17+
"-i", "--ignore", nargs="*", default=[],
18+
help="File extensions to ignore (e.g., .log .json)"
19+
)
20+
parser.add_argument(
21+
"-f", "--folders", nargs="*", default=[],
22+
help="Folder names to ignore (e.g., .venv __pycache__)"
1223
)
1324
parser.add_argument(
14-
"-i",
15-
"--ignore", nargs="*", default=[],
16-
help="List of file extensions to ignore (e.g., .log .json)"
25+
"--format", choices=["table", "json", "csv", "md"], default="table",
26+
help="Output format: table (default), json, csv, or md (markdown)"
1727
)
1828

1929
args = parser.parse_args()
2030

2131
if not args.directory.is_dir():
22-
print(f"Error: {args.directory} is not a valid directory")
32+
print(f"Error: {args.directory} is not a valid directory.")
2333
return
2434

25-
counter = LineCounter(ignore_extensions=args.ignore)
35+
counter = LineCounter(
36+
ignore_extensions=args.ignore,
37+
ignore_folder=args.folders,
38+
)
2639
result = counter.count_lines(args.directory)
2740

2841
# Remove extensions with 0 lines
@@ -31,26 +44,31 @@ def main():
3144
if counts["with_spaces"] > 0 or counts["without_spaces"] > 0
3245
}
3346

34-
# Sort result by extension
35-
result = dict(sorted(result.items()))
47+
# Sort by line count (with spaces) in descending order
48+
result = dict(sorted(result.items(), key=lambda item: item[1]["with_spaces"], reverse=True))
3649

3750
total_with_spaces = sum(counts["with_spaces"] for counts in result.values())
38-
39-
# sort the result by the number of lines with spaces
40-
result = dict(sorted(result.items(), key=lambda item: item[1]["with_spaces"], reverse=True))
4151

42-
table = []
43-
for ext, counts in result.items():
44-
with_spaces = counts["with_spaces"]
45-
without_spaces = counts["without_spaces"]
46-
percent = (with_spaces / total_with_spaces * 100) if total_with_spaces else 0
47-
table.append([ext, with_spaces, without_spaces, f"{percent:.2f}%"])
48-
49-
print(tabulate(
50-
table,
51-
headers=["Extension", "With Spaces", "Without Spaces", "% of Total"],
52-
tablefmt="grid"
53-
))
52+
if args.format == "json":
53+
print(counter.to_json(result))
54+
elif args.format == "csv":
55+
print(counter.to_csv(result))
56+
elif args.format == "md":
57+
print(counter.to_markdown(result))
58+
else:
59+
# Default: tabular CLI output
60+
table = []
61+
for ext, counts in result.items():
62+
with_spaces = counts["with_spaces"]
63+
without_spaces = counts["without_spaces"]
64+
percent = (with_spaces / total_with_spaces * 100) if total_with_spaces else 0
65+
table.append([ext, with_spaces, without_spaces, counts["file_count"], f"{percent:.2f}%"])
66+
67+
print(tabulate(
68+
table,
69+
headers=["Extension", "With Spaces", "Without Spaces", "Files", "% of Total"],
70+
tablefmt="grid"
71+
))
5472

5573

5674
if __name__ == "__main__":

extliner/main.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,33 @@
66

77

88
class LineCounter:
9-
def __init__(self, ignore_extensions: Optional[List[str]] = None):
9+
def __init__(self, ignore_extensions: Optional[List[str]] = None, ignore_folder: Optional[List[str]] = None, encoding: str = "utf-8"):
10+
self.encoding = encoding
11+
self.ignore_folder = set(ignore_folder or [])
1012
self.ignore_extensions = set(ignore_extensions or [])
1113
self.with_spaces: Dict[str, int] = defaultdict(int)
1214
self.without_spaces: Dict[str, int] = defaultdict(int)
15+
self.file_count: Dict[str, int] = defaultdict(int)
1316

1417
def count_lines(self, directory: Path) -> Dict[str, Dict[str, int]]:
1518
directory = Path(directory)
1619
if not directory.is_dir():
1720
raise ValueError(f"{directory} is not a valid directory")
18-
19-
for root, _, files in os.walk(directory):
21+
22+
for root, dirs, files in os.walk(directory):
23+
# Remove ignored folders from traversal
24+
dirs[:] = [d for d in dirs if d not in self.ignore_folder]
2025
for file in files:
2126
filepath = Path(root) / file
22-
ext = filepath.suffix or "NO_EXT"
27+
ext = (filepath.suffix or "NO_EXT").lower()
2328

2429
if ext in self.ignore_extensions:
2530
continue
2631

2732
try:
28-
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
33+
with open(filepath, "r", encoding=self.encoding, errors="ignore") as f:
2934
lines = f.readlines()
35+
self.file_count[ext] += 1
3036
self.with_spaces[ext] += len(lines)
3137
self.without_spaces[ext] += sum(1 for line in lines if line.strip())
3238
except Exception as e:
@@ -39,36 +45,34 @@ def _build_result(self) -> Dict[str, Dict[str, int]]:
3945
ext: {
4046
"with_spaces": self.with_spaces[ext],
4147
"without_spaces": self.without_spaces[ext],
48+
"file_count": self.file_count[ext],
4249
}
4350
for ext in sorted(set(self.with_spaces) | set(self.without_spaces))
4451
}
4552

46-
def to_json(self, data: Dict) -> str:
53+
@staticmethod
54+
def to_json(data: Dict) -> str:
4755
return json.dumps(data, indent=2)
48-
49-
def to_dict(self, data: Dict) -> Dict:
50-
return data
5156

52-
def to_csv(self, data: Dict) -> str:
57+
@staticmethod
58+
def to_csv(data: Dict) -> str:
5359
import csv
5460
from io import StringIO
5561

5662
output = StringIO()
5763
writer = csv.writer(output)
58-
writer.writerow(["Extension", "With Spaces", "Without Spaces"])
64+
writer.writerow(["Extension", "With Spaces", "Without Spaces", "File Count"])
5965

6066
for ext, counts in data.items():
61-
writer.writerow([ext, counts["with_spaces"], counts["without_spaces"]])
67+
writer.writerow([ext, counts["with_spaces"], counts["without_spaces"], counts["file_count"]])
6268

6369
return output.getvalue()
6470

65-
66-
def to_markdown(self, data: Dict) -> str:
67-
output = "| Extension | With Spaces | Without Spaces |\n"
68-
output += "|-----------|-------------|----------------|\n"
69-
71+
@staticmethod
72+
def to_markdown(data: Dict) -> str:
73+
output = "| Extension | With Spaces | Without Spaces | File Count |\n"
74+
output += "|-----------|-------------|----------------|------------|\n"
7075
for ext, counts in data.items():
71-
output += f"| {ext} | {counts['with_spaces']} | {counts['without_spaces']} |\n"
76+
output += f"| {ext} | {counts['with_spaces']} | {counts['without_spaces']} | {counts['file_count']} |\n"
7277

7378
return output
74-

test.py

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)