124 lines
3.8 KiB
Python
Executable File
124 lines
3.8 KiB
Python
Executable File
#!/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))
|