#!/usr/bin/env python3 """Textproc — text processing pipeline: count, search, transform, sort.""" import argparse, sys, re, collections, os def color(text, code): return f"\033[{code}m{text}\033[0m" if sys.stdout.isatty() else text def read_input(filepath): try: if filepath == "-": return sys.stdin.read() with open(filepath) as f: return f.read() except FileNotFoundError: print(f"Error: File '{filepath}' not found", file=sys.stderr) sys.exit(1) def cmd_count(args): text = read_input(args.file) lines = text.splitlines() words = text.split() chars = len(text) bytes_len = len(text.encode("utf-8")) if args.lines: print(len(lines)) elif args.words: print(len(words)) elif args.chars: print(chars) elif args.bytes: print(bytes_len) else: print(f" {color('Lines:', '36'):>8} {len(lines)}") print(f" {color('Words:', '36'):>8} {len(words)}") print(f" {color('Chars:', '36'):>8} {chars}") print(f" {color('Bytes:', '36'):>8} {bytes_len}") if lines: print(f" {color('Max line:', '36'):>8} {max(len(l) for l in lines)} chars") def cmd_freq(args): text = read_input(args.file) words = re.findall(r"[a-zA-Z0-9_']+", text.lower() if args.case_insensitive else text) counter = collections.Counter(words) n = args.top or 10 if n > len(counter): n = len(counter) if n == 0: print(" (no words found)") return max_count = max(counter.values()) bar_width = 20 for word, count in counter.most_common(n): bar = color("█" * int(count / max_count * bar_width), "32") print(f" {color(word, '36'):<15} {count:>5} {bar}") def cmd_search(args): text = read_input(args.file) lines = text.splitlines() try: pattern = re.compile(args.pattern, re.IGNORECASE if args.ignore_case else 0) except re.error as e: print(f"Error: Invalid regex '{args.pattern}': {e}", file=sys.stderr) sys.exit(1) found = 0 for i, line in enumerate(lines, 1): if pattern.search(line): found += 1 if args.count_only: continue prefix = color(f"{i:>{len(str(len(lines)))}}", "90") if args.color: highlighted = pattern.sub(lambda m: color(m.group(0), "31;1"), line) print(f" {prefix} {highlighted}") else: print(f" {prefix} {line}") if args.count_only: print(f" {color(f'{found} matches', '33')}") def cmd_transform(args): text = read_input(args.file) lines = text.splitlines() ops = args.operations if args.operations else ["lower"] for op in ops: if op == "upper": lines = [l.upper() for l in lines] elif op == "lower": lines = [l.lower() for l in lines] elif op == "capitalize": lines = [l.capitalize() for l in lines] elif op == "title": lines = [l.title() for l in lines] elif op == "reverse": lines = [l[::-1] for l in lines] elif op == "strip": lines = [l.strip() for l in lines] elif op == "sort": lines.sort() elif op == "sort-reverse": lines.sort(reverse=True) elif op == "unique": seen = set() uniq = [] for l in lines: if l not in seen: seen.add(l) uniq.append(l) lines = uniq elif op == "shuffle": import random random.shuffle(lines) elif op.startswith("tr:"): parts = op[3:].split("/") if len(parts) == 2: old, new = parts lines = [l.replace(old, new) for l in lines] else: print(f"Warning: Unknown operation '{op}'", file=sys.stderr) output = "\n".join(lines) if not output.endswith("\n"): output += "\n" if args.output: with open(args.output, "w") as f: f.write(output) print(f" {color('✓', '32')} Written to {args.output}") else: sys.stdout.write(output) def main(): parser = argparse.ArgumentParser( description="Textproc — text processing pipeline", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Examples:\n" " textproc count file.txt # Word/line/char counts\n" " textproc freq file.txt # Word frequency\n" " textproc search 'error' log.txt # Search with regex\n" " textproc transform file.txt upper # Uppercase\n" " textproc transform file.txt sort unique # Sort + unique\n" " textproc transform file.txt tr:e/a # Replace e with a\n" ), ) parser.add_argument("--version", action="version", version="textproc 1.0.0") sub = parser.add_subparsers(dest="command", required=True) c = sub.add_parser("count", help="Count lines, words, chars") c.add_argument("file", nargs="?", default="-", help="File to analyze (default: stdin)") c.add_argument("-l", "--lines", action="store_true", help="Show only line count") c.add_argument("-w", "--words", action="store_true", help="Show only word count") c.add_argument("-m", "--chars", action="store_true", help="Show only character count") c.add_argument("-b", "--bytes", action="store_true", help="Show only byte count") f = sub.add_parser("freq", help="Word frequency analysis") f.add_argument("file", nargs="?", default="-", help="File to analyze (default: stdin)") f.add_argument("-n", "--top", type=int, help="Show top N words (default: 10)") f.add_argument("-i", "--case-insensitive", action="store_true", help="Case-insensitive counting") s = sub.add_parser("search", help="Search with regex") s.add_argument("pattern", help="Regex pattern") s.add_argument("file", nargs="?", default="-", help="File to search (default: stdin)") s.add_argument("-i", "--ignore-case", action="store_true", help="Case-insensitive search") s.add_argument("-c", "--count-only", action="store_true", help="Show match count only") s.add_argument("--no-color", action="store_true", help="Disable match highlighting") t = sub.add_parser("transform", help="Transform text") t.add_argument("file", nargs="?", default="-", help="File to transform (default: stdin)") t.add_argument("operations", nargs="*", choices=["upper", "lower", "capitalize", "title", "reverse", "strip", "sort", "sort-reverse", "unique", "shuffle"], help="Operations to apply in order") t.add_argument("-o", "--output", help="Output file") # Also allow custom tr:old/new t.add_argument("--tr", help="tr:old/new syntax", metavar="OLD/NEW") args = parser.parse_args() if args.command == "count": cmd_count(args) elif args.command == "freq": cmd_freq(args) elif args.command == "search": # Set color args.color = sys.stdout.isatty() and not args.no_color cmd_search(args) elif args.command == "transform": if args.tr: args.operations = list(args.operations) + [f"tr:{args.tr}"] cmd_transform(args) if __name__ == "__main__": main()