dktcli 1.0.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.
Files changed (42) hide show
  1. devkittools/__init__.py +3 -0
  2. devkittools/__main__.py +6 -0
  3. devkittools/cli.py +179 -0
  4. devkittools/tools/__init__.py +1 -0
  5. devkittools/tools/bench.py +73 -0
  6. devkittools/tools/changeloggen.py +181 -0
  7. devkittools/tools/clean.py +110 -0
  8. devkittools/tools/colortool.py +126 -0
  9. devkittools/tools/commitwriter.py +199 -0
  10. devkittools/tools/contextpacker.py +226 -0
  11. devkittools/tools/cron.py +152 -0
  12. devkittools/tools/doctor.py +74 -0
  13. devkittools/tools/du.py +72 -0
  14. devkittools/tools/enc.py +131 -0
  15. devkittools/tools/envtool.py +141 -0
  16. devkittools/tools/findtool.py +119 -0
  17. devkittools/tools/gen.py +182 -0
  18. devkittools/tools/gx.py +184 -0
  19. devkittools/tools/hashtool.py +93 -0
  20. devkittools/tools/httptool.py +131 -0
  21. devkittools/tools/ignore.py +213 -0
  22. devkittools/tools/initproj.py +166 -0
  23. devkittools/tools/jsontool.py +200 -0
  24. devkittools/tools/net.py +114 -0
  25. devkittools/tools/portkiller.py +246 -0
  26. devkittools/tools/regextool.py +84 -0
  27. devkittools/tools/repostats.py +197 -0
  28. devkittools/tools/secrets_scan.py +108 -0
  29. devkittools/tools/serve.py +118 -0
  30. devkittools/tools/snip.py +94 -0
  31. devkittools/tools/system.py +157 -0
  32. devkittools/tools/timetool.py +130 -0
  33. devkittools/tools/todo.py +69 -0
  34. devkittools/tools/tree.py +86 -0
  35. devkittools/tools/watch.py +82 -0
  36. devkittools/utils.py +561 -0
  37. dktcli-1.0.0.dist-info/METADATA +148 -0
  38. dktcli-1.0.0.dist-info/RECORD +42 -0
  39. dktcli-1.0.0.dist-info/WHEEL +5 -0
  40. dktcli-1.0.0.dist-info/entry_points.txt +3 -0
  41. dktcli-1.0.0.dist-info/licenses/LICENSE +21 -0
  42. dktcli-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,200 @@
