#!/usr/bin/env python3 # ollie:parallel read # ollie:tier cold # args_json: {"type":"object","required":["path"],"properties":{"path":{"type":"string","description":"Absolute path to file"},"start":{"type":"string","description":"1-indexed start line (default: 1)"},"end":{"type":"string","description":"1-indexed end line, inclusive (max 2000 line segment)"},"head":{"type":"string","description":"Read first N lines (alternative to start/end)"}}} # ollie:prompt # ## file_read # # Read a file with line numbers. Prefer segments over full reads. # # **Args**: `path` (required), `start`, `end`, or `head` # # - `start`/`end`: 1-indexed line numbers, inclusive. Must be strings. Max segment: 2000 lines. # - No start/end: reads up to 500 lines from line 1. # - `head`: read first N lines (alternative to start/end). # # ``` # file_read(path="/abs/path") # file_read(path="/abs/path", start="100", end="200") # file_read(path="/abs/path", head="50") # ``` # # - `start`/`end` are strings even though they represent numbers. # - Use `head` as alternative to `start`/`end`. # # **Constraints**: Absolute paths only. # ollie:end import sys import os from itertools import islice sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args MAX_LINES = 500 MAX_SEGMENT = 2000 MAX_LINE_LEN = 2000 args = parse_args() file_path = args.require("path") head = args.get_int("head") start = args.get_int("start", 1) end_arg = args.get_int("end") if not os.path.isabs(file_path): print("STATUS=error") print(f"error: path must be absolute, got: {file_path}") sys.exit(1) if not os.path.exists(file_path): print("STATUS=not_found") print("(new file)") sys.exit(0) if not os.path.isfile(file_path): print("STATUS=error") print(f"error: not a regular file: {file_path}") sys.exit(1) with open(file_path, 'rb') as f: raw_head = f.read(8192) if b'\x00' in raw_head: IMAGE_SIGS = [ b'\x89PNG\r\n\x1a\n', b'\xff\xd8\xff', b'GIF87a', b'GIF89a', b'RIFF', ] is_image = any(raw_head.startswith(s) for s in IMAGE_SIGS) if is_image: print("STATUS=ok BINARY=true IMAGE=true") print("(image file — use image_read for visual content)") else: print("STATUS=ok BINARY=true") print("(binary file)") sys.exit(0) # Count total lines try: with open(file_path, 'r', encoding='utf-8') as f: total_lines = sum(1 for _ in f) except UnicodeDecodeError: with open(file_path, 'r', encoding='utf-8', errors='replace') as f: total_lines = sum(1 for _ in f) start = max(1, start) if head: end = start + head - 1 elif end_arg is None: end = start + MAX_LINES - 1 else: end = min(end_arg, start + MAX_SEGMENT - 1) try: with open(file_path, 'r', encoding='utf-8') as f: for _ in islice(f, start - 1): pass segment_lines = list(islice(f, end - start + 1)) except UnicodeDecodeError: with open(file_path, 'r', encoding='utf-8', errors='replace') as f: for _ in islice(f, start - 1): pass segment_lines = list(islice(f, end - start + 1)) if not segment_lines: if start == 1: print(f"STATUS=ok LINES=0 TOTAL_LINES={total_lines} TRUNCATED=false EMPTY=true") print("(empty file)") else: print(f"STATUS=ok LINES=0 TOTAL_LINES={total_lines} TRUNCATED=false EMPTY=true") print("(no lines in range)") sys.exit(0) out = [] for i, line in enumerate(segment_lines): lineno = start + i text = line.rstrip('\n') if len(text) > MAX_LINE_LEN: text = text[:MAX_LINE_LEN] + '...' out.append(f"{lineno:>6}\t{text}") last_line_shown = start + len(out) - 1 truncated = last_line_shown < total_lines print(f"STATUS=ok LINES={len(out)} TOTAL_LINES={total_lines} TRUNCATED={str(truncated).lower()}") print('\n'.join(out))