diff --git a/README.md b/README.md index 5032d7c..389f4cd 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,18 @@ This will output the contents of every file, with each file preceded by its rela files-to-prompt path/to/directory --ignore "*.log" --ignore "temp*" ``` +- `c/--cxml`: Output in Claude XML format. + + ```bash + files-to-prompt path/to/directory --cxml + ``` + +- `-o/--output `: Write the output to a file instead of printing it to the console. + + ```bash + files-to-prompt path/to/directory -o output.txt + ``` + ### Example Suppose you have a directory structure like this: diff --git a/files_to_prompt/cli.py b/files_to_prompt/cli.py index 1d4638b..fe68ad4 100644 --- a/files_to_prompt/cli.py +++ b/files_to_prompt/cli.py @@ -25,29 +25,29 @@ def read_gitignore(path): return [] -def print_path(path, content, xml): +def print_path(writer, path, content, xml): if xml: - print_as_xml(path, content) + print_as_xml(writer, path, content) else: - print_default(path, content) + print_default(writer, path, content) -def print_default(path, content): - click.echo(path) - click.echo("---") - click.echo(content) - click.echo() - click.echo("---") +def print_default(writer, path, content): + writer(path) + writer("---") + writer(content) + writer("") + writer("---") -def print_as_xml(path, content): +def print_as_xml(writer, path, content): global global_index - click.echo(f'') - click.echo(f"{path}") - click.echo("") - click.echo(content) - click.echo("") - click.echo("") + writer(f'') + writer(f"{path}") + writer("") + writer(content) + writer("") + writer("") global_index += 1 @@ -57,12 +57,13 @@ def process_path( ignore_gitignore, gitignore_rules, ignore_patterns, + writer, claude_xml, ): if os.path.isfile(path): try: with open(path, "r") as f: - print_path(path, f.read(), claude_xml) + print_path(writer, path, f.read(), claude_xml) except UnicodeDecodeError: warning_message = f"Warning: Skipping file {path} due to UnicodeDecodeError" click.echo(click.style(warning_message, fg="red"), err=True) @@ -96,7 +97,7 @@ def process_path( file_path = os.path.join(root, file) try: with open(file_path, "r") as f: - print_path(file_path, f.read(), claude_xml) + print_path(writer, file_path, f.read(), claude_xml) except UnicodeDecodeError: warning_message = ( f"Warning: Skipping file {file_path} due to UnicodeDecodeError" @@ -123,6 +124,13 @@ def process_path( default=[], help="List of patterns to ignore", ) +@click.option( + "output_file", + "-o", + "--output", + type=click.Path(writable=True), + help="Output to a file instead of stdout", +) @click.option( "claude_xml", "-c", @@ -131,7 +139,9 @@ def process_path( help="Output in XML-ish format suitable for Claude's long context window.", ) @click.version_option() -def cli(paths, include_hidden, ignore_gitignore, ignore_patterns, claude_xml): +def cli( + paths, include_hidden, ignore_gitignore, ignore_patterns, output_file, claude_xml +): """ Takes one or more paths to files or directories and outputs every file, recursively, each one preceded with its filename like this: @@ -162,20 +172,28 @@ def cli(paths, include_hidden, ignore_gitignore, ignore_patterns, claude_xml): global global_index global_index = 1 gitignore_rules = [] + writer = click.echo + fp = None + if output_file: + fp = open(output_file, "w") + writer = lambda s: print(s, file=fp) for path in paths: if not os.path.exists(path): raise click.BadArgumentUsage(f"Path does not exist: {path}") if not ignore_gitignore: gitignore_rules.extend(read_gitignore(os.path.dirname(path))) if claude_xml and path == paths[0]: - click.echo("") + writer("") process_path( path, include_hidden, ignore_gitignore, gitignore_rules, ignore_patterns, + writer, claude_xml, ) if claude_xml: - click.echo("") + writer("") + if fp: + fp.close() diff --git a/tests/test_files_to_prompt.py b/tests/test_files_to_prompt.py index 5b20566..648e9ab 100644 --- a/tests/test_files_to_prompt.py +++ b/tests/test_files_to_prompt.py @@ -222,3 +222,35 @@ Contents of file2.txt """ assert expected.strip() == actual.strip() + + +@pytest.mark.parametrize("arg", ("-o", "--output")) +def test_output_option(tmpdir, arg): + runner = CliRunner() + with tmpdir.as_cwd(): + os.makedirs("test_dir") + with open("test_dir/file1.txt", "w") as f: + f.write("Contents of file1.txt") + with open("test_dir/file2.txt", "w") as f: + f.write("Contents of file2.txt") + output_file = "output.txt" + result = runner.invoke( + cli, ["test_dir", arg, output_file], catch_exceptions=False + ) + assert result.exit_code == 0 + assert not result.output + with open(output_file, "r") as f: + actual = f.read() + expected = """ +test_dir/file1.txt +--- +Contents of file1.txt + +--- +test_dir/file2.txt +--- +Contents of file2.txt + +--- +""" + assert expected.strip() == actual.strip()