Skip to content

Commit 630bbf9

Browse files
thelastnodesimonw
andauthored
Add support for reading paths from stdin (#44)
* Add support for reading paths from stdin Fixes #43 * Refactor tests a bit --------- Co-authored-by: Simon Willison <[email protected]>
1 parent fdca384 commit 630bbf9

File tree

3 files changed

+110
-1
lines changed

3 files changed

+110
-1
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ This will output the contents of every file, with each file preceded by its rela
8686
...
8787
```
8888

89+
- `-0/--null`: Use NUL character as separator when reading paths from stdin. Useful when filenames may contain spaces.
90+
91+
```bash
92+
find . -name "*.py" -print0 | files-to-prompt --null
93+
```
94+
8995
### Example
9096

9197
Suppose you have a directory structure like this:
@@ -157,6 +163,28 @@ Contents of file2.txt
157163
---
158164
```
159165

166+
### Reading from stdin
167+
168+
The tool can also read paths from standard input. This can be used to pipe in the output of another command:
169+
170+
```bash
171+
# Find files modified in the last day
172+
find . -mtime -1 | files-to-prompt
173+
```
174+
175+
When using the `--null` (or `-0`) option, paths are expected to be NUL-separated (useful when dealing with filenames containing spaces):
176+
177+
```bash
178+
find . -name "*.txt" -print0 | files-to-prompt --null
179+
```
180+
181+
You can mix and match paths from command line arguments and stdin:
182+
183+
```bash
184+
# Include files modified in the last day, and also include README.md
185+
find . -mtime -1 | files-to-prompt README.md
186+
```
187+
160188
### Claude XML Output
161189

162190
Anthropic has provided [specific guidelines](https://docs.anthropic.com/claude/docs/long-context-window-tips) for optimally structuring prompts to take advantage of Claude's extended context window.

files_to_prompt/cli.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import sys
23
from fnmatch import fnmatch
34

45
import click
@@ -30,7 +31,7 @@ def add_line_numbers(content):
3031

3132
padding = len(str(len(lines)))
3233

33-
numbered_lines = [f"{i+1:{padding}} {line}" for i, line in enumerate(lines)]
34+
numbered_lines = [f"{i + 1:{padding}} {line}" for i, line in enumerate(lines)]
3435
return "\n".join(numbered_lines)
3536

3637

@@ -132,6 +133,19 @@ def process_path(
132133
click.echo(click.style(warning_message, fg="red"), err=True)
133134

134135

136+
def read_paths_from_stdin(use_null_separator):
137+
if sys.stdin.isatty():
138+
# No ready input from stdin, don't block for input
139+
return []
140+
141+
stdin_content = sys.stdin.read()
142+
if use_null_separator:
143+
paths = stdin_content.split("\0")
144+
else:
145+
paths = stdin_content.split() # split on whitespace
146+
return [p for p in paths if p]
147+
148+
135149
@click.command()
136150
@click.argument("paths", nargs=-1, type=click.Path(exists=True))
137151
@click.option("extensions", "-e", "--extension", multiple=True)
@@ -178,6 +192,12 @@ def process_path(
178192
is_flag=True,
179193
help="Add line numbers to the output",
180194
)
195+
@click.option(
196+
"--null",
197+
"-0",
198+
is_flag=True,
199+
help="Use NUL character as separator when reading from stdin",
200+
)
181201
@click.version_option()
182202
def cli(
183203
paths,
@@ -189,6 +209,7 @@ def cli(
189209
output_file,
190210
claude_xml,
191211
line_numbers,
212+
null,
192213
):
193214
"""
194215
Takes one or more paths to files or directories and outputs every file,
@@ -219,6 +240,13 @@ def cli(
219240
# Reset global_index for pytest
220241
global global_index
221242
global_index = 1
243+
244+
# Read paths from stdin if available
245+
stdin_paths = read_paths_from_stdin(use_null_separator=null)
246+
247+
# Combine paths from arguments and stdin
248+
paths = [*paths, *stdin_paths]
249+
222250
gitignore_rules = []
223251
writer = click.echo
224252
fp = None

tests/test_files_to_prompt.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,56 @@ def test_line_numbers(tmpdir):
322322
assert "2 Second line" in result.output
323323
assert "3 Third line" in result.output
324324
assert "4 Fourth line" in result.output
325+
326+
327+
@pytest.mark.parametrize(
328+
"input,extra_args",
329+
(
330+
("test_dir1/file1.txt\ntest_dir2/file2.txt", []),
331+
("test_dir1/file1.txt\ntest_dir2/file2.txt", []),
332+
("test_dir1/file1.txt\0test_dir2/file2.txt", ["--null"]),
333+
("test_dir1/file1.txt\0test_dir2/file2.txt", ["-0"]),
334+
),
335+
)
336+
def test_reading_paths_from_stdin(tmpdir, input, extra_args):
337+
runner = CliRunner()
338+
with tmpdir.as_cwd():
339+
# Create test files
340+
os.makedirs("test_dir1")
341+
os.makedirs("test_dir2")
342+
with open("test_dir1/file1.txt", "w") as f:
343+
f.write("Contents of file1")
344+
with open("test_dir2/file2.txt", "w") as f:
345+
f.write("Contents of file2")
346+
347+
# Test space-separated paths from stdin
348+
result = runner.invoke(cli, args=extra_args, input=input)
349+
assert result.exit_code == 0
350+
assert "test_dir1/file1.txt" in result.output
351+
assert "Contents of file1" in result.output
352+
assert "test_dir2/file2.txt" in result.output
353+
assert "Contents of file2" in result.output
354+
355+
356+
def test_paths_from_arguments_and_stdin(tmpdir):
357+
runner = CliRunner()
358+
with tmpdir.as_cwd():
359+
# Create test files
360+
os.makedirs("test_dir1")
361+
os.makedirs("test_dir2")
362+
with open("test_dir1/file1.txt", "w") as f:
363+
f.write("Contents of file1")
364+
with open("test_dir2/file2.txt", "w") as f:
365+
f.write("Contents of file2")
366+
367+
# Test paths from arguments and stdin
368+
result = runner.invoke(
369+
cli,
370+
args=["test_dir1"],
371+
input="test_dir2/file2.txt",
372+
)
373+
assert result.exit_code == 0
374+
assert "test_dir1/file1.txt" in result.output
375+
assert "Contents of file1" in result.output
376+
assert "test_dir2/file2.txt" in result.output
377+
assert "Contents of file2" in result.output

0 commit comments

Comments
 (0)