Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ This will output the contents of every file, with each file preceded by its rela
...
```

- `-0/--null`: Use NUL character as separator when reading paths from stdin. Useful when filenames may contain spaces.

```bash
find . -name "*.py" -print0 | files-to-prompt --null
```

### Example

Suppose you have a directory structure like this:
Expand Down Expand Up @@ -157,6 +163,28 @@ Contents of file2.txt
---
```

### Reading from stdin

The tool can also read paths from standard input. This can be used to pipe in the output of another command:

```bash
# Find files modified in the last day
find . -mtime -1 | files-to-prompt
```

When using the `--null` (or `-0`) option, paths are expected to be NUL-separated (useful when dealing with filenames containing spaces):

```bash
find . -name "*.txt" -print0 | files-to-prompt --null
```

You can mix and match paths from command line arguments and stdin:

```bash
# Include files modified in the last day, and also include README.md
find . -mtime -1 | files-to-prompt README.md
```

### Claude XML Output

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.
Expand Down
30 changes: 29 additions & 1 deletion files_to_prompt/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import sys
from fnmatch import fnmatch

import click
Expand Down Expand Up @@ -30,7 +31,7 @@ def add_line_numbers(content):

padding = len(str(len(lines)))

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


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


def read_paths_from_stdin(use_null_separator):
if sys.stdin.isatty():
# No ready input from stdin, don't block for input
return []

stdin_content = sys.stdin.read()
if use_null_separator:
paths = stdin_content.split("\0")
else:
paths = stdin_content.split() # split on whitespace
return [p for p in paths if p]


@click.command()
@click.argument("paths", nargs=-1, type=click.Path(exists=True))
@click.option("extensions", "-e", "--extension", multiple=True)
Expand Down Expand Up @@ -178,6 +192,12 @@ def process_path(
is_flag=True,
help="Add line numbers to the output",
)
@click.option(
"--null",
"-0",
is_flag=True,
help="Use NUL character as separator when reading from stdin",
)
@click.version_option()
def cli(
paths,
Expand All @@ -189,6 +209,7 @@ def cli(
output_file,
claude_xml,
line_numbers,
null,
):
"""
Takes one or more paths to files or directories and outputs every file,
Expand Down Expand Up @@ -219,6 +240,13 @@ def cli(
# Reset global_index for pytest
global global_index
global_index = 1

# Read paths from stdin if available
stdin_paths = read_paths_from_stdin(use_null_separator=null)

# Combine paths from arguments and stdin
paths = [*paths, *stdin_paths]

gitignore_rules = []
writer = click.echo
fp = None
Expand Down
53 changes: 53 additions & 0 deletions tests/test_files_to_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,56 @@ def test_line_numbers(tmpdir):
assert "2 Second line" in result.output
assert "3 Third line" in result.output
assert "4 Fourth line" in result.output


@pytest.mark.parametrize(
"input,extra_args",
(
("test_dir1/file1.txt\ntest_dir2/file2.txt", []),
("test_dir1/file1.txt\ntest_dir2/file2.txt", []),
("test_dir1/file1.txt\0test_dir2/file2.txt", ["--null"]),
("test_dir1/file1.txt\0test_dir2/file2.txt", ["-0"]),
),
)
def test_reading_paths_from_stdin(tmpdir, input, extra_args):
runner = CliRunner()
with tmpdir.as_cwd():
# Create test files
os.makedirs("test_dir1")
os.makedirs("test_dir2")
with open("test_dir1/file1.txt", "w") as f:
f.write("Contents of file1")
with open("test_dir2/file2.txt", "w") as f:
f.write("Contents of file2")

# Test space-separated paths from stdin
result = runner.invoke(cli, args=extra_args, input=input)
assert result.exit_code == 0
assert "test_dir1/file1.txt" in result.output
assert "Contents of file1" in result.output
assert "test_dir2/file2.txt" in result.output
assert "Contents of file2" in result.output


def test_paths_from_arguments_and_stdin(tmpdir):
runner = CliRunner()
with tmpdir.as_cwd():
# Create test files
os.makedirs("test_dir1")
os.makedirs("test_dir2")
with open("test_dir1/file1.txt", "w") as f:
f.write("Contents of file1")
with open("test_dir2/file2.txt", "w") as f:
f.write("Contents of file2")

# Test paths from arguments and stdin
result = runner.invoke(
cli,
args=["test_dir1"],
input="test_dir2/file2.txt",
)
assert result.exit_code == 0
assert "test_dir1/file1.txt" in result.output
assert "Contents of file1" in result.output
assert "test_dir2/file2.txt" in result.output
assert "Contents of file2" in result.output