telemetry-cli 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.
- telemetry_cli/__init__.py +8 -0
- telemetry_cli/_cli.py +18 -0
- telemetry_cli/_getopt.py +114 -0
- telemetry_cli/_msg.py +39 -0
- telemetry_cli/_pack.py +183 -0
- telemetry_cli/_perl.py +183 -0
- telemetry_cli/pick/__init__.py +1 -0
- telemetry_cli/pick/__main__.py +7 -0
- telemetry_cli/pick/cli.py +157 -0
- telemetry_cli/pick/commandline.py +219 -0
- telemetry_cli/pick/fields.py +166 -0
- telemetry_cli/pick/formats.py +191 -0
- telemetry_cli/pick/help.py +536 -0
- telemetry_cli/pick/symbols/airmoc +24 -0
- telemetry_cli/pick/symbols/aux +28 -0
- telemetry_cli/pick/symbols/corr +31 -0
- telemetry_cli/pick/symbols/corrOld +29 -0
- telemetry_cli/pick/symbols/hw +16 -0
- telemetry_cli/pick/symbols/moc +42 -0
- telemetry_cli/pick/symbols/subcom +83 -0
- telemetry_cli/pick/symbols/subcom_2002 +111 -0
- telemetry_cli/recl/__init__.py +1 -0
- telemetry_cli/recl/__main__.py +7 -0
- telemetry_cli/recl/cli.py +214 -0
- telemetry_cli/recl/corr.py +83 -0
- telemetry_cli/recs/__init__.py +1 -0
- telemetry_cli/recs/__main__.py +7 -0
- telemetry_cli/recs/cli.py +182 -0
- telemetry_cli/recs/extract.py +399 -0
- telemetry_cli/tgen/__init__.py +1 -0
- telemetry_cli/tgen/__main__.py +7 -0
- telemetry_cli/tgen/cli.py +128 -0
- telemetry_cli/tgen/cmdfile.py +68 -0
- telemetry_cli/tgen/convert.py +278 -0
- telemetry_cli/tgen/examples/arb_data.tgen +15 -0
- telemetry_cli/tgen/examples/calsweep.tgen +32 -0
- telemetry_cli/tgen/examples/cond_fill.tgen +17 -0
- telemetry_cli/tgen/examples/counter.tgen +11 -0
- telemetry_cli/tgen/examples/null_data.tgen +11 -0
- telemetry_cli/tgen/examples/random_size.tgen +20 -0
- telemetry_cli/tgen/examples/subcom.tgen +14 -0
- telemetry_cli/tgen/examples/valueV_value.tgen +20 -0
- telemetry_cli/tgen/runtime.py +390 -0
- telemetry_cli-1.0.0.dist-info/METADATA +106 -0
- telemetry_cli-1.0.0.dist-info/RECORD +48 -0
- telemetry_cli-1.0.0.dist-info/WHEEL +4 -0
- telemetry_cli-1.0.0.dist-info/entry_points.txt +5 -0
- telemetry_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""telemetry-cli: command-line tools for binary telemetry."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("telemetry-cli")
|
|
7
|
+
except PackageNotFoundError: # running from a source tree without installing
|
|
8
|
+
__version__ = "0+unknown"
|
telemetry_cli/_cli.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Helpers shared by the command-line programs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def broken_pipe(stdout) -> int:
|
|
11
|
+
"""Handle stdout closing early (``tool | head``): exit quietly, with status 0.
|
|
12
|
+
|
|
13
|
+
Python would otherwise report the error when it flushes stdout at exit.
|
|
14
|
+
"""
|
|
15
|
+
if stdout is sys.stdout.buffer:
|
|
16
|
+
with contextlib.suppress(OSError, ValueError):
|
|
17
|
+
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
|
|
18
|
+
return 0
|
telemetry_cli/_getopt.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Command-line options as Perl's Getopt::Long read them (with its default settings).
|
|
2
|
+
|
|
3
|
+
recs, recl and tgen used Getopt::Long, so their options behave like this:
|
|
4
|
+
|
|
5
|
+
- an option starts with one or two dashes: ``-mml 4``, ``--mml=4``;
|
|
6
|
+
- names are case-insensitive and can be shortened to any unique prefix
|
|
7
|
+
(``-ma`` for ``-max_reclen``);
|
|
8
|
+
- a value comes after ``=`` or as the next argument, even one starting with a
|
|
9
|
+
dash (``-e -r`` sets ``e`` to ``-r``);
|
|
10
|
+
- options and other arguments can be mixed; ``--`` ends the options, and a
|
|
11
|
+
lone ``-`` is an argument.
|
|
12
|
+
|
|
13
|
+
Specs use Getopt::Long's syntax: ``"name|alias"`` for a flag, and ``=i``,
|
|
14
|
+
``=f`` or ``=s`` for an integer, float or string value.
|
|
15
|
+
|
|
16
|
+
Unlike the Perl tools, which printed a warning and carried on, a bad option is
|
|
17
|
+
an error here.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
_INT = re.compile(r"^[-+]?\d+$")
|
|
26
|
+
_FLOAT = re.compile(r"^[-+]?(?=\d|\.\d)\d*(\.\d*)?([eE][-+]?\d+)?$")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OptionError(ValueError):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class _Spec:
|
|
35
|
+
key: str # the first name, used as the result key
|
|
36
|
+
names: list[str]
|
|
37
|
+
kind: str | None # None (a flag), "i", "f" or "s"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parse_specs(specs: list[str]) -> list[_Spec]:
|
|
41
|
+
out = []
|
|
42
|
+
for spec in specs:
|
|
43
|
+
m = re.fullmatch(r"([\w|]+)(?:=([ifs]))?", spec)
|
|
44
|
+
if not m:
|
|
45
|
+
raise ValueError(f"bad option spec {spec!r}")
|
|
46
|
+
names = m.group(1).split("|")
|
|
47
|
+
out.append(_Spec(names[0], names, m.group(2)))
|
|
48
|
+
return out
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def getoptions(argv: list[str], specs: list[str]) -> tuple[dict, list[str]]:
|
|
52
|
+
"""Parse argv. Returns (options by their first name, remaining arguments)."""
|
|
53
|
+
table = _parse_specs(specs)
|
|
54
|
+
opts: dict = {}
|
|
55
|
+
args: list[str] = []
|
|
56
|
+
i = 0
|
|
57
|
+
while i < len(argv):
|
|
58
|
+
a = argv[i]
|
|
59
|
+
i += 1
|
|
60
|
+
if a == "--":
|
|
61
|
+
args += argv[i:]
|
|
62
|
+
break
|
|
63
|
+
if not a.startswith("-") or a == "-":
|
|
64
|
+
args.append(a)
|
|
65
|
+
continue
|
|
66
|
+
body = a[2:] if a.startswith("--") else a[1:]
|
|
67
|
+
name, eq, value = body.partition("=")
|
|
68
|
+
spec, name = _find(table, name)
|
|
69
|
+
if spec.kind is None:
|
|
70
|
+
if eq:
|
|
71
|
+
raise OptionError(f"Option {name} does not take an argument")
|
|
72
|
+
opts[spec.key] = 1
|
|
73
|
+
continue
|
|
74
|
+
if not eq:
|
|
75
|
+
if i >= len(argv):
|
|
76
|
+
raise OptionError(f"Option {name} requires an argument")
|
|
77
|
+
value = argv[i]
|
|
78
|
+
i += 1
|
|
79
|
+
elif value == "":
|
|
80
|
+
raise OptionError(f"Option {name} requires an argument")
|
|
81
|
+
opts[spec.key] = _convert(spec, name, value)
|
|
82
|
+
return opts, args
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _find(table: list[_Spec], name: str) -> tuple[_Spec, str]:
|
|
86
|
+
"""The option a (possibly shortened) name refers to, and its full name."""
|
|
87
|
+
low = name.lower()
|
|
88
|
+
for s in table:
|
|
89
|
+
for n in s.names:
|
|
90
|
+
if n.lower() == low:
|
|
91
|
+
return s, n
|
|
92
|
+
matches: dict[str, tuple[_Spec, str]] = {}
|
|
93
|
+
for s in table:
|
|
94
|
+
for n in s.names:
|
|
95
|
+
if n.lower().startswith(low) and low:
|
|
96
|
+
matches.setdefault(s.key, (s, n))
|
|
97
|
+
if len(matches) == 1:
|
|
98
|
+
return next(iter(matches.values()))
|
|
99
|
+
if not matches:
|
|
100
|
+
raise OptionError(f"Unknown option: {low}")
|
|
101
|
+
names = ", ".join(sorted(n for _, n in matches.values()))
|
|
102
|
+
raise OptionError(f"Option {name} is ambiguous ({names})")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _convert(spec: _Spec, name: str, value: str):
|
|
106
|
+
if spec.kind == "i":
|
|
107
|
+
if not _INT.match(value):
|
|
108
|
+
raise OptionError(f'Value "{value}" invalid for option {name} (number expected)')
|
|
109
|
+
return int(value)
|
|
110
|
+
if spec.kind == "f":
|
|
111
|
+
if not _FLOAT.match(value):
|
|
112
|
+
raise OptionError(f'Value "{value}" invalid for option {name} (real number expected)')
|
|
113
|
+
return float(value)
|
|
114
|
+
return value
|
telemetry_cli/_msg.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Tagged diagnostic messages, replacing the Perl Util::Msg module.
|
|
2
|
+
|
|
3
|
+
Util::Msg printed each message as ``TAG text``, with ``NOTE`` as the default
|
|
4
|
+
tag, and could hide or select tags. recl's result lines (``RESULT 48``,
|
|
5
|
+
``CORR 97.5``) use this format, so it is part of recl's output.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from typing import TextIO
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Messenger:
|
|
15
|
+
"""Print ``TAG text`` lines to a stream, optionally filtering by tag."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
stream: TextIO | None = None,
|
|
20
|
+
*,
|
|
21
|
+
hide: set[str] | frozenset[str] = frozenset(),
|
|
22
|
+
default_tag: str = "NOTE",
|
|
23
|
+
):
|
|
24
|
+
self._stream = stream
|
|
25
|
+
self.hide = set(hide)
|
|
26
|
+
self.default_tag = default_tag
|
|
27
|
+
self.counts: dict[str, int] = {}
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def stream(self) -> TextIO:
|
|
31
|
+
# Look up sys.stderr at call time, so pytest's capsys sees the output.
|
|
32
|
+
return self._stream if self._stream is not None else sys.stderr
|
|
33
|
+
|
|
34
|
+
def __call__(self, text: object = "", tag: str | None = None) -> None:
|
|
35
|
+
tag = tag or self.default_tag
|
|
36
|
+
if tag in self.hide:
|
|
37
|
+
return
|
|
38
|
+
self.counts[tag] = self.counts.get(tag, 0) + 1
|
|
39
|
+
print(f"{tag} {text}", file=self.stream)
|
telemetry_cli/_pack.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""A subset of Perl's pack/unpack, built on struct.
|
|
2
|
+
|
|
3
|
+
tgen command files give their field types as Perl pack templates ('L', 'a10',
|
|
4
|
+
'C256', 'd2'), and pick's data types map onto the same letters. This module
|
|
5
|
+
reproduces Perl's behavior for the letters those tools use:
|
|
6
|
+
|
|
7
|
+
c C 8-bit signed / unsigned
|
|
8
|
+
s S 16-bit signed / unsigned
|
|
9
|
+
l L i I 32-bit signed / unsigned
|
|
10
|
+
q Q 64-bit signed / unsigned
|
|
11
|
+
n N 16 / 32-bit unsigned, big-endian
|
|
12
|
+
v V 16 / 32-bit unsigned, little-endian
|
|
13
|
+
f d single / double float
|
|
14
|
+
a A Z byte strings: null-padded, space-padded, null-terminated
|
|
15
|
+
x a null byte
|
|
16
|
+
|
|
17
|
+
Each letter can take a repeat count (``C4``) or ``*``, and the integer and
|
|
18
|
+
float letters wider than a byte take a ``<`` or ``>`` byte-order modifier. Without a modifier
|
|
19
|
+
the byte order is native, as in Perl.
|
|
20
|
+
|
|
21
|
+
As in Perl, integers are truncated toward zero and wrap to the field width, so
|
|
22
|
+
``pack('C', 257)`` is ``b'\\x01'`` and ``pack('L', -1)`` is ``b'\\xff\\xff\\xff\\xff'``.
|
|
23
|
+
Missing values pack as zero (or as empty strings for a/A/Z).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import math
|
|
29
|
+
import re
|
|
30
|
+
import struct
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
from ._perl import numify as _to_number
|
|
34
|
+
|
|
35
|
+
# letter -> (struct code, size in bytes, signed?) for the integer letters
|
|
36
|
+
_INTS = {
|
|
37
|
+
"c": ("b", 1, True),
|
|
38
|
+
"C": ("B", 1, False),
|
|
39
|
+
"s": ("h", 2, True),
|
|
40
|
+
"S": ("H", 2, False),
|
|
41
|
+
"l": ("i", 4, True),
|
|
42
|
+
"L": ("I", 4, False),
|
|
43
|
+
"i": ("i", 4, True),
|
|
44
|
+
"I": ("I", 4, False),
|
|
45
|
+
"q": ("q", 8, True),
|
|
46
|
+
"Q": ("Q", 8, False),
|
|
47
|
+
"n": ("H", 2, False),
|
|
48
|
+
"N": ("I", 4, False),
|
|
49
|
+
"v": ("H", 2, False),
|
|
50
|
+
"V": ("I", 4, False),
|
|
51
|
+
}
|
|
52
|
+
_FIXED_ORDER = {"n": ">", "N": ">", "v": "<", "V": "<"}
|
|
53
|
+
_FLOATS = {"f": "f", "d": "d"}
|
|
54
|
+
_STRINGS = "aAZ"
|
|
55
|
+
|
|
56
|
+
_ITEM = re.compile(r"\s*([cCsSlLiIqQnNvVfdaAZx])([<>])?(\*|\d+)?\s*")
|
|
57
|
+
NATIVE = "<" if sys.byteorder == "little" else ">"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PackError(ValueError):
|
|
61
|
+
"""A template this module can't handle."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse(template: str) -> list[tuple[str, str, int | None]]:
|
|
65
|
+
"""Split a template into (letter, byte order, count) items.
|
|
66
|
+
|
|
67
|
+
The count is None when omitted and -1 for ``*``.
|
|
68
|
+
"""
|
|
69
|
+
items = []
|
|
70
|
+
pos = 0
|
|
71
|
+
while pos < len(template):
|
|
72
|
+
m = _ITEM.match(template, pos)
|
|
73
|
+
if not m or m.end() == pos:
|
|
74
|
+
if template[pos:].strip() == "":
|
|
75
|
+
break
|
|
76
|
+
raise PackError(f"unsupported pack template {template!r} at {template[pos:]!r}")
|
|
77
|
+
letter, order, count = m.groups()
|
|
78
|
+
if order and (letter in _STRINGS or letter in _FIXED_ORDER or letter in "cCx"):
|
|
79
|
+
raise PackError(f"'{order}' is not allowed after '{letter}' in {template!r}")
|
|
80
|
+
order = _FIXED_ORDER.get(letter, order or NATIVE)
|
|
81
|
+
n = None if count is None else (-1 if count == "*" else int(count))
|
|
82
|
+
items.append((letter, order, n))
|
|
83
|
+
pos = m.end()
|
|
84
|
+
return items
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _to_int(v, size: int, signed: bool) -> int:
|
|
88
|
+
x = _to_number(v)
|
|
89
|
+
if isinstance(x, float):
|
|
90
|
+
x = 0 if math.isnan(x) else (int(x) if math.isfinite(x) else (-1 if x < 0 else 2**64 - 1))
|
|
91
|
+
bits = 8 * size
|
|
92
|
+
x &= (1 << bits) - 1
|
|
93
|
+
if signed and x >= 1 << (bits - 1):
|
|
94
|
+
x -= 1 << bits
|
|
95
|
+
return x
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _to_bytes(v) -> bytes:
|
|
99
|
+
if v is None:
|
|
100
|
+
return b""
|
|
101
|
+
if isinstance(v, (bytes, bytearray)):
|
|
102
|
+
return bytes(v)
|
|
103
|
+
if isinstance(v, float) and v.is_integer():
|
|
104
|
+
v = int(v)
|
|
105
|
+
return str(v).encode("latin-1")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def pack(template: str, *values) -> bytes:
|
|
109
|
+
"""Pack values like Perl's pack(template, values)."""
|
|
110
|
+
out = bytearray()
|
|
111
|
+
vals = list(values)
|
|
112
|
+
i = 0
|
|
113
|
+
|
|
114
|
+
def take():
|
|
115
|
+
nonlocal i
|
|
116
|
+
v = vals[i] if i < len(vals) else None
|
|
117
|
+
i += 1
|
|
118
|
+
return v
|
|
119
|
+
|
|
120
|
+
for letter, order, count in parse(template):
|
|
121
|
+
if letter == "x":
|
|
122
|
+
out += b"\0" * (1 if count is None else max(count, 0))
|
|
123
|
+
elif letter in _STRINGS:
|
|
124
|
+
s = _to_bytes(take())
|
|
125
|
+
if count == -1:
|
|
126
|
+
out += s + (b"\0" if letter == "Z" else b"")
|
|
127
|
+
else:
|
|
128
|
+
n = 1 if count is None else count
|
|
129
|
+
pad = b" " if letter == "A" else b"\0"
|
|
130
|
+
if letter == "Z" and n > 0:
|
|
131
|
+
s = s[: n - 1]
|
|
132
|
+
out += s[:n].ljust(n, pad)
|
|
133
|
+
else:
|
|
134
|
+
reps = len(vals) - i if count == -1 else (1 if count is None else count)
|
|
135
|
+
for _ in range(max(reps, 0)):
|
|
136
|
+
v = take()
|
|
137
|
+
if letter in _FLOATS:
|
|
138
|
+
x = float(_to_number(0 if v is None else v))
|
|
139
|
+
out += struct.pack(order + _FLOATS[letter], x)
|
|
140
|
+
else:
|
|
141
|
+
code, size, signed = _INTS[letter]
|
|
142
|
+
out += struct.pack(order + code, _to_int(0 if v is None else v, size, signed))
|
|
143
|
+
return bytes(out)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def unpack(template: str, data: bytes) -> tuple:
|
|
147
|
+
"""Unpack data like Perl's unpack(template, data).
|
|
148
|
+
|
|
149
|
+
Strings come back as bytes. Like Perl, unpacking stops quietly when the
|
|
150
|
+
data run out.
|
|
151
|
+
"""
|
|
152
|
+
out = []
|
|
153
|
+
pos = 0
|
|
154
|
+
for letter, order, count in parse(template):
|
|
155
|
+
if letter == "x":
|
|
156
|
+
pos += 1 if count is None else max(count, 0)
|
|
157
|
+
elif letter in _STRINGS:
|
|
158
|
+
n = len(data) - pos if count == -1 else (1 if count is None else count)
|
|
159
|
+
s = data[pos : pos + n]
|
|
160
|
+
pos += len(s)
|
|
161
|
+
if letter == "A":
|
|
162
|
+
s = s.rstrip(b" \0\t\n\r\f")
|
|
163
|
+
elif letter == "Z":
|
|
164
|
+
s = s.split(b"\0", 1)[0]
|
|
165
|
+
out.append(s)
|
|
166
|
+
else:
|
|
167
|
+
if letter in _FLOATS:
|
|
168
|
+
fmt, size = order + _FLOATS[letter], struct.calcsize(_FLOATS[letter])
|
|
169
|
+
else:
|
|
170
|
+
code, size, _ = _INTS[letter]
|
|
171
|
+
fmt = order + code
|
|
172
|
+
reps = (len(data) - pos) // size if count == -1 else (1 if count is None else count)
|
|
173
|
+
for _ in range(reps):
|
|
174
|
+
if pos + size > len(data):
|
|
175
|
+
break
|
|
176
|
+
out.append(struct.unpack_from(fmt, data, pos)[0])
|
|
177
|
+
pos += size
|
|
178
|
+
return tuple(out)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def calcsize(template: str) -> int:
|
|
182
|
+
"""Bytes produced by a template with fixed counts (no ``*``)."""
|
|
183
|
+
return len(pack(template))
|
telemetry_cli/_perl.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Small pieces of Perl semantics that the ported tools' output depends on.
|
|
2
|
+
|
|
3
|
+
- numify: Perl's numeric value of a string (leading number, else 0)
|
|
4
|
+
- to_iv / to_uv: Perl's conversion of a number to a signed / unsigned 64-bit
|
|
5
|
+
integer, as used by printf's %d, %u, %x and %o
|
|
6
|
+
- sprintf: printf for the conversions pick uses, including Perl's spelling of
|
|
7
|
+
infinities and NaN ("Inf", "-Inf", "NaN")
|
|
8
|
+
- substr: Perl's substr, which counts negative offsets from the end
|
|
9
|
+
- truthy: Perl's idea of a true string ("" and "0" are false)
|
|
10
|
+
- perl_str: a value as Perl prints it (floats with 15 significant digits)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
_NUMBER = re.compile(
|
|
19
|
+
r"\s*([+-]?)(?:(inf(?:inity)?|nan)|(\d+\.?\d*(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?))",
|
|
20
|
+
re.IGNORECASE,
|
|
21
|
+
)
|
|
22
|
+
IV_MIN, IV_MAX, UV_MAX = -(2**63), 2**63 - 1, 2**64 - 1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def numify(v) -> int | float:
|
|
26
|
+
"""Perl's numeric value of v. None (undef) and non-numeric strings are 0.
|
|
27
|
+
|
|
28
|
+
Handles ordinary numeric strings, "inf"/"nan", and Perl's odd reading of a
|
|
29
|
+
leading sign followed by another signed number ("--1" is 1, "- 1" is -1).
|
|
30
|
+
"""
|
|
31
|
+
if v is None:
|
|
32
|
+
return 0
|
|
33
|
+
if isinstance(v, bool):
|
|
34
|
+
return int(v)
|
|
35
|
+
if isinstance(v, (int, float)):
|
|
36
|
+
return v
|
|
37
|
+
if isinstance(v, (bytes, bytearray)):
|
|
38
|
+
v = bytes(v).decode("latin-1")
|
|
39
|
+
m = _NUMBER.match(v)
|
|
40
|
+
if m:
|
|
41
|
+
return _number(m)
|
|
42
|
+
m = re.match(r"\s*([+-])\s*", v)
|
|
43
|
+
if m:
|
|
44
|
+
inner = _NUMBER.match(v, m.end())
|
|
45
|
+
if inner and not inner.group(0)[:1].isspace():
|
|
46
|
+
x = _number(inner)
|
|
47
|
+
return -x if m.group(1) == "-" else x
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _number(m: re.Match) -> int | float:
|
|
52
|
+
sign, special, digits = m.groups()
|
|
53
|
+
if special:
|
|
54
|
+
x = math.inf if special[0] in "iI" else math.nan
|
|
55
|
+
elif any(c in digits for c in ".eE"):
|
|
56
|
+
x = float(digits)
|
|
57
|
+
else:
|
|
58
|
+
x = int(digits)
|
|
59
|
+
return -x if sign == "-" else x
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def to_iv(v) -> int:
|
|
63
|
+
"""Perl's IV (signed 64-bit) value of a finite number."""
|
|
64
|
+
x = numify(v)
|
|
65
|
+
if isinstance(x, int):
|
|
66
|
+
if x < IV_MIN:
|
|
67
|
+
return IV_MIN
|
|
68
|
+
return x if x <= IV_MAX else min(x, UV_MAX) - 2**64
|
|
69
|
+
if x < IV_MIN:
|
|
70
|
+
return IV_MIN
|
|
71
|
+
if x < 2**63:
|
|
72
|
+
return int(x)
|
|
73
|
+
uv = UV_MAX if x >= 2**64 else int(x) # Perl goes through UV here
|
|
74
|
+
return uv - 2**64
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def to_uv(v) -> int:
|
|
78
|
+
"""Perl's UV (unsigned 64-bit) value of a finite number."""
|
|
79
|
+
x = numify(v)
|
|
80
|
+
if isinstance(x, int):
|
|
81
|
+
return min(x, UV_MAX) if x >= 0 else max(x, IV_MIN) & UV_MAX
|
|
82
|
+
if x < 0:
|
|
83
|
+
return to_iv(x) & UV_MAX
|
|
84
|
+
return UV_MAX if x >= 2**64 else int(x)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def multiply(a, b) -> int | float:
|
|
88
|
+
"""Perl's a * b: integer arithmetic when both are integer-valued (so -0.0 * 180 is 0)."""
|
|
89
|
+
|
|
90
|
+
def as_int(x):
|
|
91
|
+
x = numify(x)
|
|
92
|
+
if isinstance(x, float) and x.is_integer() and IV_MIN <= x <= UV_MAX:
|
|
93
|
+
return int(x)
|
|
94
|
+
return x
|
|
95
|
+
|
|
96
|
+
a, b = as_int(a), as_int(b)
|
|
97
|
+
if isinstance(a, int) and isinstance(b, int) and IV_MIN <= a * b <= UV_MAX:
|
|
98
|
+
return a * b
|
|
99
|
+
return float(a) * float(b)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _special(x) -> str | None:
|
|
103
|
+
"""Perl's text for an infinity or NaN, else None."""
|
|
104
|
+
if isinstance(x, float) and not math.isfinite(x):
|
|
105
|
+
if math.isnan(x):
|
|
106
|
+
return "NaN"
|
|
107
|
+
return "Inf" if x > 0 else "-Inf"
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
_CONV = re.compile(r"%(-?)(0?)(\d*)(?:\.(\d+))?([dugGxosc%])")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def sprintf(fmt: str, *args) -> str:
|
|
115
|
+
"""Perl's sprintf for the flag/width/precision/conversion subset pick uses."""
|
|
116
|
+
it = iter(args)
|
|
117
|
+
|
|
118
|
+
def conv(m: re.Match) -> str:
|
|
119
|
+
left, zero, width, prec, c = m.groups()
|
|
120
|
+
if c == "%":
|
|
121
|
+
return "%"
|
|
122
|
+
v = next(it, None)
|
|
123
|
+
x = numify(v) if c != "s" else v
|
|
124
|
+
text = _special(x) if c != "s" else None
|
|
125
|
+
if text and c in "duxo" and isinstance(v, str) and not re.search("[a-zA-Z]{3}", v):
|
|
126
|
+
# Perl's integer conversions read a numeric string that overflows
|
|
127
|
+
# (like "1e400") as the largest integer, not as Inf
|
|
128
|
+
text = None
|
|
129
|
+
if text is not None:
|
|
130
|
+
w = int(width or 0)
|
|
131
|
+
return text.ljust(w) if left else text.rjust(w)
|
|
132
|
+
spec = f"%{left}{zero}{width}" + (f".{prec}" if prec is not None else "")
|
|
133
|
+
if c == "d":
|
|
134
|
+
return (spec + "d") % to_iv(x)
|
|
135
|
+
if c == "u":
|
|
136
|
+
return (spec + "d") % to_uv(x)
|
|
137
|
+
if c in "xo":
|
|
138
|
+
return (spec + c) % to_uv(x)
|
|
139
|
+
if c in "gG":
|
|
140
|
+
return (spec + c) % float(x)
|
|
141
|
+
if c == "c":
|
|
142
|
+
return (spec + "c") % chr(to_iv(x) & 0x10FFFF)
|
|
143
|
+
return (spec + "s") % ("" if v is None else v)
|
|
144
|
+
|
|
145
|
+
return _CONV.sub(conv, fmt)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def substr(data: bytes, offset: int, length: int | None = None) -> bytes:
|
|
149
|
+
"""Perl's substr(data, offset, length) for non-negative length.
|
|
150
|
+
|
|
151
|
+
A negative offset counts from the end; a start before the beginning is
|
|
152
|
+
clipped (so substr("abc", -5, 4) is "ab"). Past the end gives b"".
|
|
153
|
+
"""
|
|
154
|
+
n = len(data)
|
|
155
|
+
start = n + offset if offset < 0 else offset
|
|
156
|
+
if start > n:
|
|
157
|
+
return b""
|
|
158
|
+
end = n if length is None else start + max(length, 0)
|
|
159
|
+
return data[max(start, 0) : max(min(end, n), 0)]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def truthy(s) -> bool:
|
|
163
|
+
"""Perl truth of a scalar: undef, "", "0" and 0 are false."""
|
|
164
|
+
if s is None:
|
|
165
|
+
return False
|
|
166
|
+
if isinstance(s, str):
|
|
167
|
+
return s not in ("", "0")
|
|
168
|
+
return bool(s)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def perl_str(v) -> str:
|
|
172
|
+
"""A value as Perl turns it into text: floats with 15 significant digits."""
|
|
173
|
+
if v is None:
|
|
174
|
+
return ""
|
|
175
|
+
if isinstance(v, bool):
|
|
176
|
+
return "1" if v else ""
|
|
177
|
+
if isinstance(v, float):
|
|
178
|
+
if v.is_integer() and abs(v) < 1e15:
|
|
179
|
+
return str(int(v))
|
|
180
|
+
return sprintf("%.15g", v)
|
|
181
|
+
if isinstance(v, bytes):
|
|
182
|
+
return v.decode("latin-1")
|
|
183
|
+
return str(v)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""pick: extract and print fields from fixed-length binary records."""
|