diff --git a/files_to_prompt/cli.py b/files_to_prompt/cli.py index 7eee04f..c69e647 100644 --- a/files_to_prompt/cli.py +++ b/files_to_prompt/cli.py @@ -1,4 +1,6 @@ import os +import shlex +import subprocess import sys from fnmatch import fnmatch @@ -98,6 +100,46 @@ def print_as_markdown(writer, path, content, line_numbers): writer(f"{backticks}") +def get_file_content(file_path, execute_command=None): + """ + Get content for a file, either by reading it directly or by executing a command. + + Args: + file_path: Path to the file + execute_command: Optional command to execute on the file + + Returns: + Content of the file or output of the command + """ + if not execute_command: + # Default behavior - just read the file + with open(file_path, "r") as f: + return f.read() + else: + # Execute command with file path + cmd = f"{execute_command} {shlex.quote(file_path)}" + try: + result = subprocess.run( + cmd, + shell=True, + check=False, # Don't raise exception on non-zero exit code + capture_output=True, + text=True, + env=os.environ, + ) + if result.returncode != 0: + warning_message = f"Warning: Command '{cmd}' failed with exit code {result.returncode}" + if result.stderr: + warning_message += f"\nError: {result.stderr}" + click.echo(click.style(warning_message, fg="red"), err=True) + return f"Error executing command (exit code {result.returncode})" + return result.stdout + except Exception as e: + warning_message = f"Warning: Error executing command '{cmd}': {str(e)}" + click.echo(click.style(warning_message, fg="red"), err=True) + return f"Error executing command: {str(e)}" + + def process_path( path, extensions, @@ -110,11 +152,12 @@ def process_path( claude_xml, markdown, line_numbers=False, + execute_command=None, ): if os.path.isfile(path): try: - with open(path, "r") as f: - print_path(writer, path, f.read(), claude_xml, markdown, line_numbers) + content = get_file_content(path, execute_command) + print_path(writer, path, content, claude_xml, markdown, line_numbers) except UnicodeDecodeError: warning_message = f"Warning: Skipping file {path} due to UnicodeDecodeError" click.echo(click.style(warning_message, fg="red"), err=True) @@ -156,15 +199,15 @@ def process_path( for file in sorted(files): file_path = os.path.join(root, file) try: - with open(file_path, "r") as f: - print_path( - writer, - file_path, - f.read(), - claude_xml, - markdown, - line_numbers, - ) + content = get_file_content(file_path, execute_command) + print_path( + writer, + file_path, + content, + claude_xml, + markdown, + line_numbers, + ) except UnicodeDecodeError: warning_message = ( f"Warning: Skipping file {file_path} due to UnicodeDecodeError" @@ -244,6 +287,12 @@ def read_paths_from_stdin(use_null_separator): is_flag=True, help="Use NUL character as separator when reading from stdin", ) +@click.option( + "execute_command", + "-x", + "--execute", + help="Execute this command for each file and use the output as content", +) @click.version_option() def cli( paths, @@ -257,6 +306,7 @@ def cli( markdown, line_numbers, null, + execute_command, ): """ Takes one or more paths to files or directories and outputs every file, @@ -291,6 +341,13 @@ def cli( ```python Contents of file1.py ``` + + If the --execute option is provided, the tool will execute the specified + command for each file and use the command's output as the content instead. + + \b + # Show the first 10 lines of each file + files-to-prompt path/to/directory --execute "head -n 10" """ # Reset global_index for pytest global global_index @@ -327,6 +384,7 @@ def cli( claude_xml, markdown, line_numbers, + execute_command, ) if claude_xml: writer("") diff --git a/tests/test_files_to_prompt.py b/tests/test_files_to_prompt.py index 5268995..d723b6d 100644 --- a/tests/test_files_to_prompt.py +++ b/tests/test_files_to_prompt.py @@ -439,3 +439,92 @@ def test_markdown(tmpdir, option): "`````\n" ) assert expected.strip() == actual.strip() + + +@pytest.mark.parametrize("option", ["-x", "--execute"]) +def test_execute_command(tmpdir, option): + runner = CliRunner() + with tmpdir.as_cwd(): + os.makedirs("test_dir") + with open("test_dir/file1.txt", "w") as f: + f.write("Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n") + + # Test with head command + result = runner.invoke(cli, ["test_dir", option, "head -n 2"]) + assert result.exit_code == 0 + assert "test_dir/file1.txt" in result.output + assert "Line 1" in result.output + assert "Line 2" in result.output + assert "Line 3" not in result.output + + # Test with grep command + result = runner.invoke(cli, ["test_dir", option, "grep 'Line 3'"]) + assert result.exit_code == 0 + assert "test_dir/file1.txt" in result.output + assert "Line 1" not in result.output + assert "Line 2" not in result.output + assert "Line 3" in result.output + assert "Line 4" not in result.output + + +def test_execute_command_with_error(tmpdir): + runner = CliRunner(mix_stderr=False) + with tmpdir.as_cwd(): + os.makedirs("test_dir") + with open("test_dir/file1.txt", "w") as f: + f.write("Contents of file1") + + # Test with command that returns non-zero exit code + result = runner.invoke( + cli, ["test_dir", "--execute", "grep 'nonexistent' || true"] + ) + assert result.exit_code == 0 + assert "test_dir/file1.txt" in result.stdout + + # Test with invalid command + result = runner.invoke( + cli, ["test_dir", "--execute", "invalid_command_that_does_not_exist"] + ) + assert result.exit_code == 0 + assert "test_dir/file1.txt" in result.stdout + assert "Error executing command" in result.stdout + assert "invalid_command_that_does_not_exist" in result.stderr + + +def test_execute_command_with_output_formats(tmpdir): + runner = CliRunner() + with tmpdir.as_cwd(): + os.makedirs("test_dir") + with open("test_dir/file1.txt", "w") as f: + f.write("Line 1\nLine 2\nLine 3\n") + + # Test with XML output + result = runner.invoke(cli, ["test_dir", "--execute", "head -n 1", "--cxml"]) + assert result.exit_code == 0 + assert "test_dir/file1.txt" in result.output + assert "\nLine 1\n\n" in result.output + + # Test with Markdown output + result = runner.invoke( + cli, ["test_dir", "--execute", "head -n 1", "--markdown"] + ) + assert result.exit_code == 0 + assert "test_dir/file1.txt" in result.output + assert "```\nLine 1\n\n```" in result.output + + +def test_execute_command_with_line_numbers(tmpdir): + runner = CliRunner() + with tmpdir.as_cwd(): + os.makedirs("test_dir") + with open("test_dir/file1.txt", "w") as f: + f.write("Line 1\nLine 2\nLine 3\n") + + # Test with line numbers + result = runner.invoke( + cli, ["test_dir", "--execute", "head -n 2", "--line-numbers"] + ) + assert result.exit_code == 0 + assert "test_dir/file1.txt" in result.output + assert "1 Line 1" in result.output + assert "2 Line 2" in result.output