Skip to content

Commit 7875893

Browse files
committed
Add wc
For counting
1 parent 7644af5 commit 7875893

1 file changed

Lines changed: 242 additions & 0 deletions

File tree

‎userland/utilities/wc.py‎

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import itertools
2+
import sys
3+
from collections.abc import Callable, Iterable, Sequence
4+
from enum import Enum, auto
5+
from typing import Self, TextIO
6+
7+
from .. import core
8+
9+
10+
def wc_count_bytes(stream: Iterable[str]) -> int:
11+
return sum(len(line.encode()) for line in stream)
12+
13+
14+
def wc_count_bytes_fast(stream: Iterable[bytes]) -> int:
15+
return sum(len(line) for line in stream)
16+
17+
18+
def wc_count_chars(stream: Iterable[str]) -> int:
19+
return sum(len(line) for line in stream)
20+
21+
22+
def wc_count_words(stream: Iterable[str]) -> int:
23+
return sum(len(line.split()) for line in stream)
24+
25+
26+
def wc_count_lines(stream: Iterable[bytes]) -> int:
27+
return sum(1 for line in stream if line.endswith("\n"))
28+
29+
30+
def wc_get_max_line_len(stream: Iterable[bytes]) -> int:
31+
return max(map(len, stream))
32+
33+
34+
wc_enabled_funcs: dict[Callable[[Iterable[bytes]], int], bool] = dict(
35+
(func, False)
36+
for func in (
37+
wc_count_lines,
38+
wc_count_words,
39+
wc_count_chars,
40+
wc_count_bytes,
41+
wc_get_max_line_len,
42+
)
43+
)
44+
45+
46+
class TotalMode(Enum):
47+
AUTO = auto()
48+
ALWAYS = auto()
49+
ONLY = auto()
50+
NEVER = auto()
51+
52+
@classmethod
53+
def from_string(cls, string: str | None) -> Self:
54+
match string:
55+
case None | "auto":
56+
return cls.AUTO
57+
case "always":
58+
return cls.ALWAYS
59+
case "only":
60+
return cls.ONLY
61+
case "never":
62+
return cls.NEVER
63+
64+
raise ValueError(f"invalid value '{string}' for --total")
65+
66+
67+
def format_counts(counts: Sequence[int]) -> str:
68+
return " ".join(map(str, counts))
69+
70+
71+
def display_lines(
72+
lines: Iterable[tuple[str, Sequence[int]] | tuple[None, None]],
73+
total_mode: TotalMode,
74+
n_counts: int,
75+
) -> bool:
76+
n_lines = 0
77+
total = [0] * n_counts
78+
79+
failed = False
80+
81+
for filename, counts in lines:
82+
n_lines += 1
83+
84+
if filename is None:
85+
failed = True
86+
continue
87+
88+
if total_mode != TotalMode.ONLY:
89+
print(
90+
f"{format_counts(counts)}"
91+
if filename is None
92+
else f"{format_counts(counts)} {filename}"
93+
)
94+
95+
for i, count in enumerate(counts):
96+
total[i] += count
97+
98+
if total_mode == TotalMode.ONLY:
99+
print(format_counts(total))
100+
elif total_mode == TotalMode.ALWAYS or (
101+
total_mode == TotalMode.AUTO and n_lines > 1
102+
):
103+
print(f"{format_counts(total)} total")
104+
105+
return failed
106+
107+
108+
def count_file(filename: str | None) -> tuple[str, Sequence[int]] | tuple[None, None]:
109+
if filename == "":
110+
core.perror("empty filename")
111+
return None, None
112+
113+
bytes_only = (
114+
wc_count_bytes in wc_enabled_funcs
115+
and sum(1 for enabled in wc_enabled_funcs.values() if enabled) < 2
116+
)
117+
118+
if filename is None or filename == "-":
119+
return filename, (
120+
(wc_count_bytes_fast(sys.stdin.buffer),)
121+
if bytes_only
122+
else wc_apply_funcs(sys.stdin)
123+
)
124+
125+
try:
126+
if bytes_only:
127+
with open(filename, "rb") as f:
128+
return filename, (wc_count_bytes_fast(f),)
129+
else:
130+
with open(filename, "r", encoding="utf-8") as f:
131+
return filename, wc_apply_funcs(f)
132+
except OSError as e:
133+
core.perror(e)
134+
return None, None
135+
136+
137+
def wc_apply_funcs(stream: Iterable[str]) -> Sequence[int]:
138+
streams = itertools.tee(stream, len(wc_enabled_funcs))
139+
counts = []
140+
141+
for i, (wc_func, enabled) in enumerate(wc_enabled_funcs.items()):
142+
if enabled:
143+
counts.append(wc_func(streams[i]))
144+
145+
return counts
146+
147+
148+
def wc_get_enabler(func) -> Callable:
149+
def callback(*_):
150+
wc_enabled_funcs[func] = True
151+
152+
return callback
153+
154+
155+
def get_filenames(source_file: TextIO) -> Iterable[str]:
156+
# Note that coreutils wc reads all the names from
157+
# the source file before starting to count anything.
158+
filenames = source_file.read().split("\0")
159+
if not filenames[-1]:
160+
filenames = filenames[:-1]
161+
162+
return filenames
163+
164+
165+
parser = core.ExtendedOptionParser(
166+
usage="%prog [FILE]...", description="Print newline, word and byte counts."
167+
)
168+
169+
parser.add_option(
170+
"-l",
171+
"--lines",
172+
action="callback",
173+
callback=wc_get_enabler(wc_count_lines),
174+
help="print line counts",
175+
)
176+
parser.add_option(
177+
"-w",
178+
"--words",
179+
action="callback",
180+
callback=wc_get_enabler(wc_count_words),
181+
help="print word counts",
182+
)
183+
parser.add_option(
184+
"-m",
185+
"--chars",
186+
action="callback",
187+
callback=wc_get_enabler(wc_count_chars),
188+
help="print character counts",
189+
)
190+
parser.add_option(
191+
"-c",
192+
"--bytes",
193+
action="callback",
194+
callback=wc_get_enabler(wc_count_bytes),
195+
help="print byte counts",
196+
)
197+
parser.add_option(
198+
"-L",
199+
"--max-line-length",
200+
action="callback",
201+
callback=wc_get_enabler(wc_get_max_line_len),
202+
help="print length of longest line",
203+
)
204+
parser.add_option(
205+
"--total",
206+
action="store",
207+
dest="total",
208+
metavar="WHEN",
209+
help="set whether to print total counts",
210+
)
211+
parser.add_option(
212+
"--files0-from", action="store", dest="filename_source", metavar="FILE", help="read"
213+
)
214+
215+
216+
@core.command(parser)
217+
def python_userland_wc(opts, args: list[str]) -> int:
218+
if not any(wc_enabled_funcs.values()):
219+
wc_enabled_funcs[wc_count_bytes] = True
220+
wc_enabled_funcs[wc_count_words] = True
221+
wc_enabled_funcs[wc_count_lines] = True
222+
223+
n_cols = sum(1 for enabled in wc_enabled_funcs.values() if enabled)
224+
225+
try:
226+
total_mode = TotalMode.from_string(opts.total)
227+
except ValueError as e:
228+
core.perror(e)
229+
return 1
230+
231+
if opts.filename_source is not None:
232+
parser.expect_nargs(args, 0)
233+
234+
if opts.filename_source == "-":
235+
args = get_filenames(sys.stdin)
236+
else:
237+
with open(opts.filename_source, "r", encoding="utf-8") as f:
238+
args = get_filenames(f)
239+
elif not args:
240+
args = [None]
241+
242+
return int(display_lines(map(count_file, args), total_mode, n_cols))

0 commit comments

Comments
 (0)