casecrash 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.
- casecrash-0.1.0/LICENSE +21 -0
- casecrash-0.1.0/PKG-INFO +132 -0
- casecrash-0.1.0/README.md +112 -0
- casecrash-0.1.0/pyproject.toml +32 -0
- casecrash-0.1.0/setup.cfg +4 -0
- casecrash-0.1.0/src/casecrash/__init__.py +5 -0
- casecrash-0.1.0/src/casecrash/cli.py +147 -0
- casecrash-0.1.0/src/casecrash/core.py +285 -0
- casecrash-0.1.0/src/casecrash.egg-info/PKG-INFO +132 -0
- casecrash-0.1.0/src/casecrash.egg-info/SOURCES.txt +13 -0
- casecrash-0.1.0/src/casecrash.egg-info/dependency_links.txt +1 -0
- casecrash-0.1.0/src/casecrash.egg-info/entry_points.txt +2 -0
- casecrash-0.1.0/src/casecrash.egg-info/top_level.txt +1 -0
- casecrash-0.1.0/tests/test_cli.py +198 -0
- casecrash-0.1.0/tests/test_core.py +297 -0
casecrash-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hao li
|
|
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.
|
casecrash-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: casecrash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Find filenames in your git index that would collide on macOS or Windows checkouts
|
|
5
|
+
Author: hao li
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/hahahahahahahahah6/casecrash
|
|
8
|
+
Keywords: git,macos,windows,filename,case-sensitivity,unicode,ci
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
15
|
+
Classifier: Topic :: Utilities
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# casecrash
|
|
22
|
+
|
|
23
|
+
**Git can track two files your Mac cannot check out.**
|
|
24
|
+
|
|
25
|
+
`README.md` and `readme.md` can sit side by side in a git repo — until
|
|
26
|
+
someone on macOS or Windows clones it and the checkout explodes. The same
|
|
27
|
+
goes for Unicode lookalikes: NFC `café` vs NFD `café` look identical in
|
|
28
|
+
your terminal but are different byte sequences, and macOS treats them as
|
|
29
|
+
the same file.
|
|
30
|
+
|
|
31
|
+
casecrash reads your git index and flags every pair of tracked files that
|
|
32
|
+
would collide on a case-insensitive or Unicode-normalizing filesystem —
|
|
33
|
+
*before* it breaks a teammate's checkout.
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install casecrash
|
|
39
|
+
cd your-repo
|
|
40
|
+
casecrash
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
casecrash: found 2 filename collision(s) in /tmp/demorepo (5 files scanned):
|
|
45
|
+
|
|
46
|
+
[normalization] collide after Unicode normalization (macOS):
|
|
47
|
+
'cafe\u0301.md' [NFD]
|
|
48
|
+
'caf\u00e9.md' [NFC]
|
|
49
|
+
-> breaks on: macOS
|
|
50
|
+
|
|
51
|
+
[case] collide on case-insensitive filesystems (macOS, Windows):
|
|
52
|
+
'README.md'
|
|
53
|
+
'readme.md'
|
|
54
|
+
-> breaks on: macOS, Windows
|
|
55
|
+
|
|
56
|
+
Fix: rename one of each colliding pair before a macOS/Windows user clones this repo. 'git mv' the odd one out and commit.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
No collisions? Exit 0 and a clean bill of health:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
casecrash: no filename collisions in the git index (128 files scanned).
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## What it detects
|
|
66
|
+
|
|
67
|
+
| Kind | Example | Breaks on |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `case` | `README.md` vs `readme.md`, `É` vs `é` | macOS, Windows |
|
|
70
|
+
| `normalization` | NFC `café` vs NFD `café` | macOS (APFS compares names without regard to normalization) |
|
|
71
|
+
| `exact-duplicate` | identical index entry twice | everywhere (index corruption) |
|
|
72
|
+
|
|
73
|
+
Detection is done on the git index (`git ls-files`), not the working
|
|
74
|
+
tree, so it works on any OS — a Linux CI runner can catch a collision
|
|
75
|
+
that would only bite macOS users. Filenames are handled as raw bytes, so
|
|
76
|
+
undecodable names can never crash the tool. Submodules are skipped (and
|
|
77
|
+
counted).
|
|
78
|
+
|
|
79
|
+
## CI usage
|
|
80
|
+
|
|
81
|
+
casecrash exits **1** when collisions are found, **0** when clean, **2** on
|
|
82
|
+
errors — so it drops straight into any pipeline:
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
# .github/workflows/casecrash.yml
|
|
86
|
+
name: casecheck
|
|
87
|
+
on: [push, pull_request]
|
|
88
|
+
jobs:
|
|
89
|
+
casecrash:
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
steps:
|
|
92
|
+
- uses: actions/checkout@v4
|
|
93
|
+
- uses: actions/setup-python@v5
|
|
94
|
+
with:
|
|
95
|
+
python-version: "3.12"
|
|
96
|
+
- run: pip install casecrash
|
|
97
|
+
- run: casecrash
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
JSON output for scripting:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
casecrash --format json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Why this happens
|
|
107
|
+
|
|
108
|
+
Git was born on case-sensitive Linux filesystems and its index happily
|
|
109
|
+
stores `File.txt` and `file.txt` as two separate entries. macOS and
|
|
110
|
+
Windows disagree: checking out the second name silently overwrites the
|
|
111
|
+
first (or the clone fails outright). Normalization collisions are
|
|
112
|
+
sneakier — macOS's HFS+/APFS normalizes filenames, so two
|
|
113
|
+
byte-different, canonically-equivalent names are the same file there.
|
|
114
|
+
|
|
115
|
+
This bites real projects: agents and scripts that generate files with
|
|
116
|
+
slightly different casings, Unicode filenames pasted from different
|
|
117
|
+
platforms, and the occasional CVE (see
|
|
118
|
+
[CVE-2021-21300](https://github.blog/security/vulnerability-research/github-security-lab-cve-2021-21300/),
|
|
119
|
+
where git's own checkout could be tricked on case-insensitive
|
|
120
|
+
filesystems).
|
|
121
|
+
|
|
122
|
+
## Install
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pip install casecrash
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Requires Python 3.9+ and git. Zero dependencies.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# casecrash
|
|
2
|
+
|
|
3
|
+
**Git can track two files your Mac cannot check out.**
|
|
4
|
+
|
|
5
|
+
`README.md` and `readme.md` can sit side by side in a git repo — until
|
|
6
|
+
someone on macOS or Windows clones it and the checkout explodes. The same
|
|
7
|
+
goes for Unicode lookalikes: NFC `café` vs NFD `café` look identical in
|
|
8
|
+
your terminal but are different byte sequences, and macOS treats them as
|
|
9
|
+
the same file.
|
|
10
|
+
|
|
11
|
+
casecrash reads your git index and flags every pair of tracked files that
|
|
12
|
+
would collide on a case-insensitive or Unicode-normalizing filesystem —
|
|
13
|
+
*before* it breaks a teammate's checkout.
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install casecrash
|
|
19
|
+
cd your-repo
|
|
20
|
+
casecrash
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
casecrash: found 2 filename collision(s) in /tmp/demorepo (5 files scanned):
|
|
25
|
+
|
|
26
|
+
[normalization] collide after Unicode normalization (macOS):
|
|
27
|
+
'cafe\u0301.md' [NFD]
|
|
28
|
+
'caf\u00e9.md' [NFC]
|
|
29
|
+
-> breaks on: macOS
|
|
30
|
+
|
|
31
|
+
[case] collide on case-insensitive filesystems (macOS, Windows):
|
|
32
|
+
'README.md'
|
|
33
|
+
'readme.md'
|
|
34
|
+
-> breaks on: macOS, Windows
|
|
35
|
+
|
|
36
|
+
Fix: rename one of each colliding pair before a macOS/Windows user clones this repo. 'git mv' the odd one out and commit.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
No collisions? Exit 0 and a clean bill of health:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
casecrash: no filename collisions in the git index (128 files scanned).
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What it detects
|
|
46
|
+
|
|
47
|
+
| Kind | Example | Breaks on |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `case` | `README.md` vs `readme.md`, `É` vs `é` | macOS, Windows |
|
|
50
|
+
| `normalization` | NFC `café` vs NFD `café` | macOS (APFS compares names without regard to normalization) |
|
|
51
|
+
| `exact-duplicate` | identical index entry twice | everywhere (index corruption) |
|
|
52
|
+
|
|
53
|
+
Detection is done on the git index (`git ls-files`), not the working
|
|
54
|
+
tree, so it works on any OS — a Linux CI runner can catch a collision
|
|
55
|
+
that would only bite macOS users. Filenames are handled as raw bytes, so
|
|
56
|
+
undecodable names can never crash the tool. Submodules are skipped (and
|
|
57
|
+
counted).
|
|
58
|
+
|
|
59
|
+
## CI usage
|
|
60
|
+
|
|
61
|
+
casecrash exits **1** when collisions are found, **0** when clean, **2** on
|
|
62
|
+
errors — so it drops straight into any pipeline:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
# .github/workflows/casecrash.yml
|
|
66
|
+
name: casecheck
|
|
67
|
+
on: [push, pull_request]
|
|
68
|
+
jobs:
|
|
69
|
+
casecrash:
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v4
|
|
73
|
+
- uses: actions/setup-python@v5
|
|
74
|
+
with:
|
|
75
|
+
python-version: "3.12"
|
|
76
|
+
- run: pip install casecrash
|
|
77
|
+
- run: casecrash
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
JSON output for scripting:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
casecrash --format json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Why this happens
|
|
87
|
+
|
|
88
|
+
Git was born on case-sensitive Linux filesystems and its index happily
|
|
89
|
+
stores `File.txt` and `file.txt` as two separate entries. macOS and
|
|
90
|
+
Windows disagree: checking out the second name silently overwrites the
|
|
91
|
+
first (or the clone fails outright). Normalization collisions are
|
|
92
|
+
sneakier — macOS's HFS+/APFS normalizes filenames, so two
|
|
93
|
+
byte-different, canonically-equivalent names are the same file there.
|
|
94
|
+
|
|
95
|
+
This bites real projects: agents and scripts that generate files with
|
|
96
|
+
slightly different casings, Unicode filenames pasted from different
|
|
97
|
+
platforms, and the occasional CVE (see
|
|
98
|
+
[CVE-2021-21300](https://github.blog/security/vulnerability-research/github-security-lab-cve-2021-21300/),
|
|
99
|
+
where git's own checkout could be tricked on case-insensitive
|
|
100
|
+
filesystems).
|
|
101
|
+
|
|
102
|
+
## Install
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pip install casecrash
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Requires Python 3.9+ and git. Zero dependencies.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "casecrash"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Find filenames in your git index that would collide on macOS or Windows checkouts"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "hao li"}]
|
|
13
|
+
keywords = ["git", "macos", "windows", "filename", "case-sensitivity", "unicode", "ci"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
21
|
+
"Topic :: Utilities",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
casecrash = "casecrash.cli:main"
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/hahahahahahahahah6/casecrash"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
include = ["casecrash*"]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Command-line interface for casecrash."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .core import (
|
|
12
|
+
GitNotFound,
|
|
13
|
+
NotAGitRepo,
|
|
14
|
+
escaped_display,
|
|
15
|
+
normalization_form,
|
|
16
|
+
safe_display,
|
|
17
|
+
scan,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
EXIT_OK = 0
|
|
21
|
+
EXIT_COLLISIONS = 1
|
|
22
|
+
EXIT_ERROR = 2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
26
|
+
parser = argparse.ArgumentParser(
|
|
27
|
+
prog="casecrash",
|
|
28
|
+
description=(
|
|
29
|
+
"Find filenames in your git index that would collide on "
|
|
30
|
+
"case-insensitive or Unicode-normalizing filesystems "
|
|
31
|
+
"(macOS, Windows) — before they break someone's checkout."
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"path",
|
|
36
|
+
nargs="?",
|
|
37
|
+
default=".",
|
|
38
|
+
help="directory inside the git repository to scan (default: .)",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--format",
|
|
42
|
+
choices=["text", "json"],
|
|
43
|
+
default="text",
|
|
44
|
+
help="output format (default: text)",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--version",
|
|
48
|
+
action="version",
|
|
49
|
+
version="casecrash " + __version__,
|
|
50
|
+
)
|
|
51
|
+
return parser
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _platform_note(kind: str) -> str:
|
|
55
|
+
if kind == "case":
|
|
56
|
+
return "collide on case-insensitive filesystems (macOS, Windows)"
|
|
57
|
+
if kind == "normalization":
|
|
58
|
+
return "collide after Unicode normalization (macOS)"
|
|
59
|
+
return "identical index entries (index corruption)"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _format_name(raw: bytes) -> str:
|
|
63
|
+
form = normalization_form(raw)
|
|
64
|
+
shown = escaped_display(raw)
|
|
65
|
+
if form:
|
|
66
|
+
return "'%s' [%s]" % (shown, form)
|
|
67
|
+
return "'%s'" % shown
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def render_text(result) -> str:
|
|
71
|
+
lines = []
|
|
72
|
+
if not result.collisions:
|
|
73
|
+
lines.append(
|
|
74
|
+
"casecrash: no filename collisions in the git index "
|
|
75
|
+
"(%d files scanned%s)." % (
|
|
76
|
+
result.files_scanned,
|
|
77
|
+
", %d submodule(s) skipped" % result.gitlinks_skipped
|
|
78
|
+
if result.gitlinks_skipped else "",
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return "\n".join(lines) + "\n"
|
|
82
|
+
|
|
83
|
+
lines.append(
|
|
84
|
+
"casecrash: found %d filename collision(s) in %s "
|
|
85
|
+
"(%d files scanned):"
|
|
86
|
+
% (len(result.collisions), result.repo_root, result.files_scanned)
|
|
87
|
+
)
|
|
88
|
+
for collision in result.collisions:
|
|
89
|
+
lines.append("")
|
|
90
|
+
lines.append("[%s] %s:" % (collision.kind, _platform_note(collision.kind)))
|
|
91
|
+
for raw in collision.names:
|
|
92
|
+
lines.append(" %s" % _format_name(raw))
|
|
93
|
+
lines.append(
|
|
94
|
+
" -> breaks on: %s" % ", ".join(collision.platforms)
|
|
95
|
+
)
|
|
96
|
+
if result.gitlinks_skipped:
|
|
97
|
+
lines.append("")
|
|
98
|
+
lines.append(
|
|
99
|
+
"note: %d submodule(s) skipped (not checked)"
|
|
100
|
+
% result.gitlinks_skipped
|
|
101
|
+
)
|
|
102
|
+
lines.append("")
|
|
103
|
+
lines.append(
|
|
104
|
+
"Fix: rename one of each colliding pair before a macOS/Windows "
|
|
105
|
+
"user clones this repo. "
|
|
106
|
+
"'git mv' the odd one out and commit."
|
|
107
|
+
)
|
|
108
|
+
return "\n".join(lines) + "\n"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def render_json(result) -> str:
|
|
112
|
+
payload = {
|
|
113
|
+
"tool": "casecrash",
|
|
114
|
+
"version": __version__,
|
|
115
|
+
"repo_root": result.repo_root,
|
|
116
|
+
"files_scanned": result.files_scanned,
|
|
117
|
+
"gitlinks_skipped": result.gitlinks_skipped,
|
|
118
|
+
"collisions": [
|
|
119
|
+
{
|
|
120
|
+
"kind": collision.kind,
|
|
121
|
+
"platforms": collision.platforms,
|
|
122
|
+
"files": [safe_display(n) for n in collision.names],
|
|
123
|
+
}
|
|
124
|
+
for collision in result.collisions
|
|
125
|
+
],
|
|
126
|
+
}
|
|
127
|
+
return json.dumps(payload, indent=2, ensure_ascii=True) + "\n"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def main(argv: list[str] | None = None) -> int:
|
|
131
|
+
args = build_parser().parse_args(argv)
|
|
132
|
+
try:
|
|
133
|
+
result = scan(args.path)
|
|
134
|
+
except (NotAGitRepo, GitNotFound) as exc:
|
|
135
|
+
print("casecrash: error: %s" % exc, file=sys.stderr)
|
|
136
|
+
return EXIT_ERROR
|
|
137
|
+
|
|
138
|
+
if args.format == "json":
|
|
139
|
+
sys.stdout.write(render_json(result))
|
|
140
|
+
else:
|
|
141
|
+
sys.stdout.write(render_text(result))
|
|
142
|
+
|
|
143
|
+
return EXIT_COLLISIONS if result.has_collisions else EXIT_OK
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
sys.exit(main())
|