Skip to content

Add -x option for custom output #58

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
80 changes: 69 additions & 11 deletions files_to_prompt/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import os
import shlex
import subprocess
import sys
from fnmatch import fnmatch

Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -327,6 +384,7 @@ def cli(
claude_xml,
markdown,
line_numbers,
execute_command,
)
if claude_xml:
writer("</documents>")
Expand Down
89 changes: 89 additions & 0 deletions tests/test_files_to_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<source>test_dir/file1.txt</source>" in result.output
assert "<document_content>\nLine 1\n\n</document_content>" 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