1
+ """dkt json (js) - format, minify, query, validate, flatten and convert JSON."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import csv
6
+ import io
7
+ import json
8
+ import re
9
+ import sys
10
+
11
+ from ..utils import DktError, c, color_enabled, info, ok, output_result, read_input, sym
12
+
13
+ META = ("Data & Text", "json", "js", "pretty-print, minify, query, flatten, JSON -> CSV")
14
+
15
+ EPILOG = """\
16
+ examples:
17
+ dkt js fmt data.json pretty-print (colored in a terminal)
18
+ cat resp.json | dkt js fmt --sort
19
+ dkt js min -f data.json --copy
20
+ dkt js get users[0].name -f data.json
21
+ dkt js keys -f data.json show the shape of the document
22
+ dkt js flat -f data.json a.b[0].c = value
23
+ dkt js csv -f users.json array of objects -> CSV
24
+ dkt js fmt --paste --copy fix whatever is on your clipboard
25
+ Trailing commas and // comments are tolerated and cleaned up.
26
+ """
27
+
28
+ _TOKEN = re.compile(r'("(?:\\.|[^"\\])*")(\s*:)?|(\btrue\b|\bfalse\b)|(\bnull\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)')
29
+
30
+
31
+ def colorize(text: str) -> str:
32
+ if not color_enabled():
33
+ return text
34
+
35
+ def sub(m):
36
+ if m.group(1):
37
+ return (c(m.group(1), "bcyan") + m.group(2)) if m.group(2) else c(m.group(1), "bgreen")
38
+ if m.group(3):
39
+ return c(m.group(3), "byellow")
40
+ if m.group(4):
41
+ return c(m.group(4), "gray")
42
+ return c(m.group(5), "bmagenta")
43
+ return _TOKEN.sub(sub, text)
44
+
45
+
46
+ def _strip_lenient(text: str) -> str:
47
+ """Remove // and /* */ comments (outside strings) and trailing commas."""
48
+ out, i, n, in_str = [], 0, len(text), False
49
+ while i < n:
50
+ ch = text[i]
51
+ if in_str:
52
+ out.append(ch)
53
+ if ch == "\\" and i + 1 < n:
54
+ out.append(text[i + 1])
55
+ i += 1
56
+ elif ch == '"':
57
+ in_str = False
58
+ elif ch == '"':
59
+ in_str = True
60
+ out.append(ch)
61
+ elif text.startswith("//", i):
62
+ while i < n and text[i] != "\n":
63
+ i += 1
64
+ continue
65
+ elif text.startswith("/*", i):
66
+ end = text.find("*/", i + 2)
67
+ i = n if end == -1 else end + 2
68
+ continue
69
+ else:
70
+ out.append(ch)
71
+ i += 1
72
+ return re.sub(r",(\s*[}\]])", r"\1", "".join(out))
73
+
74
+
75
+ def loads(text: str):
76
+ try:
77
+ return json.loads(text)
78
+ except json.JSONDecodeError as first:
79
+ try:
80
+ data = json.loads(_strip_lenient(text))
81
+ info(c("note: fixed comments / trailing commas", "gray"))
82
+ return data
83
+ except json.JSONDecodeError:
84
+ lines = text.splitlines()
85
+ ctx = lines[first.lineno - 1] if 0 < first.lineno <= len(lines) else ""
86
+ raise DktError("Invalid JSON at line %d, column %d: %s\n %s\n %s^" % (
87
+ first.lineno, first.colno, first.msg, ctx.strip()[:100], " " * min(first.colno, 100)))
88
+
89
+
90
+ def get_path(data, path: str):
91
+ cur = data
92
+ for tok in re.findall(r"[^.\[\]]+|\[\d+\]", path):
93
+ try:
94
+ if tok.startswith("["):
95
+ cur = cur[int(tok[1:-1])]
96
+ elif isinstance(cur, list) and tok.isdigit():
97
+ cur = cur[int(tok)]
98
+ else:
99
+ cur = cur[tok]
100
+ except (KeyError, IndexError, TypeError):
101
+ raise DktError("Path not found at '%s'" % tok)
102
+ return cur
103
+
104
+
105
+ def flatten(data, prefix=""):
106
+ items = []
107
+ if isinstance(data, dict):
108
+ for k, v in data.items():
109
+ items += flatten(v, "%s.%s" % (prefix, k) if prefix else str(k))
110
+ elif isinstance(data, list):
111
+ for i, v in enumerate(data):
112
+ items += flatten(v, "%s[%d]" % (prefix, i))
113
+ else:
114
+ items.append((prefix, data))
115
+ return items
116
+
117
+
118
+ def shape(data, indent=0, name="$", out=None):
119
+ out = [] if out is None else out
120
+ pad = " " * indent
121
+ if isinstance(data, dict):
122
+ out.append("%s%s: object (%d keys)" % (pad, name, len(data)))
123
+ for k, v in data.items():
124
+ shape(v, indent + 1, k, out)
125
+ elif isinstance(data, list):
126
+ out.append("%s%s: array (%d items)" % (pad, name, len(data)))
127
+ if data:
128
+ shape(data[0], indent + 1, "[0]", out)
129
+ else:
130
+ out.append("%s%s: %s" % (pad, name, type(data).__name__ if data is not None else "null"))
131
+ return out
132
+
133
+
134
+ def run(args) -> int:
135
+ text = read_input(args.file_pos, args.file, args.paste, "JSON")
136
+ data = loads(text)
137
+ op = args.op
138
+ if op == "validate":
139
+ ok("valid JSON")
140
+ return 0
141
+ if op == "fmt":
142
+ out = json.dumps(data, indent=args.indent, sort_keys=args.sort, ensure_ascii=False)
143
+ output_result(out, copy=True) if args.copy else print(colorize(out))
144
+ elif op == "min":
145
+ output_result(json.dumps(data, separators=(",", ":"), sort_keys=args.sort, ensure_ascii=False), copy=args.copy)
146
+ elif op == "get":
147
+ val = get_path(data, args.path)
148
+ out = json.dumps(val, indent=2, ensure_ascii=False) if isinstance(val, (dict, list)) else (
149
+ val if isinstance(val, str) else json.dumps(val))
150
+ print(colorize(out) if isinstance(val, (dict, list)) else out)
151
+ elif op == "keys":
152
+ print("\n".join(shape(data)))
153
+ elif op == "flat":
154
+ for k, v in flatten(data):
155
+ print("%s = %s" % (c(k, "bcyan"), json.dumps(v, ensure_ascii=False)))
156
+ elif op == "csv":
157
+ if not isinstance(data, list) or not data or not all(isinstance(x, dict) for x in data):
158
+ raise DktError("CSV conversion needs a JSON array of objects.")
159
+ cols = []
160
+ for row in data:
161
+ for k in row:
162
+ if k not in cols:
163
+ cols.append(k)
164
+ buf = io.StringIO()
165
+ w = csv.writer(buf, lineterminator="\n")
166
+ w.writerow(cols)
167
+ for row in data:
168
+ w.writerow([json.dumps(row.get(k), ensure_ascii=False) if isinstance(row.get(k), (dict, list))
169
+ else ("" if row.get(k) is None else row.get(k)) for k in cols])
170
+ output_result(buf.getvalue().rstrip("\n"), copy=args.copy)
171
+ return 0
172
+
173
+
174
+ def register(sub, common) -> None:
175
+ p = sub.add_parser("json", aliases=["js"], parents=[common], help=META[3],
176
+ description="Swiss army knife for JSON.", epilog=EPILOG,
177
+ formatter_class=argparse.RawDescriptionHelpFormatter)
178
+ p.add_argument("op", choices=["fmt", "min", "get", "keys", "flat", "csv", "validate"])
179
+ p.add_argument("path", nargs="?", help="for 'get': a.b[0].c")
180
+ p.add_argument("file_pos", nargs="?", metavar="TEXT", help=argparse.SUPPRESS)
181
+ p.add_argument("-f", "--file", metavar="FILE", help="read JSON from a file")
182
+ p.add_argument("--paste", action="store_true", help="read JSON from clipboard")
183
+ p.add_argument("--copy", action="store_true", help="copy result to clipboard")
184
+ p.add_argument("--indent", type=int, default=2)
185
+ p.add_argument("--sort", action="store_true", help="sort keys")
186
+ p.set_defaults(func=_dispatch)
187
+
188
+
189
+ def _dispatch(args) -> int:
190
+ import os
191
+ if args.file_pos and not args.file and os.path.isfile(args.file_pos):
192
+ args.file, args.file_pos = args.file_pos, None
193
+ # 'fmt data.json' style: positional after op is the file for every op except 'get'.
194
+ if args.op != "get":
195
+ if args.path and not args.file:
196
+ args.file = args.path
197
+ args.path = None
198
+ elif not args.path:
199
+ raise DktError("'get' needs a path, e.g. dkt js get users[0].name -f data.json")
200
+ return run(args)
@@ -0,0 +1,114 @@
1
+ """dkt net - IP addresses, DNS lookups and TCP reachability checks."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import socket
6
+ import time
7
+ import urllib.request
8
+
9
+ from ..utils import DktError, c, fail, kv, ok, sym, table
10
+
11
+ META = ("Servers & Network", "net", "", "ip addresses, DNS lookup, is-this-port-open checks")
12
+
13
+ EPILOG = """\
14
+ examples:
15
+ dkt net ip local IPs
16
+ dkt net ip --public also your public IP (asks api.ipify.org)
17
+ dkt net dns github.com
18
+ dkt net check github.com:443 localhost:5432 192.168.1.10:22
19
+ """
20
+
21
+
22
+ def local_ips():
23
+ ips = set()
24
+ try:
25
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
26
+ s.connect(("10.255.255.255", 1))
27
+ ips.add(s.getsockname()[0])
28
+ s.close()
29
+ except Exception:
30
+ pass
31
+ try:
32
+ for info_ in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
33
+ ips.add(info_[4][0])
34
+ except Exception:
35
+ pass
36
+ return sorted(i for i in ips if not i.startswith("127."))
37
+
38
+
39
+ def cmd_ip(args) -> int:
40
+ rows = [("hostname", socket.gethostname())]
41
+ for i, ip in enumerate(local_ips()):
42
+ rows.append(("local ip" if i == 0 else "", c(ip, "bold")))
43
+ if args.public:
44
+ try:
45
+ req = urllib.request.Request("https://api.ipify.org", headers={"User-Agent": "dkt"})
46
+ rows.append(("public ip", c(urllib.request.urlopen(req, timeout=6).read().decode().strip(), "bold bcyan")))
47
+ except Exception:
48
+ rows.append(("public ip", c("unavailable (offline?)", "red")))
49
+ kv(rows)
50
+ return 0
51
+
52
+
53
+ def cmd_dns(args) -> int:
54
+ for host in args.hosts:
55
+ try:
56
+ t0 = time.perf_counter()
57
+ res = socket.getaddrinfo(host, None)
58
+ dt = (time.perf_counter() - t0) * 1000
59
+ except socket.gaierror as e:
60
+ fail("%s %s" % (host, e))
61
+ continue
62
+ addrs = []
63
+ for r in res:
64
+ a = r[4][0]
65
+ if a not in addrs:
66
+ addrs.append(a)
67
+ print("%s %s %s" % (c(sym("ok"), "bgreen"), c(host, "bold"), c("(%.0f ms)" % dt, "gray")))
68
+ for a in addrs:
69
+ print(" %s %s" % (c("IPv6" if ":" in a else "IPv4", "gray"), a))
70
+ try:
71
+ print(" %s %s" % (c("rdns", "gray"), socket.gethostbyaddr(addrs[0])[0]))
72
+ except Exception:
73
+ pass
74
+ return 0
75
+
76
+
77
+ def cmd_check(args) -> int:
78
+ rows = []
79
+ bad = 0
80
+ for target in args.targets:
81
+ host, _, port = target.rpartition(":")
82
+ if not host or not port.isdigit():
83
+ raise DktError("Use host:port, e.g. github.com:443 (got '%s')" % target)
84
+ t0 = time.perf_counter()
85
+ try:
86
+ with socket.create_connection((host, int(port)), timeout=args.timeout):
87
+ dt = (time.perf_counter() - t0) * 1000
88
+ rows.append((c(sym("ok"), "bgreen"), target, c("open", "green"), "%.0f ms" % dt))
89
+ except socket.timeout:
90
+ rows.append((c(sym("fail"), "bred"), target, c("timed out", "red"), ""))
91
+ bad += 1
92
+ except OSError as e:
93
+ rows.append((c(sym("fail"), "bred"), target, c("closed / unreachable", "red"), c(str(e.strerror or e)[:30], "gray")))
94
+ bad += 1
95
+ table(["", "TARGET", "STATUS", "LATENCY"], rows)
96
+ return 1 if bad else 0
97
+
98
+
99
+ def register(sub, common) -> None:
100
+ p = sub.add_parser("net", parents=[common], help=META[3],
101
+ description="Network helpers.", epilog=EPILOG,
102
+ formatter_class=argparse.RawDescriptionHelpFormatter)
103
+ g = p.add_subparsers(dest="sub", metavar="<ip|dns|check>")
104
+ p.set_defaults(func=lambda a: (p.print_help() or 0))
105
+ ip = g.add_parser("ip", help="show local (and public) IP addresses")
106
+ ip.add_argument("--public", action="store_true")
107
+ ip.set_defaults(func=cmd_ip)
108
+ dns = g.add_parser("dns", help="resolve host names")
109
+ dns.add_argument("hosts", nargs="+")
110
+ dns.set_defaults(func=cmd_dns)
111
+ ck = g.add_parser("check", help="test if host:port accepts TCP connections")
112
+ ck.add_argument("targets", nargs="+", metavar="HOST:PORT")
113
+ ck.add_argument("--timeout", type=float, default=3.0)
114
+ ck.set_defaults(func=cmd_check)
@@ -0,0 +1,246 @@
1
+ """dkt portkiller (pk) - see what is using a port and optionally kill it."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import csv
6
+ import os
7
+ import re
8
+ import shutil
9
+ import signal
10
+ import time
11
+ from typing import Dict, List, Optional, Tuple
12
+
13
+ from ..utils import DktError, c, info, print_table, run_capture, str2bool
14
+
15
+ PROTECTED_PIDS = {0, 4}
16
+
17
+ EPILOG = """\
18
+ examples:
19
+ dkt pk list everything that is listening
20
+ dkt pk --show 3000 who is using port 3000?
21
+ dkt pk --show 3000 --kill true ...and kill it
22
+ dkt pk --show 3000 8080 --kill several ports at once
23
+ """
24
+
25
+
26
+ # ------------------------------------------------------------ listing
27
+
28
+ def _windows_names() -> Dict[int, str]:
29
+ names = {} # type: Dict[int, str]
30
+ _, out, _ = run_capture(["tasklist", "/FO", "CSV", "/NH"])
31
+ for row in csv.reader(out.splitlines()):
32
+ if len(row) >= 2 and row[1].isdigit():
33
+ names[int(row[1])] = row[0]
34
+ return names
35
+
36
+
37
+ def _listeners_windows() -> List[dict]:
38
+ code, out, err = run_capture(["netstat", "-ano"])
39
+ if code != 0:
40
+ raise DktError("netstat failed: %s" % err.strip())
41
+ names = _windows_names()
42
+ entries = []
43
+ for line in out.splitlines():
44
+ parts = line.split()
45
+ if len(parts) < 5 or parts[0].upper() != "TCP" or parts[3].upper() != "LISTENING":
46
+ continue
47
+ local, pid_s = parts[1], parts[4]
48
+ addr, _, port_s = local.rpartition(":")
49
+ if not port_s.isdigit() or not pid_s.isdigit():
50
+ continue
51
+ pid = int(pid_s)
52
+ entries.append({"port": int(port_s), "address": addr, "pid": pid, "name": names.get(pid, "?")})
53
+ return entries
54
+
55
+
56
+ def _listeners_posix() -> List[dict]:
57
+ entries = []
58
+ if shutil.which("ss"):
59
+ _, out, _ = run_capture(["ss", "-ltnp"])
60
+ for line in out.splitlines():
61
+ parts = line.split()
62
+ if len(parts) < 4 or parts[0] == "State":
63
+ continue
64
+ local = parts[3]
65
+ addr, _, port_s = local.rpartition(":")
66
+ if not port_s.isdigit():
67
+ continue
68
+ m = re.search(r'\("([^"]*)",pid=(\d+)', line)
69
+ entries.append({"port": int(port_s), "address": addr,
70
+ "pid": int(m.group(2)) if m else None,
71
+ "name": m.group(1) if m else "? (try sudo)"})
72
+ return entries
73
+ if shutil.which("lsof"):
74
+ _, out, _ = run_capture(["lsof", "-nP", "-iTCP", "-sTCP:LISTEN"])
75
+ for line in out.splitlines()[1:]:
76
+ parts = line.split()
77
+ if len(parts) < 9:
78
+ continue
79
+ addr, _, port_s = parts[8].rpartition(":")
80
+ if not port_s.isdigit() or not parts[1].isdigit():
81
+ continue
82
+ entries.append({"port": int(port_s), "address": addr, "pid": int(parts[1]), "name": parts[0]})
83
+ return entries
84
+ if os.path.exists("/proc/net/tcp"):
85
+ return _listeners_proc()
86
+ raise DktError("Neither 'ss' nor 'lsof' is available on this system.")
87
+
88
+
89
+ def _listeners_proc() -> List[dict]:
90
+ """Fallback for minimal Linux systems: read /proc directly."""
91
+ sockets = {} # inode -> (port, address)
92
+ for fname in ("/proc/net/tcp", "/proc/net/tcp6"):
93
+ try:
94
+ with open(fname) as f:
95
+ next(f, None)
96
+ for line in f:
97
+ parts = line.split()
98
+ if len(parts) < 10 or parts[3] != "0A": # 0A = LISTEN
99
+ continue
100
+ addr_hex, port_hex = parts[1].rsplit(":", 1)
101
+ sockets[parts[9]] = (int(port_hex, 16), addr_hex)
102
+ except OSError:
103
+ continue
104
+ owners = {} # inode -> pid
105
+ for pid_s in os.listdir("/proc"):
106
+ if not pid_s.isdigit():
107
+ continue
108
+ fd_dir = "/proc/%s/fd" % pid_s
109
+ try:
110
+ for fd in os.listdir(fd_dir):
111
+ target = os.readlink("%s/%s" % (fd_dir, fd))
112
+ if target.startswith("socket:["):
113
+ inode = target[8:-1]
114
+ if inode in sockets:
115
+ owners[inode] = int(pid_s)
116
+ except OSError:
117
+ continue
118
+ entries = []
119
+ for inode, (port, addr_hex) in sockets.items():
120
+ pid = owners.get(inode)
121
+ name = "? (try sudo)"
122
+ if pid is not None:
123
+ try:
124
+ with open("/proc/%d/comm" % pid) as f:
125
+ name = f.read().strip()
126
+ except OSError:
127
+ name = "?"
128
+ address = "::" if len(addr_hex) > 8 else ("0.0.0.0" if int(addr_hex, 16) == 0 else "127.0.0.1")
129
+ entries.append({"port": port, "address": address, "pid": pid, "name": name})
130
+ return entries
131
+
132
+
133
+ def get_listeners() -> List[dict]:
134
+ raw = _listeners_windows() if os.name == "nt" else _listeners_posix()
135
+ seen = set()
136
+ unique = []
137
+ for e in sorted(raw, key=lambda e: (e["port"], e["pid"] or 0, e["address"])):
138
+ key = (e["port"], e["pid"], e["address"])
139
+ if key not in seen:
140
+ seen.add(key)
141
+ unique.append(e)
142
+ return unique
143
+
144
+
145
+ # ------------------------------------------------------------ killing
146
+
147
+ def _alive(pid: int) -> bool:
148
+ try:
149
+ os.kill(pid, 0)
150
+ except ProcessLookupError:
151
+ return False
152
+ except PermissionError:
153
+ return True
154
+ return True
155
+
156
+
157
+ def kill_pid(pid: int) -> Tuple[bool, str]:
158
+ if pid in PROTECTED_PIDS or pid == os.getpid():
159
+ return False, "refusing to kill a protected system process"
160
+ if os.name == "nt":
161
+ code, out, err = run_capture(["taskkill", "/PID", str(pid), "/F", "/T"])
162
+ if code == 0:
163
+ return True, "killed"
164
+ msg = (err or out).strip().splitlines()
165
+ text = msg[0] if msg else "taskkill failed"
166
+ if "denied" in text.lower():
167
+ text += " (try an Administrator PowerShell)"
168
+ return False, text
169
+ try:
170
+ os.kill(pid, signal.SIGTERM)
171
+ except ProcessLookupError:
172
+ return True, "already gone"
173
+ except PermissionError:
174
+ return False, "permission denied (try sudo)"
175
+ for _ in range(20):
176
+ time.sleep(0.1)
177
+ if not _alive(pid):
178
+ return True, "killed"
179
+ try:
180
+ os.kill(pid, signal.SIGKILL)
181
+ except (ProcessLookupError, PermissionError) as e:
182
+ return False, str(e)
183
+ return True, "killed (SIGKILL)"
184
+
185
+
186
+ # ------------------------------------------------------------ command
187
+
188
+ def run(args: argparse.Namespace) -> int:
189
+ kill = str2bool(args.kill)
190
+ listeners = get_listeners()
191
+
192
+ if args.show is None:
193
+ if kill:
194
+ raise DktError("--kill needs a port: dkt pk --show 3000 --kill true")
195
+ if not listeners:
196
+ print("Nothing is listening on any TCP port.")
197
+ return 0
198
+ print(c("Listening TCP ports", "bold"))
199
+ print_table(["PORT", "PID", "PROCESS", "ADDRESS"],
200
+ [(e["port"], e["pid"] if e["pid"] is not None else "-", e["name"], e["address"])
201
+ for e in listeners])
202
+ info("\nTip: dkt pk --show <port> --kill true")
203
+ return 0
204
+
205
+ for port in args.show:
206
+ if not 1 <= port <= 65535:
207
+ raise DktError("Invalid port: %d (must be 1-65535)" % port)
208
+
209
+ for port in args.show:
210
+ matches = [e for e in listeners if e["port"] == port]
211
+ if not matches:
212
+ print("Port %d: %s" % (port, c("free - nothing is listening", "green")))
213
+ continue
214
+ print("Port %d: %s" % (port, c("in use", "yellow")))
215
+ print_table(["PID", "PROCESS", "ADDRESS"],
216
+ [(e["pid"] if e["pid"] is not None else "-", e["name"], e["address"]) for e in matches])
217
+ if not kill:
218
+ info(" (add --kill true to stop it)")
219
+ continue
220
+ pids = sorted({e["pid"] for e in matches if e["pid"] is not None})
221
+ if not pids:
222
+ print(c(" Can't see the owning process - try running with sudo/Administrator.", "red"))
223
+ continue
224
+ for pid in pids:
225
+ name = next((e["name"] for e in matches if e["pid"] == pid), "?")
226
+ ok, msg = kill_pid(pid)
227
+ mark = c("✔", "green") if ok else c("✘", "red")
228
+ print(" %s PID %d (%s): %s" % (mark, pid, name, msg))
229
+ return 0
230
+
231
+
232
+ def register(sub, common) -> None:
233
+ p = sub.add_parser(
234
+ "portkiller", aliases=["pk"], parents=[common],
235
+ help="see what is using a port, optionally kill it",
236
+ description="Show which process is listening on a TCP port and optionally kill it.\n"
237
+ "With no options, lists every listening port.",
238
+ epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
239
+ p.add_argument("-s", "--show", nargs="+", type=int, metavar="PORT",
240
+ help="port(s) to inspect")
241
+ p.add_argument("-k", "--kill", nargs="?", const="true", default="false", metavar="true|false",
242
+ help="kill the process(es) using the port (use with --show)")
243
+ p.set_defaults(func=run)
244
+
245
+
246
+ META = ("Servers & Network", "portkiller", "pk", "see what's using a port, kill it")
@@ -0,0 +1,84 @@
1
+ """dkt regex (rx) - test a regular expression against text or a file with highlighted matches."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import re
6
+
7
+ from ..utils import DktError, c, color_enabled, kv, read_input, warn
8
+
9
+ META = ("Data & Text", "regex", "rx", "test regexes with highlighted matches, groups, replace")
10
+
11
+ EPILOG = """\
12
+ examples:
13
+ dkt rx "\\d{3}-\\d{4}" "call 555-1234 now"
14
+ dkt rx "(?P<user>\\w+)@(?P<host>[\\w.]+)" -f contacts.txt
15
+ dkt rx "foo" "foo Foo FOO" -i
16
+ dkt rx "(\\w+) (\\w+)" "hello world" --replace "\\2 \\1"
17
+ """
18
+
19
+
20
+ def highlight(text: str, matches) -> str:
21
+ if not color_enabled():
22
+ return text
23
+ out, last = [], 0
24
+ for m in matches:
25
+ if m.end() == m.start():
26
+ continue
27
+ out.append(text[last:m.start()])
28
+ out.append(c(text[m.start():m.end()], "reverse bgreen"))
29
+ last = m.end()
30
+ out.append(text[last:])
31
+ return "".join(out)
32
+
33
+
34
+ def run(args) -> int:
35
+ flags = 0
36
+ if args.i:
37
+ flags |= re.I
38
+ if args.m:
39
+ flags |= re.M
40
+ if args.s:
41
+ flags |= re.S
42
+ try:
43
+ rx = re.compile(args.pattern, flags)
44
+ except re.error as e:
45
+ raise DktError("Invalid regex: %s" % e)
46
+ text = read_input(args.text, args.file, False, "text")
47
+ matches = list(rx.finditer(text))
48
+ if args.replace is not None:
49
+ try:
50
+ print(rx.sub(args.replace, text))
51
+ except (re.error, IndexError) as e:
52
+ raise DktError("Bad replacement: %s" % e)
53
+ return 0
54
+ if not matches:
55
+ warn("no matches")
56
+ return 1
57
+ print(highlight(text.rstrip("\n"), matches))
58
+ print()
59
+ print(" %s match%s" % (c(len(matches), "bold bgreen"), "" if len(matches) == 1 else "es"))
60
+ for n, m in enumerate(matches[:args.limit], 1):
61
+ loc = c("@%d-%d" % (m.start(), m.end()), "gray")
62
+ print(" %s %s %s" % (c("#%d" % n, "bcyan"), repr(m.group(0)), loc))
63
+ if m.groups():
64
+ names = {v: k for k, v in rx.groupindex.items()}
65
+ for gi, val in enumerate(m.groups(), 1):
66
+ print(" %s %s" % (c("$%s" % names.get(gi, gi), "bmagenta"), repr(val)))
67
+ if len(matches) > args.limit:
68
+ print(c(" ... %d more (use --limit)" % (len(matches) - args.limit), "gray"))
69
+ return 0
70
+
71
+
72
+ def register(sub, common) -> None:
73
+ p = sub.add_parser("regex", aliases=["rx"], parents=[common], help=META[3],
74
+ description="Test a Python regular expression.", epilog=EPILOG,
75
+ formatter_class=argparse.RawDescriptionHelpFormatter)
76
+ p.add_argument("pattern")
77
+ p.add_argument("text", nargs="?", help="text to test (or -f FILE / pipe stdin)")
78
+ p.add_argument("-f", "--file", metavar="FILE")
79
+ p.add_argument("-i", action="store_true", help="ignore case")
80
+ p.add_argument("-m", action="store_true", help="multiline (^ $ per line)")
81
+ p.add_argument("-s", action="store_true", help="dot matches newline")
82
+ p.add_argument("--replace", metavar="REPL", help="show the result of re.sub instead")
83
+ p.add_argument("--limit", type=int, default=20, help="max matches to detail (default 20)")
84
+ p.set_defaults(func=run)