diff-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diff_cli-0.1.0/.gitignore +8 -0
- diff_cli-0.1.0/LICENSE +21 -0
- diff_cli-0.1.0/PKG-INFO +75 -0
- diff_cli-0.1.0/README.md +50 -0
- diff_cli-0.1.0/SKILL.md +262 -0
- diff_cli-0.1.0/diffcli/__init__.py +2 -0
- diff_cli-0.1.0/diffcli/cli.py +452 -0
- diff_cli-0.1.0/pyproject.toml +37 -0
diff_cli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
diff_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diff-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: File and text differ with multiple output formats
|
|
5
|
+
Project-URL: Homepage, https://github.com/marcusbuildsthings-droid/diff-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/marcusbuildsthings-droid/diff-cli
|
|
7
|
+
Author-email: Marcus <marcus.builds.things@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cli,compare,diff,side-by-side,text,unified
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Text Processing :: General
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: click>=8.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# diff-cli
|
|
27
|
+
|
|
28
|
+
File and text differ with multiple output formats.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install diff-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Compare files
|
|
40
|
+
diff-cli files file1.txt file2.txt
|
|
41
|
+
|
|
42
|
+
# Compare text strings
|
|
43
|
+
diff-cli text "hello world" "hello there"
|
|
44
|
+
|
|
45
|
+
# Compare directories
|
|
46
|
+
diff-cli dirs dir1/ dir2/
|
|
47
|
+
|
|
48
|
+
# Output formats
|
|
49
|
+
diff-cli files a.txt b.txt --unified # Default
|
|
50
|
+
diff-cli files a.txt b.txt --side-by-side # Two-column
|
|
51
|
+
diff-cli files a.txt b.txt --context # Context format
|
|
52
|
+
diff-cli files a.txt b.txt --minimal # Changes only
|
|
53
|
+
diff-cli files a.txt b.txt --json # JSON for automation
|
|
54
|
+
|
|
55
|
+
# Options
|
|
56
|
+
diff-cli files a.txt b.txt -n 5 # 5 context lines
|
|
57
|
+
diff-cli files a.txt b.txt --ignore-whitespace
|
|
58
|
+
diff-cli files a.txt b.txt --ignore-case
|
|
59
|
+
diff-cli files a.txt b.txt --ignore-blank-lines
|
|
60
|
+
|
|
61
|
+
# Watch mode
|
|
62
|
+
diff-cli watch file1.txt file2.txt
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Exit Codes
|
|
66
|
+
|
|
67
|
+
| Code | Meaning |
|
|
68
|
+
|------|---------|
|
|
69
|
+
| 0 | Files are identical |
|
|
70
|
+
| 1 | Files differ |
|
|
71
|
+
| 2 | Error (file not found, etc.) |
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
diff_cli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# diff-cli
|
|
2
|
+
|
|
3
|
+
File and text differ with multiple output formats.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install diff-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Compare files
|
|
15
|
+
diff-cli files file1.txt file2.txt
|
|
16
|
+
|
|
17
|
+
# Compare text strings
|
|
18
|
+
diff-cli text "hello world" "hello there"
|
|
19
|
+
|
|
20
|
+
# Compare directories
|
|
21
|
+
diff-cli dirs dir1/ dir2/
|
|
22
|
+
|
|
23
|
+
# Output formats
|
|
24
|
+
diff-cli files a.txt b.txt --unified # Default
|
|
25
|
+
diff-cli files a.txt b.txt --side-by-side # Two-column
|
|
26
|
+
diff-cli files a.txt b.txt --context # Context format
|
|
27
|
+
diff-cli files a.txt b.txt --minimal # Changes only
|
|
28
|
+
diff-cli files a.txt b.txt --json # JSON for automation
|
|
29
|
+
|
|
30
|
+
# Options
|
|
31
|
+
diff-cli files a.txt b.txt -n 5 # 5 context lines
|
|
32
|
+
diff-cli files a.txt b.txt --ignore-whitespace
|
|
33
|
+
diff-cli files a.txt b.txt --ignore-case
|
|
34
|
+
diff-cli files a.txt b.txt --ignore-blank-lines
|
|
35
|
+
|
|
36
|
+
# Watch mode
|
|
37
|
+
diff-cli watch file1.txt file2.txt
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Exit Codes
|
|
41
|
+
|
|
42
|
+
| Code | Meaning |
|
|
43
|
+
|------|---------|
|
|
44
|
+
| 0 | Files are identical |
|
|
45
|
+
| 1 | Files differ |
|
|
46
|
+
| 2 | Error (file not found, etc.) |
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
diff_cli-0.1.0/SKILL.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# diff-cli - Agent Skill
|
|
2
|
+
|
|
3
|
+
File and text differ with multiple output formats. Compare files, text strings, and directories with various diff formats and ignore options.
|
|
4
|
+
|
|
5
|
+
## Quick Command Reference
|
|
6
|
+
|
|
7
|
+
| Command | Purpose | Key Options |
|
|
8
|
+
|---------|---------|-------------|
|
|
9
|
+
| `diff-cli files <file1> <file2>` | Compare two files | `--format`, `--ignore-*`, `--json` |
|
|
10
|
+
| `diff-cli text "<text1>" "<text2>"` | Compare text strings | `--format`, `--ignore-*`, `--json` |
|
|
11
|
+
| `diff-cli dirs <dir1> <dir2>` | Compare directories | `--recursive`, `--json` |
|
|
12
|
+
| `diff-cli watch <file1> <file2>` | Monitor files for changes | `--interval`, `--format` |
|
|
13
|
+
|
|
14
|
+
## Command Details
|
|
15
|
+
|
|
16
|
+
### File Comparison
|
|
17
|
+
```bash
|
|
18
|
+
# Basic comparison with different formats
|
|
19
|
+
diff-cli files old.txt new.txt # Unified diff (default)
|
|
20
|
+
diff-cli files old.txt new.txt --format side-by-side
|
|
21
|
+
diff-cli files old.txt new.txt --format context
|
|
22
|
+
diff-cli files old.txt new.txt --format minimal
|
|
23
|
+
|
|
24
|
+
# Ignore options
|
|
25
|
+
diff-cli files file1.txt file2.txt --ignore-whitespace
|
|
26
|
+
diff-cli files FILE1.txt file2.txt --ignore-case
|
|
27
|
+
diff-cli files log1.txt log2.txt --ignore-blank-lines
|
|
28
|
+
|
|
29
|
+
# JSON output
|
|
30
|
+
diff-cli files config.json config.json.bak --json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Text String Comparison
|
|
34
|
+
```bash
|
|
35
|
+
# Direct text comparison
|
|
36
|
+
diff-cli text "hello world" "hello universe"
|
|
37
|
+
diff-cli text "line1\nline2" "line1\nmodified" --format unified
|
|
38
|
+
diff-cli text "TEXT" "text" --ignore-case --json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Directory Comparison
|
|
42
|
+
```bash
|
|
43
|
+
# Compare directory contents
|
|
44
|
+
diff-cli dirs project/ backup/
|
|
45
|
+
diff-cli dirs src/ dist/ --json
|
|
46
|
+
diff-cli dirs folder1/ folder2/ --recursive
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Watch Mode
|
|
50
|
+
```bash
|
|
51
|
+
# Monitor files for changes
|
|
52
|
+
diff-cli watch config.ini config.ini.tmp
|
|
53
|
+
diff-cli watch log.txt rotated.log --interval 0.5 --format minimal
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Exit Codes
|
|
57
|
+
|
|
58
|
+
- **0**: Files/texts are identical
|
|
59
|
+
- **1**: Files/texts are different
|
|
60
|
+
- **2**: Error occurred (file not found, binary file, permission denied)
|
|
61
|
+
|
|
62
|
+
Always check exit codes when using in scripts:
|
|
63
|
+
```bash
|
|
64
|
+
if diff-cli files file1.txt file2.txt --format minimal; then
|
|
65
|
+
echo "Files are identical"
|
|
66
|
+
else
|
|
67
|
+
echo "Files differ (exit code: $?)"
|
|
68
|
+
fi
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## JSON Output Schemas
|
|
72
|
+
|
|
73
|
+
### File/Text Comparison (`--json`)
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"files": ["file1.txt", "file2.txt"], // Input files
|
|
77
|
+
"identical": false, // Boolean comparison result
|
|
78
|
+
"format": "unified", // Format used
|
|
79
|
+
"diff": [ // Diff lines (if not minimal)
|
|
80
|
+
"--- file1.txt",
|
|
81
|
+
"+++ file2.txt",
|
|
82
|
+
"@@ -1,1 +1,1 @@",
|
|
83
|
+
"-old line",
|
|
84
|
+
"+new line"
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Minimal Format (`--format minimal --json`)
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"files": ["file1.txt", "file2.txt"],
|
|
93
|
+
"identical": false,
|
|
94
|
+
"format": "minimal",
|
|
95
|
+
"changes": 2, // Total changes
|
|
96
|
+
"additions": 1, // Lines added
|
|
97
|
+
"deletions": 1, // Lines removed
|
|
98
|
+
"total_lines_1": 10, // Lines in file1
|
|
99
|
+
"total_lines_2": 10 // Lines in file2
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Directory Comparison (`--json`)
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"total_files": 15,
|
|
107
|
+
"only_in_first": ["old_file.txt"], // Files only in dir1
|
|
108
|
+
"only_in_second": ["new_file.txt"], // Files only in dir2
|
|
109
|
+
"different": ["config.json", "readme.md"], // Files that differ
|
|
110
|
+
"identical": ["license.txt"], // Identical files
|
|
111
|
+
"summary": {
|
|
112
|
+
"only_in_first": 1,
|
|
113
|
+
"only_in_second": 1,
|
|
114
|
+
"different": 2,
|
|
115
|
+
"identical": 1
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Common Automation Patterns
|
|
121
|
+
|
|
122
|
+
### Check if files are identical
|
|
123
|
+
```bash
|
|
124
|
+
# Simple check
|
|
125
|
+
diff-cli files file1.txt file2.txt --format minimal >/dev/null
|
|
126
|
+
identical=$?
|
|
127
|
+
|
|
128
|
+
# With details
|
|
129
|
+
result=$(diff-cli files file1.txt file2.txt --format minimal --json)
|
|
130
|
+
identical=$(echo "$result" | jq -r '.identical')
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Get change statistics
|
|
134
|
+
```bash
|
|
135
|
+
# Count changes
|
|
136
|
+
stats=$(diff-cli files old.txt new.txt --format minimal --json)
|
|
137
|
+
changes=$(echo "$stats" | jq -r '.changes')
|
|
138
|
+
additions=$(echo "$stats" | jq -r '.additions')
|
|
139
|
+
deletions=$(echo "$stats" | jq -r '.deletions')
|
|
140
|
+
|
|
141
|
+
echo "Changes: $changes (+$additions -$deletions)"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Directory sync validation
|
|
145
|
+
```bash
|
|
146
|
+
# Check if backup is complete
|
|
147
|
+
result=$(diff-cli dirs source/ backup/ --json)
|
|
148
|
+
different=$(echo "$result" | jq -r '.summary.different')
|
|
149
|
+
only_in_source=$(echo "$result" | jq -r '.summary.only_in_first')
|
|
150
|
+
|
|
151
|
+
if [ "$different" -eq 0 ] && [ "$only_in_source" -eq 0 ]; then
|
|
152
|
+
echo "Backup is complete and up-to-date"
|
|
153
|
+
fi
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Monitor configuration changes
|
|
157
|
+
```bash
|
|
158
|
+
# Watch for config changes with logging
|
|
159
|
+
diff-cli watch config.ini config.ini.new --format minimal | while IFS= read -r line; do
|
|
160
|
+
echo "$(date): $line" >> config_changes.log
|
|
161
|
+
done
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Error Handling
|
|
165
|
+
|
|
166
|
+
Common error conditions:
|
|
167
|
+
- **File not found**: Exit code 2, stderr message
|
|
168
|
+
- **Permission denied**: Exit code 2, stderr message
|
|
169
|
+
- **Binary files**: Exit code 2, "Binary files cannot be compared" message
|
|
170
|
+
- **Directory not found**: Exit code 2, stderr message
|
|
171
|
+
|
|
172
|
+
Error output in JSON mode:
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"error": "Binary files cannot be compared in text mode",
|
|
176
|
+
"exit_code": 2
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Integration Examples
|
|
181
|
+
|
|
182
|
+
### With jq for processing
|
|
183
|
+
```bash
|
|
184
|
+
# Extract only the added lines
|
|
185
|
+
diff-cli files old.txt new.txt --json | jq -r '.diff[] | select(startswith("+") and (startswith("+++") | not))'
|
|
186
|
+
|
|
187
|
+
# Get summary statistics
|
|
188
|
+
diff-cli dirs project/ backup/ --json | jq '.summary'
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### In shell scripts
|
|
192
|
+
```bash
|
|
193
|
+
#!/bin/bash
|
|
194
|
+
# Backup validation script
|
|
195
|
+
|
|
196
|
+
source_dir="$1"
|
|
197
|
+
backup_dir="$2"
|
|
198
|
+
|
|
199
|
+
result=$(diff-cli dirs "$source_dir" "$backup_dir" --json)
|
|
200
|
+
exit_code=$?
|
|
201
|
+
|
|
202
|
+
if [ $exit_code -eq 0 ]; then
|
|
203
|
+
echo "✓ Backup is identical to source"
|
|
204
|
+
elif [ $exit_code -eq 1 ]; then
|
|
205
|
+
different=$(echo "$result" | jq -r '.summary.different')
|
|
206
|
+
missing=$(echo "$result" | jq -r '.summary.only_in_first')
|
|
207
|
+
echo "⚠ Backup differs: $different files different, $missing files missing"
|
|
208
|
+
else
|
|
209
|
+
echo "✗ Error occurred during comparison"
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
exit $exit_code
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### In Python (subprocess)
|
|
216
|
+
```python
|
|
217
|
+
import subprocess
|
|
218
|
+
import json
|
|
219
|
+
|
|
220
|
+
def compare_files(file1, file2, format='minimal'):
|
|
221
|
+
"""Compare two files and return results."""
|
|
222
|
+
try:
|
|
223
|
+
result = subprocess.run([
|
|
224
|
+
'diff-cli', 'files', file1, file2,
|
|
225
|
+
'--format', format, '--json'
|
|
226
|
+
], capture_output=True, text=True, check=False)
|
|
227
|
+
|
|
228
|
+
if result.returncode in [0, 1]: # Success or diff found
|
|
229
|
+
return json.loads(result.stdout)
|
|
230
|
+
else: # Error occurred
|
|
231
|
+
return {"error": result.stderr, "exit_code": result.returncode}
|
|
232
|
+
except subprocess.SubprocessError as e:
|
|
233
|
+
return {"error": str(e), "exit_code": -1}
|
|
234
|
+
|
|
235
|
+
# Usage
|
|
236
|
+
diff_result = compare_files('file1.txt', 'file2.txt')
|
|
237
|
+
if diff_result.get('identical'):
|
|
238
|
+
print("Files are identical")
|
|
239
|
+
else:
|
|
240
|
+
print(f"Files differ: {diff_result.get('changes', 'unknown')} changes")
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Prerequisites
|
|
244
|
+
|
|
245
|
+
- Python 3.9+
|
|
246
|
+
- Files must be readable (check permissions)
|
|
247
|
+
- For directory comparison, directories must be accessible
|
|
248
|
+
- Text files only (binary files return error)
|
|
249
|
+
|
|
250
|
+
## Performance Notes
|
|
251
|
+
|
|
252
|
+
- Large files: Use `--format minimal` for fastest comparison
|
|
253
|
+
- Directory comparison: Recursive mode can be slow on large trees
|
|
254
|
+
- Watch mode: Higher intervals reduce CPU usage
|
|
255
|
+
- JSON output: Slightly slower due to formatting overhead
|
|
256
|
+
|
|
257
|
+
## Related Tools
|
|
258
|
+
|
|
259
|
+
- Standard `diff` command (POSIX)
|
|
260
|
+
- `git diff` for version control
|
|
261
|
+
- `rsync --dry-run` for directory synchronization
|
|
262
|
+
- `cmp` for binary file comparison
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""diff-cli: File and text differ with multiple output formats."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
import difflib
|
|
9
|
+
import threading
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, List, Dict, Any
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def colorize_diff_line(line: str, color_enabled: bool = True) -> str:
|
|
17
|
+
"""Add color to diff lines if color is enabled."""
|
|
18
|
+
if not color_enabled or not sys.stdout.isatty():
|
|
19
|
+
return line
|
|
20
|
+
|
|
21
|
+
if line.startswith('+++') or line.startswith('---'):
|
|
22
|
+
return click.style(line, fg='white', bold=True)
|
|
23
|
+
elif line.startswith('@@'):
|
|
24
|
+
return click.style(line, fg='cyan', bold=True)
|
|
25
|
+
elif line.startswith('+'):
|
|
26
|
+
return click.style(line, fg='green')
|
|
27
|
+
elif line.startswith('-'):
|
|
28
|
+
return click.style(line, fg='red')
|
|
29
|
+
elif line.startswith('?'):
|
|
30
|
+
return click.style(line, fg='yellow')
|
|
31
|
+
else:
|
|
32
|
+
return line
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def normalize_text(text: str, ignore_whitespace: bool = False,
|
|
36
|
+
ignore_case: bool = False, ignore_blank_lines: bool = False) -> str:
|
|
37
|
+
"""Normalize text according to ignore options."""
|
|
38
|
+
if ignore_case:
|
|
39
|
+
text = text.lower()
|
|
40
|
+
|
|
41
|
+
if ignore_blank_lines:
|
|
42
|
+
lines = [line for line in text.split('\n') if line.strip()]
|
|
43
|
+
text = '\n'.join(lines)
|
|
44
|
+
|
|
45
|
+
if ignore_whitespace:
|
|
46
|
+
# Replace multiple whitespace with single space, strip line ends
|
|
47
|
+
lines = [' '.join(line.split()) for line in text.split('\n')]
|
|
48
|
+
text = '\n'.join(lines)
|
|
49
|
+
|
|
50
|
+
return text
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def files_are_identical(file1: Path, file2: Path, ignore_whitespace: bool = False,
|
|
54
|
+
ignore_case: bool = False, ignore_blank_lines: bool = False) -> bool:
|
|
55
|
+
"""Check if two files are identical after normalization."""
|
|
56
|
+
try:
|
|
57
|
+
with open(file1, 'r', encoding='utf-8') as f1, open(file2, 'r', encoding='utf-8') as f2:
|
|
58
|
+
text1 = normalize_text(f1.read(), ignore_whitespace, ignore_case, ignore_blank_lines)
|
|
59
|
+
text2 = normalize_text(f2.read(), ignore_whitespace, ignore_case, ignore_blank_lines)
|
|
60
|
+
return text1 == text2
|
|
61
|
+
except UnicodeDecodeError:
|
|
62
|
+
# Binary comparison for non-text files
|
|
63
|
+
with open(file1, 'rb') as f1, open(file2, 'rb') as f2:
|
|
64
|
+
return f1.read() == f2.read()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def unified_diff(text1: str, text2: str, filename1: str = 'file1',
|
|
68
|
+
filename2: str = 'file2', context: int = 3) -> List[str]:
|
|
69
|
+
"""Generate unified diff between two texts."""
|
|
70
|
+
lines1 = text1.splitlines(keepends=True)
|
|
71
|
+
lines2 = text2.splitlines(keepends=True)
|
|
72
|
+
|
|
73
|
+
return list(difflib.unified_diff(
|
|
74
|
+
lines1, lines2,
|
|
75
|
+
fromfile=filename1,
|
|
76
|
+
tofile=filename2,
|
|
77
|
+
n=context
|
|
78
|
+
))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def side_by_side_diff(text1: str, text2: str, width: int = 80) -> List[str]:
|
|
82
|
+
"""Generate side-by-side diff between two texts."""
|
|
83
|
+
lines1 = text1.splitlines()
|
|
84
|
+
lines2 = text2.splitlines()
|
|
85
|
+
|
|
86
|
+
max_len = max(len(lines1), len(lines2))
|
|
87
|
+
col_width = width // 2 - 2
|
|
88
|
+
|
|
89
|
+
result = []
|
|
90
|
+
result.append(f"{'File 1':<{col_width}} | {'File 2':<{col_width}}")
|
|
91
|
+
result.append(f"{'-' * col_width} | {'-' * col_width}")
|
|
92
|
+
|
|
93
|
+
for i in range(max_len):
|
|
94
|
+
left = lines1[i] if i < len(lines1) else ""
|
|
95
|
+
right = lines2[i] if i < len(lines2) else ""
|
|
96
|
+
|
|
97
|
+
# Truncate long lines
|
|
98
|
+
if len(left) > col_width:
|
|
99
|
+
left = left[:col_width-3] + "..."
|
|
100
|
+
if len(right) > col_width:
|
|
101
|
+
right = right[:col_width-3] + "..."
|
|
102
|
+
|
|
103
|
+
# Determine line status
|
|
104
|
+
if i >= len(lines1):
|
|
105
|
+
marker = "+"
|
|
106
|
+
elif i >= len(lines2):
|
|
107
|
+
marker = "-"
|
|
108
|
+
elif left != right:
|
|
109
|
+
marker = "!"
|
|
110
|
+
else:
|
|
111
|
+
marker = " "
|
|
112
|
+
|
|
113
|
+
result.append(f"{left:<{col_width}} {marker} {right:<{col_width}}")
|
|
114
|
+
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def context_diff(text1: str, text2: str, filename1: str = 'file1',
|
|
119
|
+
filename2: str = 'file2', context: int = 3) -> List[str]:
|
|
120
|
+
"""Generate context diff between two texts."""
|
|
121
|
+
lines1 = text1.splitlines(keepends=True)
|
|
122
|
+
lines2 = text2.splitlines(keepends=True)
|
|
123
|
+
|
|
124
|
+
return list(difflib.context_diff(
|
|
125
|
+
lines1, lines2,
|
|
126
|
+
fromfile=filename1,
|
|
127
|
+
tofile=filename2,
|
|
128
|
+
n=context
|
|
129
|
+
))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def minimal_diff(text1: str, text2: str) -> Dict[str, Any]:
|
|
133
|
+
"""Generate minimal diff summary."""
|
|
134
|
+
lines1 = text1.splitlines()
|
|
135
|
+
lines2 = text2.splitlines()
|
|
136
|
+
|
|
137
|
+
if lines1 == lines2:
|
|
138
|
+
return {
|
|
139
|
+
"identical": True,
|
|
140
|
+
"changes": 0,
|
|
141
|
+
"additions": 0,
|
|
142
|
+
"deletions": 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Use difflib to get a basic comparison
|
|
146
|
+
diff = list(difflib.unified_diff(lines1, lines2, n=0))
|
|
147
|
+
|
|
148
|
+
additions = sum(1 for line in diff if line.startswith('+') and not line.startswith('+++'))
|
|
149
|
+
deletions = sum(1 for line in diff if line.startswith('-') and not line.startswith('---'))
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
"identical": False,
|
|
153
|
+
"changes": additions + deletions,
|
|
154
|
+
"additions": additions,
|
|
155
|
+
"deletions": deletions,
|
|
156
|
+
"total_lines_1": len(lines1),
|
|
157
|
+
"total_lines_2": len(lines2)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def scan_directory(directory: Path, recursive: bool = True) -> List[Path]:
|
|
162
|
+
"""Scan directory for files."""
|
|
163
|
+
files = []
|
|
164
|
+
if recursive:
|
|
165
|
+
for root, dirs, filenames in os.walk(directory):
|
|
166
|
+
for filename in filenames:
|
|
167
|
+
files.append(Path(root) / filename)
|
|
168
|
+
else:
|
|
169
|
+
for item in directory.iterdir():
|
|
170
|
+
if item.is_file():
|
|
171
|
+
files.append(item)
|
|
172
|
+
return sorted(files)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def compare_directories(dir1: Path, dir2: Path, recursive: bool = True) -> Dict[str, Any]:
|
|
176
|
+
"""Compare two directories."""
|
|
177
|
+
files1 = {f.relative_to(dir1): f for f in scan_directory(dir1, recursive)}
|
|
178
|
+
files2 = {f.relative_to(dir2): f for f in scan_directory(dir2, recursive)}
|
|
179
|
+
|
|
180
|
+
all_files = set(files1.keys()) | set(files2.keys())
|
|
181
|
+
only_in_1 = set(files1.keys()) - set(files2.keys())
|
|
182
|
+
only_in_2 = set(files2.keys()) - set(files1.keys())
|
|
183
|
+
common_files = set(files1.keys()) & set(files2.keys())
|
|
184
|
+
|
|
185
|
+
different_files = []
|
|
186
|
+
identical_files = []
|
|
187
|
+
|
|
188
|
+
for rel_path in common_files:
|
|
189
|
+
file1 = files1[rel_path]
|
|
190
|
+
file2 = files2[rel_path]
|
|
191
|
+
|
|
192
|
+
if files_are_identical(file1, file2):
|
|
193
|
+
identical_files.append(str(rel_path))
|
|
194
|
+
else:
|
|
195
|
+
different_files.append(str(rel_path))
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"total_files": len(all_files),
|
|
199
|
+
"only_in_first": sorted([str(f) for f in only_in_1]),
|
|
200
|
+
"only_in_second": sorted([str(f) for f in only_in_2]),
|
|
201
|
+
"different": sorted(different_files),
|
|
202
|
+
"identical": sorted(identical_files),
|
|
203
|
+
"summary": {
|
|
204
|
+
"only_in_first": len(only_in_1),
|
|
205
|
+
"only_in_second": len(only_in_2),
|
|
206
|
+
"different": len(different_files),
|
|
207
|
+
"identical": len(identical_files)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@click.group()
|
|
213
|
+
@click.version_option(version="0.1.0", prog_name="diff-cli")
|
|
214
|
+
def cli():
|
|
215
|
+
"""diff-cli: File and text differ with multiple output formats."""
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@cli.command()
|
|
220
|
+
@click.argument('file1', type=click.Path(exists=True, path_type=Path))
|
|
221
|
+
@click.argument('file2', type=click.Path(exists=True, path_type=Path))
|
|
222
|
+
@click.option('--format', '-f', type=click.Choice(['unified', 'side-by-side', 'context', 'minimal']),
|
|
223
|
+
default='unified', help='Diff output format')
|
|
224
|
+
@click.option('--context', '-n', default=3, help='Number of context lines')
|
|
225
|
+
@click.option('--ignore-whitespace', '-w', is_flag=True, help='Ignore whitespace differences')
|
|
226
|
+
@click.option('--ignore-case', '-i', is_flag=True, help='Ignore case differences')
|
|
227
|
+
@click.option('--ignore-blank-lines', '-B', is_flag=True, help='Ignore blank lines')
|
|
228
|
+
@click.option('--no-color', is_flag=True, help='Disable colored output')
|
|
229
|
+
@click.option('--json', 'output_json', is_flag=True, help='Output in JSON format')
|
|
230
|
+
def files(file1: Path, file2: Path, format: str, context: int,
|
|
231
|
+
ignore_whitespace: bool, ignore_case: bool, ignore_blank_lines: bool,
|
|
232
|
+
no_color: bool, output_json: bool):
|
|
233
|
+
"""Compare two files."""
|
|
234
|
+
try:
|
|
235
|
+
with open(file1, 'r', encoding='utf-8') as f1, open(file2, 'r', encoding='utf-8') as f2:
|
|
236
|
+
text1 = normalize_text(f1.read(), ignore_whitespace, ignore_case, ignore_blank_lines)
|
|
237
|
+
text2 = normalize_text(f2.read(), ignore_whitespace, ignore_case, ignore_blank_lines)
|
|
238
|
+
except UnicodeDecodeError:
|
|
239
|
+
if output_json:
|
|
240
|
+
click.echo(json.dumps({"error": "Binary files cannot be compared in text mode", "exit_code": 2}))
|
|
241
|
+
else:
|
|
242
|
+
click.echo("Error: Binary files cannot be compared in text mode", err=True)
|
|
243
|
+
sys.exit(2)
|
|
244
|
+
|
|
245
|
+
# Check if files are identical
|
|
246
|
+
identical = text1 == text2
|
|
247
|
+
|
|
248
|
+
if output_json:
|
|
249
|
+
result = {
|
|
250
|
+
"files": [str(file1), str(file2)],
|
|
251
|
+
"identical": identical,
|
|
252
|
+
"format": format
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if format == 'minimal':
|
|
256
|
+
result.update(minimal_diff(text1, text2))
|
|
257
|
+
else:
|
|
258
|
+
if format == 'unified':
|
|
259
|
+
diff_lines = unified_diff(text1, text2, str(file1), str(file2), context)
|
|
260
|
+
elif format == 'side-by-side':
|
|
261
|
+
diff_lines = side_by_side_diff(text1, text2)
|
|
262
|
+
elif format == 'context':
|
|
263
|
+
diff_lines = context_diff(text1, text2, str(file1), str(file2), context)
|
|
264
|
+
|
|
265
|
+
result["diff"] = diff_lines
|
|
266
|
+
|
|
267
|
+
click.echo(json.dumps(result, indent=2))
|
|
268
|
+
else:
|
|
269
|
+
if identical:
|
|
270
|
+
click.echo("Files are identical")
|
|
271
|
+
else:
|
|
272
|
+
if format == 'minimal':
|
|
273
|
+
summary = minimal_diff(text1, text2)
|
|
274
|
+
click.echo(f"Files differ: {summary['changes']} changes ({summary['additions']} additions, {summary['deletions']} deletions)")
|
|
275
|
+
else:
|
|
276
|
+
if format == 'unified':
|
|
277
|
+
diff_lines = unified_diff(text1, text2, str(file1), str(file2), context)
|
|
278
|
+
elif format == 'side-by-side':
|
|
279
|
+
diff_lines = side_by_side_diff(text1, text2)
|
|
280
|
+
elif format == 'context':
|
|
281
|
+
diff_lines = context_diff(text1, text2, str(file1), str(file2), context)
|
|
282
|
+
|
|
283
|
+
color_enabled = not no_color
|
|
284
|
+
for line in diff_lines:
|
|
285
|
+
click.echo(colorize_diff_line(line.rstrip(), color_enabled))
|
|
286
|
+
|
|
287
|
+
sys.exit(0 if identical else 1)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@cli.command()
|
|
291
|
+
@click.argument('text1')
|
|
292
|
+
@click.argument('text2')
|
|
293
|
+
@click.option('--format', '-f', type=click.Choice(['unified', 'side-by-side', 'context', 'minimal']),
|
|
294
|
+
default='unified', help='Diff output format')
|
|
295
|
+
@click.option('--context', '-n', default=3, help='Number of context lines')
|
|
296
|
+
@click.option('--ignore-whitespace', '-w', is_flag=True, help='Ignore whitespace differences')
|
|
297
|
+
@click.option('--ignore-case', '-i', is_flag=True, help='Ignore case differences')
|
|
298
|
+
@click.option('--ignore-blank-lines', '-B', is_flag=True, help='Ignore blank lines')
|
|
299
|
+
@click.option('--no-color', is_flag=True, help='Disable colored output')
|
|
300
|
+
@click.option('--json', 'output_json', is_flag=True, help='Output in JSON format')
|
|
301
|
+
def text(text1: str, text2: str, format: str, context: int,
|
|
302
|
+
ignore_whitespace: bool, ignore_case: bool, ignore_blank_lines: bool,
|
|
303
|
+
no_color: bool, output_json: bool):
|
|
304
|
+
"""Compare two text strings."""
|
|
305
|
+
normalized_text1 = normalize_text(text1, ignore_whitespace, ignore_case, ignore_blank_lines)
|
|
306
|
+
normalized_text2 = normalize_text(text2, ignore_whitespace, ignore_case, ignore_blank_lines)
|
|
307
|
+
|
|
308
|
+
identical = normalized_text1 == normalized_text2
|
|
309
|
+
|
|
310
|
+
if output_json:
|
|
311
|
+
result = {
|
|
312
|
+
"texts": [text1, text2],
|
|
313
|
+
"identical": identical,
|
|
314
|
+
"format": format
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if format == 'minimal':
|
|
318
|
+
result.update(minimal_diff(normalized_text1, normalized_text2))
|
|
319
|
+
else:
|
|
320
|
+
if format == 'unified':
|
|
321
|
+
diff_lines = unified_diff(normalized_text1, normalized_text2, "text1", "text2", context)
|
|
322
|
+
elif format == 'side-by-side':
|
|
323
|
+
diff_lines = side_by_side_diff(normalized_text1, normalized_text2)
|
|
324
|
+
elif format == 'context':
|
|
325
|
+
diff_lines = context_diff(normalized_text1, normalized_text2, "text1", "text2", context)
|
|
326
|
+
|
|
327
|
+
result["diff"] = diff_lines
|
|
328
|
+
|
|
329
|
+
click.echo(json.dumps(result, indent=2))
|
|
330
|
+
else:
|
|
331
|
+
if identical:
|
|
332
|
+
click.echo("Texts are identical")
|
|
333
|
+
else:
|
|
334
|
+
if format == 'minimal':
|
|
335
|
+
summary = minimal_diff(normalized_text1, normalized_text2)
|
|
336
|
+
click.echo(f"Texts differ: {summary['changes']} changes ({summary['additions']} additions, {summary['deletions']} deletions)")
|
|
337
|
+
else:
|
|
338
|
+
if format == 'unified':
|
|
339
|
+
diff_lines = unified_diff(normalized_text1, normalized_text2, "text1", "text2", context)
|
|
340
|
+
elif format == 'side-by-side':
|
|
341
|
+
diff_lines = side_by_side_diff(normalized_text1, normalized_text2)
|
|
342
|
+
elif format == 'context':
|
|
343
|
+
diff_lines = context_diff(normalized_text1, normalized_text2, "text1", "text2", context)
|
|
344
|
+
|
|
345
|
+
color_enabled = not no_color
|
|
346
|
+
for line in diff_lines:
|
|
347
|
+
click.echo(colorize_diff_line(line.rstrip(), color_enabled))
|
|
348
|
+
|
|
349
|
+
sys.exit(0 if identical else 1)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@cli.command()
|
|
353
|
+
@click.argument('dir1', type=click.Path(exists=True, file_okay=False, path_type=Path))
|
|
354
|
+
@click.argument('dir2', type=click.Path(exists=True, file_okay=False, path_type=Path))
|
|
355
|
+
@click.option('--recursive', '-r', is_flag=True, default=True, help='Compare directories recursively')
|
|
356
|
+
@click.option('--json', 'output_json', is_flag=True, help='Output in JSON format')
|
|
357
|
+
def dirs(dir1: Path, dir2: Path, recursive: bool, output_json: bool):
|
|
358
|
+
"""Compare two directories."""
|
|
359
|
+
result = compare_directories(dir1, dir2, recursive)
|
|
360
|
+
|
|
361
|
+
if output_json:
|
|
362
|
+
click.echo(json.dumps(result, indent=2))
|
|
363
|
+
else:
|
|
364
|
+
summary = result["summary"]
|
|
365
|
+
click.echo(f"Directory comparison: {dir1} vs {dir2}")
|
|
366
|
+
click.echo(f"Total files: {result['total_files']}")
|
|
367
|
+
click.echo(f"Only in first: {summary['only_in_first']}")
|
|
368
|
+
click.echo(f"Only in second: {summary['only_in_second']}")
|
|
369
|
+
click.echo(f"Different: {summary['different']}")
|
|
370
|
+
click.echo(f"Identical: {summary['identical']}")
|
|
371
|
+
|
|
372
|
+
if result['only_in_first']:
|
|
373
|
+
click.echo(f"\nFiles only in {dir1}:")
|
|
374
|
+
for f in result['only_in_first']:
|
|
375
|
+
click.echo(f" - {f}")
|
|
376
|
+
|
|
377
|
+
if result['only_in_second']:
|
|
378
|
+
click.echo(f"\nFiles only in {dir2}:")
|
|
379
|
+
for f in result['only_in_second']:
|
|
380
|
+
click.echo(f" + {f}")
|
|
381
|
+
|
|
382
|
+
if result['different']:
|
|
383
|
+
click.echo(f"\nDifferent files:")
|
|
384
|
+
for f in result['different']:
|
|
385
|
+
click.echo(f" ! {f}")
|
|
386
|
+
|
|
387
|
+
# Exit with 1 if there are any differences
|
|
388
|
+
has_differences = (summary['only_in_first'] > 0 or
|
|
389
|
+
summary['only_in_second'] > 0 or
|
|
390
|
+
summary['different'] > 0)
|
|
391
|
+
sys.exit(1 if has_differences else 0)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@cli.command()
|
|
395
|
+
@click.argument('file1', type=click.Path(exists=True, path_type=Path))
|
|
396
|
+
@click.argument('file2', type=click.Path(exists=True, path_type=Path))
|
|
397
|
+
@click.option('--interval', '-i', default=1.0, help='Check interval in seconds')
|
|
398
|
+
@click.option('--format', '-f', type=click.Choice(['unified', 'side-by-side', 'context', 'minimal']),
|
|
399
|
+
default='unified', help='Diff output format')
|
|
400
|
+
@click.option('--no-color', is_flag=True, help='Disable colored output')
|
|
401
|
+
def watch(file1: Path, file2: Path, interval: float, format: str, no_color: bool):
|
|
402
|
+
"""Watch two files for changes and show diff when they change."""
|
|
403
|
+
click.echo(f"Watching {file1} and {file2} for changes (Ctrl+C to stop)...")
|
|
404
|
+
|
|
405
|
+
last_mtime1 = file1.stat().st_mtime if file1.exists() else 0
|
|
406
|
+
last_mtime2 = file2.stat().st_mtime if file2.exists() else 0
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
while True:
|
|
410
|
+
time.sleep(interval)
|
|
411
|
+
|
|
412
|
+
current_mtime1 = file1.stat().st_mtime if file1.exists() else 0
|
|
413
|
+
current_mtime2 = file2.stat().st_mtime if file2.exists() else 0
|
|
414
|
+
|
|
415
|
+
if current_mtime1 != last_mtime1 or current_mtime2 != last_mtime2:
|
|
416
|
+
click.echo(f"\n--- Change detected at {time.strftime('%Y-%m-%d %H:%M:%S')} ---")
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
with open(file1, 'r', encoding='utf-8') as f1, open(file2, 'r', encoding='utf-8') as f2:
|
|
420
|
+
text1 = f1.read()
|
|
421
|
+
text2 = f2.read()
|
|
422
|
+
|
|
423
|
+
if text1 == text2:
|
|
424
|
+
click.echo("Files are now identical")
|
|
425
|
+
else:
|
|
426
|
+
if format == 'minimal':
|
|
427
|
+
summary = minimal_diff(text1, text2)
|
|
428
|
+
click.echo(f"Files differ: {summary['changes']} changes")
|
|
429
|
+
else:
|
|
430
|
+
if format == 'unified':
|
|
431
|
+
diff_lines = unified_diff(text1, text2, str(file1), str(file2))
|
|
432
|
+
elif format == 'side-by-side':
|
|
433
|
+
diff_lines = side_by_side_diff(text1, text2)
|
|
434
|
+
elif format == 'context':
|
|
435
|
+
diff_lines = context_diff(text1, text2, str(file1), str(file2))
|
|
436
|
+
|
|
437
|
+
color_enabled = not no_color
|
|
438
|
+
for line in diff_lines:
|
|
439
|
+
click.echo(colorize_diff_line(line.rstrip(), color_enabled))
|
|
440
|
+
|
|
441
|
+
except Exception as e:
|
|
442
|
+
click.echo(f"Error reading files: {e}", err=True)
|
|
443
|
+
|
|
444
|
+
last_mtime1 = current_mtime1
|
|
445
|
+
last_mtime2 = current_mtime2
|
|
446
|
+
|
|
447
|
+
except KeyboardInterrupt:
|
|
448
|
+
click.echo("\nStopped watching.")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
if __name__ == '__main__':
|
|
452
|
+
cli()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "diff-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "File and text differ with multiple output formats"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{ name = "Marcus", email = "marcus.builds.things@gmail.com" }]
|
|
12
|
+
keywords = ["diff", "compare", "cli", "unified", "side-by-side", "text"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Text Processing :: General",
|
|
24
|
+
"Topic :: Utilities",
|
|
25
|
+
]
|
|
26
|
+
requires-python = ">=3.9"
|
|
27
|
+
dependencies = ["click>=8.0"]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
diff-cli = "diffcli.cli:cli"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/marcusbuildsthings-droid/diff-cli"
|
|
34
|
+
Repository = "https://github.com/marcusbuildsthings-droid/diff-cli"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["diffcli"]
|