Python’s fileinput module iterates over lines from multiple files (or stdin) as a single stream, with per-line metadata and optional in-place editing. import fileinput. Basic use: for line in fileinput.input(files) — transparently opens each file in files; if files=() reads from stdin or sys.argv[1:]. Per-line metadata: fileinput.filename() → current file path string; fileinput.fileno() → file descriptor int; fileinput.lineno() → cumulative line count across all files; fileinput.filelineno() → line number within current file; fileinput.isfirstline() → True on line 1 of each file; fileinput.isstdin() → True when reading stdin. Control: fileinput.nextfile() — skip rest of current file; fileinput.close(). Open hook: fileinput.input(files, openhook=fileinput.hook_compressed) — auto-decompresses .gz/.bz2 files; fileinput.hook_encoded("utf-8") — decodes with specified charset. In-place editing: fileinput.input(files, inplace=True) — redirects stdout back to each file; print(modified_line, end="") writes the replacement. Context manager: with fileinput.input(files) as f: for line in f:. Class API: fileinput.FileInput(files, ...). Claude Code generates multi-file search tools, sed-style in-place editors, line number annotators, and grep replacement utilities.
CLAUDE.md for fileinput
## fileinput Stack
- Stdlib: import fileinput
- Read: for line in fileinput.input(files):
- print(fileinput.filename(), fileinput.filelineno(), line, end="")
- Stdin: for line in fileinput.input(): # reads sys.argv[1:] or stdin
- Inplace: with fileinput.input(files, inplace=True) as f:
- for line in f: print(line.replace("old", "new"), end="")
- Hooks: fileinput.hook_compressed # auto-decompress .gz .bz2
- fileinput.hook_encoded("utf-8")
fileinput Multi-File Line Pipeline
# app/fileinpututil.py — search, replace, annotate, compress, grep, context
from __future__ import annotations
import fileinput
import re
import shutil
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
# ─────────────────────────────────────────────────────────────────────────────
# 1. Line iterator with metadata
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class LineRecord:
filename: str
lineno: int # within file
global_n: int # cumulative across all files
text: str # with trailing newline stripped
is_first: bool # first line of file
is_stdin: bool
def __str__(self) -> str:
return f"{self.filename}:{self.lineno}: {self.text}"
def iter_lines(
files: list[str | Path] | None = None,
encoding: str = "utf-8",
compressed: bool = False,
) -> "Iterator[LineRecord]":
"""
Iterate over lines from files (or stdin) producing LineRecord objects.
If compressed=True uses hook_compressed for .gz/.bz2 transparency.
Example:
for rec in iter_lines(["a.txt", "b.txt"]):
print(rec)
"""
str_files = [str(f) for f in (files or [])]
if compressed:
hook = fileinput.hook_compressed
else:
hook = fileinput.hook_encoded(encoding)
with fileinput.input(str_files, openhook=hook) as fi:
for raw in fi:
yield LineRecord(
filename=fi.filename() or "<stdin>",
lineno=fi.filelineno(),
global_n=fi.lineno(),
text=raw.rstrip("\n\r"),
is_first=fi.isfirstline(),
is_stdin=fi.isstdin(),
)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Grep — search across multiple files
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class GrepMatch:
filename: str
lineno: int
text: str
match: re.Match
def __str__(self) -> str:
return f"{self.filename}:{self.lineno}: {self.text}"
def grep(
pattern: str | re.Pattern,
files: list[str | Path],
ignore_case: bool = False,
invert: bool = False,
encoding: str = "utf-8",
) -> list[GrepMatch]:
"""
Grep multiple files for a regex pattern.
Returns list of GrepMatch objects.
Example:
matches = grep(r"def \\w+\\(", ["a.py", "b.py"])
for m in matches:
print(m)
"""
flags = re.IGNORECASE if ignore_case else 0
if isinstance(pattern, str):
pat = re.compile(pattern, flags)
else:
pat = pattern
results = []
for rec in iter_lines(files, encoding=encoding):
m = pat.search(rec.text)
hit = m is not None
if (hit and not invert) or (not hit and invert):
results.append(GrepMatch(
filename=rec.filename,
lineno=rec.lineno,
text=rec.text,
match=m,
))
return results
def count_pattern(
pattern: str,
files: list[str | Path],
ignore_case: bool = False,
) -> dict[str, int]:
"""
Count pattern occurrences per file.
Example:
counts = count_pattern(r"TODO", ["a.py", "b.py"])
for fname, n in sorted(counts.items()):
print(f" {n:4d} {fname}")
"""
flags = re.IGNORECASE if ignore_case else 0
pat = re.compile(pattern, flags)
counts: dict[str, int] = {}
for rec in iter_lines(files):
key = rec.filename
if key not in counts:
counts[key] = 0
if pat.search(rec.text):
counts[key] += 1
return counts
# ─────────────────────────────────────────────────────────────────────────────
# 3. In-place editing
# ─────────────────────────────────────────────────────────────────────────────
def inplace_replace(
files: list[str | Path],
old: str,
new: str,
regex: bool = False,
backup: str = "",
encoding: str = "utf-8",
) -> dict[str, int]:
"""
Replace old with new in files in-place.
If regex=True, old is treated as a regular expression.
Returns {filename: count_of_replacements}.
Example:
counts = inplace_replace(["a.py"], "print ", "log.info(")
inplace_replace(["b.py"], r"\\bFOO\\b", "BAR", regex=True)
"""
str_files = [str(f) for f in files]
counts: dict[str, int] = {}
with fileinput.input(str_files, inplace=True,
backup=backup, encoding=encoding) as fi:
for line in fi:
fname = fi.filename()
if regex:
new_line, n = re.subn(old, new, line)
else:
new_line = line.replace(old, new)
n = line.count(old)
counts[fname] = counts.get(fname, 0) + n
print(new_line, end="")
return counts
def inplace_filter(
files: list[str | Path],
fn,
backup: str = "",
encoding: str = "utf-8",
) -> None:
"""
Apply fn(line_text) → str | None to each line.
If fn returns None, the line is deleted; otherwise the returned string is written.
Example:
# Remove blank lines
inplace_filter(["a.txt"], lambda line: None if not line.strip() else line)
"""
str_files = [str(f) for f in files]
with fileinput.input(str_files, inplace=True,
backup=backup, encoding=encoding) as fi:
for line in fi:
result = fn(line.rstrip("\n\r"))
if result is not None:
print(result)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Annotators
# ─────────────────────────────────────────────────────────────────────────────
def annotate_line_numbers(
files: list[str | Path],
separator: str = " ",
encoding: str = "utf-8",
) -> str:
"""
Return the contents of all files with line numbers prefixed.
Example:
print(annotate_line_numbers(["a.py"]))
"""
lines = []
for rec in iter_lines(files, encoding=encoding):
lines.append(f"{rec.lineno:4d}{separator}{rec.text}")
return "\n".join(lines)
def file_summary(files: list[str | Path], encoding: str = "utf-8") -> dict:
"""
Return a summary dict per file: {filename: {lines, chars, blank_lines}}.
Example:
for fname, info in file_summary(["a.py", "b.py"]).items():
print(fname, info)
"""
summary: dict[str, dict] = {}
for rec in iter_lines(files, encoding=encoding):
s = summary.setdefault(rec.filename, {"lines": 0, "chars": 0, "blank_lines": 0})
s["lines"] += 1
s["chars"] += len(rec.text)
if not rec.text.strip():
s["blank_lines"] += 1
return summary
def first_lines(files: list[str | Path], n: int = 10) -> dict[str, list[str]]:
"""
Return the first n lines of each file.
Example:
heads = first_lines(["a.py", "b.py"], n=5)
for fname, lines in heads.items():
print(fname, lines)
"""
result: dict[str, list[str]] = {}
fi = fileinput.input([str(f) for f in files])
try:
for raw in fi:
fname = fi.filename()
bucket = result.setdefault(fname, [])
if len(bucket) < n:
bucket.append(raw.rstrip("\n\r"))
elif fi.filelineno() > n:
fi.nextfile() # done with this file, skip to next
finally:
fi.close()
return result
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import os
print("=== fileinput demo ===")
with tempfile.TemporaryDirectory() as tmp:
# Create test files
fa = Path(tmp) / "a.py"
fb = Path(tmp) / "b.py"
fa.write_text(
"# File A\ndef hello():\n print('hello')\n\nhello()\n",
encoding="utf-8"
)
fb.write_text(
"# File B\n# TODO: implement feature\ndef foo(x):\n return x * 2\n\n",
encoding="utf-8"
)
# ── iter_lines ─────────────────────────────────────────────────────────
print("\n--- iter_lines ---")
for rec in iter_lines([fa, fb]):
marker = "▶" if rec.is_first else " "
print(f" {marker} {rec}")
# ── grep ───────────────────────────────────────────────────────────────
print("\n--- grep 'def ' ---")
for m in grep(r"def \w+", [fa, fb]):
print(f" {m}")
# ── count_pattern ─────────────────────────────────────────────────────
print("\n--- count_pattern '#' ---")
counts = count_pattern(r"^#", [fa, fb])
for fname, n in counts.items():
print(f" {fname}: {n} comment lines")
# ── annotate_line_numbers ─────────────────────────────────────────────
print("\n--- annotate_line_numbers(a.py) ---")
for line in annotate_line_numbers([fa]).splitlines()[:5]:
print(f" {line}")
# ── file_summary ──────────────────────────────────────────────────────
print("\n--- file_summary ---")
for fname, info in file_summary([fa, fb]).items():
print(f" {Path(fname).name}: {info}")
# ── first_lines ───────────────────────────────────────────────────────
print("\n--- first_lines(n=3) ---")
for fname, lines in first_lines([fa, fb], n=3).items():
print(f" {Path(fname).name}: {lines}")
# ── inplace_replace ───────────────────────────────────────────────────
print("\n--- inplace_replace 'print' → 'log.info' ---")
counts_rep = inplace_replace([fa], "print", "log.info")
print(f" replacements: {counts_rep}")
print(f" fa after: {fa.read_text()[:80]!r}")
# ── inplace_filter (remove blank lines) ───────────────────────────────
print("\n--- inplace_filter (remove blank lines from b.py) ---")
blank_before = fb.read_text().count("\n\n")
inplace_filter([fb], lambda line: None if not line.strip() else line)
blank_after = fb.read_text().count("\n\n")
print(f" blank lines before: {blank_before} after: {blank_after}")
print("\n=== done ===")
For the pathlib + open() alternative — for path in paths: for line in path.open(): provides the same multi-file line reading with explicit file management, path.name for filename, and a manual line counter variable — use plain open() loops when you know which files you’re processing and don’t need isfirstline(), nextfile(), or in-place editing; use fileinput when you want transparent stdin fallback, automatic compressed-file decompression, or in-place editing that handles file replacement safely (backup suffix, atomic write-back). For the grep CLI alternative — subprocess.run(["grep", "-r", "-n", pattern] + files, capture_output=True) delegates to the system grep with full POSIX regex support, binary file awareness, and parallel I/O — use system grep for large codebases or performance-sensitive searches; use fileinput.input() + re.search() when you need Python logic per line (e.g., multi-field extraction, conditional transformations, or accumulating state across lines). The Claude Skills 360 bundle includes fileinput skill sets covering LineRecord with iter_lines() metadata iterator, GrepMatch with grep()/count_pattern() multi-file search, inplace_replace()/inplace_filter() in-place editors, and annotate_line_numbers()/file_summary()/first_lines(). Start with the free tier to try multi-file line processing patterns and fileinput pipeline code generation.