evolver-tools 1.4.0__py3-none-any.whl
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.
- evolver_tools/__init__.py +2 -0
- evolver_tools/__main__.py +3 -0
- evolver_tools/cli.py +89 -0
- evolver_tools/vendor/b64/__init__.py +2 -0
- evolver_tools/vendor/b64/b64.py +176 -0
- evolver_tools/vendor/cal_tool/__init__.py +1 -0
- evolver_tools/vendor/cal_tool/cli.py +234 -0
- evolver_tools/vendor/chart_cli/__init__.py +444 -0
- evolver_tools/vendor/chart_cli/__main__.py +3 -0
- evolver_tools/vendor/colors/__init__.py +5 -0
- evolver_tools/vendor/colors/__main__.py +97 -0
- evolver_tools/vendor/csv_stats/__init__.py +5 -0
- evolver_tools/vendor/csv_stats/__main__.py +4 -0
- evolver_tools/vendor/csv_stats/analyzer.py +258 -0
- evolver_tools/vendor/csv_stats/cli.py +45 -0
- evolver_tools/vendor/dirsize/__init__.py +183 -0
- evolver_tools/vendor/envcheck/__init__.py +426 -0
- evolver_tools/vendor/ff/__init__.py +427 -0
- evolver_tools/vendor/ff/__main__.py +3 -0
- evolver_tools/vendor/find_dups/__init__.py +7 -0
- evolver_tools/vendor/find_dups/cli.py +392 -0
- evolver_tools/vendor/hashsum/__init__.py +211 -0
- evolver_tools/vendor/hashsum/__main__.py +5 -0
- evolver_tools/vendor/http_live/__init__.py +265 -0
- evolver_tools/vendor/http_live/__main__.py +2 -0
- evolver_tools/vendor/ipinfo/__init__.py +3 -0
- evolver_tools/vendor/ipinfo/__main__.py +30 -0
- evolver_tools/vendor/jq_lite/__init__.py +257 -0
- evolver_tools/vendor/jq_lite/__main__.py +5 -0
- evolver_tools/vendor/json2csv/__init__.py +3 -0
- evolver_tools/vendor/json2csv/__main__.py +82 -0
- evolver_tools/vendor/jsonql/__init__.py +326 -0
- evolver_tools/vendor/jsonql/__main__.py +5 -0
- evolver_tools/vendor/license_cli/__init__.py +1 -0
- evolver_tools/vendor/license_cli/__main__.py +4 -0
- evolver_tools/vendor/license_cli/cli.py +289 -0
- evolver_tools/vendor/markdown_check/__init__.py +211 -0
- evolver_tools/vendor/nb/__init__.py +319 -0
- evolver_tools/vendor/nb/__main__.py +3 -0
- evolver_tools/vendor/passgen/__init__.py +224 -0
- evolver_tools/vendor/portcheck/__init__.py +2 -0
- evolver_tools/vendor/portcheck/__main__.py +66 -0
- evolver_tools/vendor/project_doctor/__init__.py +412 -0
- evolver_tools/vendor/project_doctor/__main__.py +3 -0
- evolver_tools/vendor/ren/__init__.py +283 -0
- evolver_tools/vendor/ren/__main__.py +3 -0
- evolver_tools/vendor/siege_lite/__init__.py +250 -0
- evolver_tools/vendor/siege_lite/__main__.py +3 -0
- evolver_tools/vendor/smellfinder/__init__.py +376 -0
- evolver_tools/vendor/smellfinder/__main__.py +3 -0
- evolver_tools/vendor/sqlite_cli/__init__.py +326 -0
- evolver_tools/vendor/sqlite_cli/__main__.py +5 -0
- evolver_tools/vendor/sysmon/__init__.py +299 -0
- evolver_tools/vendor/sysmon/__main__.py +3 -0
- evolver_tools/vendor/timer/__init__.py +127 -0
- evolver_tools/vendor/treedir/__init__.py +2 -0
- evolver_tools/vendor/treedir/__main__.py +128 -0
- evolver_tools/vendor/urlparse_tool/__init__.py +3 -0
- evolver_tools/vendor/urlparse_tool/cli.py +212 -0
- evolver_tools/vendor/web_summary/__init__.py +341 -0
- evolver_tools/vendor/web_summary/__main__.py +3 -0
- evolver_tools/vendor/wordcount/__init__.py +2 -0
- evolver_tools/vendor/wordcount/__main__.py +101 -0
- evolver_tools-1.4.0.dist-info/METADATA +107 -0
- evolver_tools-1.4.0.dist-info/RECORD +69 -0
- evolver_tools-1.4.0.dist-info/WHEEL +5 -0
- evolver_tools-1.4.0.dist-info/entry_points.txt +34 -0
- evolver_tools-1.4.0.dist-info/licenses/LICENSE +21 -0
- evolver_tools-1.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""timer — 终端计时器 / Terminal countdown timer & stopwatch.
|
|
3
|
+
|
|
4
|
+
Zero-dependency CLI timer with countdown, stopwatch, and alarm modes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
import argparse
|
|
11
|
+
import itertools
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def entry():
|
|
15
|
+
args = parse_args()
|
|
16
|
+
|
|
17
|
+
if args.stopwatch:
|
|
18
|
+
run_stopwatch()
|
|
19
|
+
elif args.countdown:
|
|
20
|
+
run_countdown(parse_duration(args.countdown))
|
|
21
|
+
elif args.countdown_from:
|
|
22
|
+
run_countdown(args.countdown_from)
|
|
23
|
+
else:
|
|
24
|
+
# Default: show help
|
|
25
|
+
print("Use --stopwatch, --countdown <time>, or --timer <seconds>")
|
|
26
|
+
sys.exit(0)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def format_time(seconds: float) -> str:
|
|
30
|
+
"""Format seconds as HH:MM:SS or MM:SS."""
|
|
31
|
+
mins, secs = divmod(int(seconds), 60)
|
|
32
|
+
hours, mins = divmod(mins, 60)
|
|
33
|
+
if hours > 0:
|
|
34
|
+
return f"{hours:02d}:{mins:02d}:{secs:02d}"
|
|
35
|
+
return f"{mins:02d}:{secs:02d}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_duration(s: str) -> int:
|
|
39
|
+
"""Parse duration like '90', '1m30s', '2h' to seconds."""
|
|
40
|
+
s = s.strip().lower()
|
|
41
|
+
if s.isdigit():
|
|
42
|
+
return int(s)
|
|
43
|
+
|
|
44
|
+
total = 0
|
|
45
|
+
num = ""
|
|
46
|
+
for ch in s:
|
|
47
|
+
if ch.isdigit() or ch == ".":
|
|
48
|
+
num += ch
|
|
49
|
+
else:
|
|
50
|
+
if num:
|
|
51
|
+
val = float(num)
|
|
52
|
+
if ch == "h":
|
|
53
|
+
total += int(val * 3600)
|
|
54
|
+
elif ch == "m":
|
|
55
|
+
total += int(val * 60)
|
|
56
|
+
elif ch == "s":
|
|
57
|
+
total += int(val)
|
|
58
|
+
num = ""
|
|
59
|
+
if num:
|
|
60
|
+
total += int(num)
|
|
61
|
+
return max(total, 1)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def run_stopwatch():
|
|
65
|
+
"""Run an interactive stopwatch."""
|
|
66
|
+
print("Stopwatch started. Press Ctrl+C to stop.")
|
|
67
|
+
print()
|
|
68
|
+
start = time.time()
|
|
69
|
+
try:
|
|
70
|
+
for _ in itertools.count():
|
|
71
|
+
elapsed = time.time() - start
|
|
72
|
+
print(f"\r {format_time(elapsed)}", end="", flush=True)
|
|
73
|
+
time.sleep(0.1)
|
|
74
|
+
except KeyboardInterrupt:
|
|
75
|
+
elapsed = time.time() - start
|
|
76
|
+
print(f"\n\n Stopped at {format_time(elapsed)}")
|
|
77
|
+
print(f" ({elapsed:.1f} seconds)")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def run_countdown(seconds: int):
|
|
81
|
+
"""Run a countdown timer."""
|
|
82
|
+
print(f"Countdown: {format_time(seconds)}")
|
|
83
|
+
print()
|
|
84
|
+
try:
|
|
85
|
+
remaining = seconds
|
|
86
|
+
while remaining > 0:
|
|
87
|
+
print(f"\r {format_time(remaining)}", end="", flush=True)
|
|
88
|
+
time.sleep(1)
|
|
89
|
+
remaining -= 1
|
|
90
|
+
print(f"\r 00:00")
|
|
91
|
+
print()
|
|
92
|
+
# Alarm notification
|
|
93
|
+
bell = "\a" * 3
|
|
94
|
+
print(f"{bell} TIME'S UP! ({format_time(seconds)})")
|
|
95
|
+
# Keep beeping for a bit
|
|
96
|
+
for _ in range(5):
|
|
97
|
+
print("\a", end="", flush=True)
|
|
98
|
+
time.sleep(0.5)
|
|
99
|
+
except KeyboardInterrupt:
|
|
100
|
+
elapsed = seconds - (remaining if 'remaining' in dir() else seconds)
|
|
101
|
+
print(f"\n\n Stopped at {format_time(elapsed)}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_args():
|
|
105
|
+
parser = argparse.ArgumentParser(
|
|
106
|
+
description="timer — 终端计时器 / Terminal timer & stopwatch",
|
|
107
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
108
|
+
epilog="""Examples:
|
|
109
|
+
timer --stopwatch # Start stopwatch
|
|
110
|
+
timer --countdown 90 # Countdown 90 seconds
|
|
111
|
+
timer --countdown 5m # Countdown 5 minutes
|
|
112
|
+
timer --countdown 1h30m # Countdown 1 hour 30 minutes
|
|
113
|
+
timer -t 300 # Countdown 300 seconds
|
|
114
|
+
""")
|
|
115
|
+
parser.add_argument("-s", "--stopwatch", action="store_true",
|
|
116
|
+
help="Start stopwatch mode")
|
|
117
|
+
parser.add_argument("-c", "--countdown", type=str, nargs="?", const="60",
|
|
118
|
+
metavar="DURATION",
|
|
119
|
+
help="Countdown timer (default: 60s). Format: 90, 5m, 1h30m")
|
|
120
|
+
parser.add_argument("-t", "--timer", dest="countdown_from", type=int,
|
|
121
|
+
metavar="SECONDS",
|
|
122
|
+
help="Countdown from N seconds")
|
|
123
|
+
return parser.parse_args()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
entry()
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""treedir — 目录树可视化工具。零外部依赖(替代 tree 命令)。"""
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
PIPE = "│ "
|
|
7
|
+
BRANCH = "├── "
|
|
8
|
+
LAST = "└── "
|
|
9
|
+
|
|
10
|
+
def build_tree(root_path, max_depth=3, show_hidden=False, dirs_only=False):
|
|
11
|
+
"""递归构建目录树"""
|
|
12
|
+
root_path = os.path.abspath(root_path)
|
|
13
|
+
if not os.path.isdir(root_path):
|
|
14
|
+
print(f"❌ 不是目录: {root_path}", file=sys.stderr)
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
basename = os.path.basename(root_path) or root_path
|
|
18
|
+
print(basename)
|
|
19
|
+
|
|
20
|
+
def _walk(dirpath, prefix="", depth=0):
|
|
21
|
+
if depth >= max_depth:
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
entries = sorted(os.listdir(dirpath))
|
|
26
|
+
except PermissionError:
|
|
27
|
+
print(f"{prefix}{LAST} [权限不足]")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
if not show_hidden:
|
|
31
|
+
entries = [e for e in entries if not e.startswith(".")]
|
|
32
|
+
|
|
33
|
+
# Separate dirs and files
|
|
34
|
+
dirs = []
|
|
35
|
+
files = []
|
|
36
|
+
for e in entries:
|
|
37
|
+
full = os.path.join(dirpath, e)
|
|
38
|
+
if os.path.isdir(full):
|
|
39
|
+
dirs.append(e)
|
|
40
|
+
else:
|
|
41
|
+
files.append(e)
|
|
42
|
+
|
|
43
|
+
if dirs_only:
|
|
44
|
+
items = dirs
|
|
45
|
+
else:
|
|
46
|
+
items = dirs + files
|
|
47
|
+
|
|
48
|
+
for i, entry in enumerate(items):
|
|
49
|
+
is_last = (i == len(items) - 1)
|
|
50
|
+
connector = LAST if is_last else BRANCH
|
|
51
|
+
full = os.path.join(dirpath, entry)
|
|
52
|
+
|
|
53
|
+
if os.path.isdir(full):
|
|
54
|
+
print(f"{prefix}{connector}{entry}/")
|
|
55
|
+
next_prefix = prefix + (" " if is_last else PIPE)
|
|
56
|
+
_walk(full, next_prefix, depth + 1)
|
|
57
|
+
else:
|
|
58
|
+
# Show file size
|
|
59
|
+
try:
|
|
60
|
+
size = os.path.getsize(full)
|
|
61
|
+
size_str = format_size(size)
|
|
62
|
+
print(f"{prefix}{connector}{entry} ({size_str})")
|
|
63
|
+
except OSError:
|
|
64
|
+
print(f"{prefix}{connector}{entry}")
|
|
65
|
+
|
|
66
|
+
_walk(root_path)
|
|
67
|
+
print(f"\n{count_items(root_path, show_hidden, dirs_only)}")
|
|
68
|
+
|
|
69
|
+
def count_items(root_path, show_hidden=False, dirs_only=False):
|
|
70
|
+
"""统计目录内容"""
|
|
71
|
+
dcount = 0
|
|
72
|
+
fcount = 0
|
|
73
|
+
for root, dirs, files in os.walk(root_path):
|
|
74
|
+
if not show_hidden:
|
|
75
|
+
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
|
76
|
+
files = [f for f in files if not f.startswith(".")]
|
|
77
|
+
dcount += len(dirs)
|
|
78
|
+
if not dirs_only:
|
|
79
|
+
fcount += len(files)
|
|
80
|
+
# Only first level for dirs_only
|
|
81
|
+
if dirs_only:
|
|
82
|
+
break
|
|
83
|
+
if dirs_only:
|
|
84
|
+
return f"{dcount} 个目录"
|
|
85
|
+
return f"{dcount} 个目录, {fcount} 个文件"
|
|
86
|
+
|
|
87
|
+
def format_size(size):
|
|
88
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
89
|
+
if size < 1024:
|
|
90
|
+
return f"{size:.0f} {unit}"
|
|
91
|
+
size /= 1024
|
|
92
|
+
return f"{size:.1f} TB"
|
|
93
|
+
|
|
94
|
+
def main():
|
|
95
|
+
args = sys.argv[1:]
|
|
96
|
+
|
|
97
|
+
if "-h" in args or "--help" in args:
|
|
98
|
+
print("用法: treedir [选项] [目录]")
|
|
99
|
+
print("选项:")
|
|
100
|
+
print(" -d, --max-depth N 最大深度 (默认: 3)")
|
|
101
|
+
print(" -a, --all 显示隐藏文件")
|
|
102
|
+
print(" --dirs-only 只显示目录")
|
|
103
|
+
print(" 如果不指定目录,使用当前目录")
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
root = "."
|
|
107
|
+
max_depth = 3
|
|
108
|
+
show_hidden = False
|
|
109
|
+
dirs_only = False
|
|
110
|
+
|
|
111
|
+
i = 0
|
|
112
|
+
while i < len(args):
|
|
113
|
+
if args[i] in ("-d", "--max-depth"):
|
|
114
|
+
i += 1
|
|
115
|
+
if i < len(args):
|
|
116
|
+
max_depth = int(args[i])
|
|
117
|
+
elif args[i] in ("-a", "--all"):
|
|
118
|
+
show_hidden = True
|
|
119
|
+
elif args[i] == "--dirs-only":
|
|
120
|
+
dirs_only = True
|
|
121
|
+
else:
|
|
122
|
+
root = args[i]
|
|
123
|
+
i += 1
|
|
124
|
+
|
|
125
|
+
build_tree(root, max_depth, show_hidden, dirs_only)
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
main()
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""CLI entry point for urlparse — URL parser/debugger.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
urlparse https://user:pass@example.com:8080/path;params?a=1&b=2#frag
|
|
5
|
+
echo 'https://example.com/path?q=hello' | urlparse
|
|
6
|
+
urlparse --encode 'hello world'
|
|
7
|
+
urlparse --decode 'hello+world'
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
from urllib.parse import (
|
|
13
|
+
urlparse,
|
|
14
|
+
urlencode,
|
|
15
|
+
parse_qs,
|
|
16
|
+
parse_qsl,
|
|
17
|
+
quote,
|
|
18
|
+
unquote,
|
|
19
|
+
urlunparse,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_url(url: str) -> dict:
|
|
24
|
+
"""Parse a URL and return all components."""
|
|
25
|
+
parsed = urlparse(url)
|
|
26
|
+
result = {
|
|
27
|
+
"url": url,
|
|
28
|
+
"scheme": parsed.scheme or "",
|
|
29
|
+
"netloc": parsed.netloc or "",
|
|
30
|
+
"username": parsed.username or "",
|
|
31
|
+
"password": parsed.password or "",
|
|
32
|
+
"hostname": parsed.hostname or "",
|
|
33
|
+
"port": parsed.port,
|
|
34
|
+
"path": parsed.path or "",
|
|
35
|
+
"params": parsed.params or "",
|
|
36
|
+
"query": parsed.query or "",
|
|
37
|
+
"fragment": parsed.fragment or "",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Parse query string into key-value pairs
|
|
41
|
+
if parsed.query:
|
|
42
|
+
result["query_pairs"] = parse_qs(parsed.query, keep_blank_values=True)
|
|
43
|
+
result["query_pairs_ordered"] = parse_qsl(parsed.query, keep_blank_values=True)
|
|
44
|
+
else:
|
|
45
|
+
result["query_pairs"] = {}
|
|
46
|
+
result["query_pairs_ordered"] = []
|
|
47
|
+
|
|
48
|
+
# Reconstruct the URL (canonical form)
|
|
49
|
+
reconstructed = urlunparse(
|
|
50
|
+
(
|
|
51
|
+
parsed.scheme,
|
|
52
|
+
parsed.netloc,
|
|
53
|
+
parsed.path,
|
|
54
|
+
parsed.params,
|
|
55
|
+
parsed.query,
|
|
56
|
+
parsed.fragment,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
result["reconstructed"] = reconstructed
|
|
60
|
+
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def format_parse_result(result: dict) -> str:
|
|
65
|
+
"""Format parsed URL components into a human-readable string."""
|
|
66
|
+
lines = []
|
|
67
|
+
lines.append(f" URL: {result['url']}")
|
|
68
|
+
lines.append(f" Reconstructed: {result['reconstructed']}")
|
|
69
|
+
lines.append(" ───────────────────────────────────────────")
|
|
70
|
+
lines.append(f" Scheme: {result['scheme']}")
|
|
71
|
+
lines.append(f" Netloc: {result['netloc']}")
|
|
72
|
+
lines.append(f" Username: {result['username']}")
|
|
73
|
+
lines.append(f" Password: {'●' * max(1, len(result['password'])) if result['password'] else ''}")
|
|
74
|
+
lines.append(f" Hostname: {result['hostname']}")
|
|
75
|
+
lines.append(f" Port: {result['port'] if result['port'] is not None else ''}")
|
|
76
|
+
lines.append(f" Path: {result['path']}")
|
|
77
|
+
lines.append(f" Params: {result['params']}")
|
|
78
|
+
lines.append(f" Query: {result['query']}")
|
|
79
|
+
lines.append(f" Fragment: {result['fragment']}")
|
|
80
|
+
|
|
81
|
+
if result["query_pairs"]:
|
|
82
|
+
lines.append(" ───────────────────────────────────────────")
|
|
83
|
+
lines.append(" Query parameters:")
|
|
84
|
+
for key, values in result["query_pairs"].items():
|
|
85
|
+
for val in values:
|
|
86
|
+
lines.append(f" {key} = {val}")
|
|
87
|
+
|
|
88
|
+
return "\n".join(lines)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def format_parse_result_json(result: dict) -> str:
|
|
92
|
+
"""Format parsed URL components as JSON."""
|
|
93
|
+
import json
|
|
94
|
+
|
|
95
|
+
output = {
|
|
96
|
+
"url": result["url"],
|
|
97
|
+
"scheme": result["scheme"],
|
|
98
|
+
"netloc": result["netloc"],
|
|
99
|
+
"username": result["username"],
|
|
100
|
+
"password": result["password"],
|
|
101
|
+
"hostname": result["hostname"],
|
|
102
|
+
"port": result["port"],
|
|
103
|
+
"path": result["path"],
|
|
104
|
+
"params": result["params"],
|
|
105
|
+
"query": result["query"],
|
|
106
|
+
"fragment": result["fragment"],
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if result["query_pairs"]:
|
|
110
|
+
output["query_parameters"] = dict(result["query_pairs"])
|
|
111
|
+
|
|
112
|
+
return json.dumps(output, indent=2)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def do_encode(text: str) -> str:
|
|
116
|
+
"""URL-encode a string."""
|
|
117
|
+
return quote(text, safe="")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def do_decode(text: str) -> str:
|
|
121
|
+
"""URL-decode a string."""
|
|
122
|
+
return unquote(text, encoding="utf-8", errors="replace")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def read_stdin() -> str:
|
|
126
|
+
"""Read raw input from stdin (pipe mode)."""
|
|
127
|
+
if not sys.stdin.isatty():
|
|
128
|
+
return sys.stdin.read().strip()
|
|
129
|
+
return ""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main() -> None:
|
|
133
|
+
parser = argparse.ArgumentParser(
|
|
134
|
+
description="URL parser/debugger — parse, encode, and decode URLs.",
|
|
135
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
136
|
+
epilog=(
|
|
137
|
+
"Examples:\n"
|
|
138
|
+
" urlparse https://example.com/path?q=hello#section\n"
|
|
139
|
+
" echo 'https://example.com/search?q=hello+world' | urlparse\n"
|
|
140
|
+
" urlparse --encode 'hello world'\n"
|
|
141
|
+
" urlparse --decode 'hello+world%21'\n"
|
|
142
|
+
" urlparse --json https://example.com?foo=1&bar=2\n"
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"url",
|
|
148
|
+
nargs="?",
|
|
149
|
+
help="URL to parse (omit to read from stdin)",
|
|
150
|
+
)
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"--encode",
|
|
153
|
+
nargs="?",
|
|
154
|
+
const=True,
|
|
155
|
+
metavar="TEXT",
|
|
156
|
+
help="URL-encode the given text (or pipe input)",
|
|
157
|
+
)
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"--decode",
|
|
160
|
+
nargs="?",
|
|
161
|
+
const=True,
|
|
162
|
+
metavar="TEXT",
|
|
163
|
+
help="URL-decode the given text (or pipe input)",
|
|
164
|
+
)
|
|
165
|
+
parser.add_argument(
|
|
166
|
+
"--json",
|
|
167
|
+
action="store_true",
|
|
168
|
+
help="Output parse results as JSON",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
args = parser.parse_args()
|
|
172
|
+
|
|
173
|
+
# --- Encode mode ---
|
|
174
|
+
if args.encode is not None:
|
|
175
|
+
text = args.encode if isinstance(args.encode, str) else read_stdin()
|
|
176
|
+
if not text:
|
|
177
|
+
# If --encode was used as a flag with pipe, read_stdin already tried
|
|
178
|
+
text = read_stdin()
|
|
179
|
+
if not text:
|
|
180
|
+
print("error: --encode requires text argument or pipe input", file=sys.stderr)
|
|
181
|
+
sys.exit(1)
|
|
182
|
+
print(do_encode(text))
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# --- Decode mode ---
|
|
186
|
+
if args.decode is not None:
|
|
187
|
+
text = args.decode if isinstance(args.decode, str) else read_stdin()
|
|
188
|
+
if not text:
|
|
189
|
+
text = read_stdin()
|
|
190
|
+
if not text:
|
|
191
|
+
print("error: --decode requires text argument or pipe input", file=sys.stderr)
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
print(do_decode(text))
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
# --- Parse mode ---
|
|
197
|
+
url = args.url or read_stdin()
|
|
198
|
+
|
|
199
|
+
if not url:
|
|
200
|
+
parser.print_help()
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
result = parse_url(url)
|
|
204
|
+
|
|
205
|
+
if args.json:
|
|
206
|
+
print(format_parse_result_json(result))
|
|
207
|
+
else:
|
|
208
|
+
print(format_parse_result(result))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
main()
|