#!/usr/bin/python3 # The MIT License (MIT) # # Copyright © 2024 pacman64 # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from multiprocessing import Pool from sys import argv, exit, stderr, stdin, stdout from typing import Dict info = ''' coby [filepaths/URIs...] COunt BYtes finds various byte-related stats for the files/URIs given. When given no named inputs, it uses standard input by default. ''' # a leading help-option arg means show the help message and quit if len(argv) == 2 and argv[1] in ('-h', '--h', '-help', '--help'): print(info.strip(), file=stderr) exit(0) def fail(msg, code: int = 1) -> None: 'Show the error message given, and quit the app right away.' print(f'\x1b[31m{msg}\x1b[0m', file=stderr) exit(code) def count_bytes(src) -> Dict[str, int]: lines = 0 crlf = 0 trails = 0 tally = [0] * 256 # counting lines with trailing spaces needs remembering the previous 2 # bytes, as the last trailing space in a line can come either before a # single line-feed byte, or a CRLF byte-pair prev2 = 0 prev1 = 0 for chunk in src: for b in chunk: tally[b] += 1 is10 = b == 10 # 10 is ord('\n') crlf += is10 and prev1 == 13 # 13 is ord('\r') trails += (is10 and prev1 == 32) or (prev1 == 13 and prev2 == 32) # notice how the last 2 bytes are remembered even across chunks prev2 = prev1 prev1 = b n = sum(tally) lines = tally[ord('\n')] if lines == 0 and n > 0: lines += 1 return { 'n': sum(tally), 'lines': lines, 'lf': tally[ord('\n')], 'crlf': crlf, 'tabs': tally[ord('\t')], 'spaces': tally[ord(' ')], 'trails': trails, 'nulls': tally[0], 'fulls': tally[255], 'highs': sum(tally[128:]), } def seems_url(s: str) -> bool: protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') return any(s.startswith(p) for p in protocols) def handle_named_input(path: str) -> Dict[str, int]: if path == '-': return count_bytes(stdin.buffer) if seems_url(path): with urlopen(path) as inp: return count_bytes(inp) with open(path, mode='rb') as inp: return count_bytes(inp) header = ''' name\tbytes\tlines\tlf\tcrlf\ttabs\tspaces\ttrails\tnulls\tfulls\thighs '''.strip() def show_results(name, counts) -> None: stdout.write(name) stdout.write('\t') stdout.write(str(counts['n'])) for k in header.split('\t')[2:]: stdout.write('\t') stdout.write(str(counts[k])) stdout.write('\n') try: args = argv[1:] if args.count('-') > 1: msg = 'reading from `-` (standard input) more than once not allowed' raise ValueError(msg) if any(seems_url(e) for e in args): from urllib.request import urlopen # given no named inputs, just use stdin if len(args) == 0: args = ['-'] # show header line right away, to reassure users something's happening stdout.write(header) stdout.write('\n') if len(args) == 1: # don't bother starting multiple interpreters for a single input results = [handle_named_input(args[0])] else: # vastly speed-up script by handling multiple inputs concurrently with Pool(processes=min(4, len(args))) as pool: results = pool.map(handle_named_input, args) for name, counts in zip(args, results): show_results(name, counts) except BrokenPipeError: # quit quietly, instead of showing a confusing error message stderr.close() except KeyboardInterrupt: exit(2) except Exception as e: fail(e, 1)