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