Skip to content

Commit 5fb4d38

Browse files
committed
Add -f/--file option to execute SQL commands from file
Implements the -f/--file command-line option similar to psql's behavior. This allows executing SQL commands from a file and then exiting. Features: - Support for -f and --file (short and long forms) - Multiple files can be specified (-f file1 -f file2) - Can be combined with -c option (-c commands execute first, then -f files) - Pager is disabled in file mode (consistent with -c behavior) - Comprehensive BDD tests added for all scenarios - Version bumped to 4.3.1
1 parent 309ffc2 commit 5fb4d38

File tree

7 files changed

+205
-12
lines changed

7 files changed

+205
-12
lines changed

changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Features:
1212
* Add support for `single-command` to run a SQL command and exit.
1313
* Command line option `-c` or `--command`.
1414
* You can specify multiple times.
15+
* Add support for `file` to execute commands from a file and exit.
16+
* Command line option `-f` or `--file`.
17+
* You can specify multiple times.
18+
* Similar to psql's `-f` option.
1519

1620
Internal:
1721
---------

pgcli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "4.3.0"
1+
__version__ = "4.3.1"

pgcli/main.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -911,14 +911,33 @@ def _check_ongoing_transaction_and_allow_quitting(self):
911911
def run_cli(self):
912912
logger = self.logger
913913

914-
# Handle command mode (-c flag) - similar to psql behavior
915-
# Multiple -c options are executed sequentially
916-
if hasattr(self, 'commands') and self.commands:
914+
# Handle command mode (-c flag) and/or file mode (-f flag)
915+
# Similar to psql behavior: execute commands/files and exit
916+
has_commands = hasattr(self, 'commands') and self.commands
917+
has_input_files = hasattr(self, 'input_files') and self.input_files
918+
919+
if has_commands or has_input_files:
917920
try:
918-
for command in self.commands:
919-
logger.debug("Running command: %s", command)
920-
# Execute the command using the same logic as interactive mode
921-
self.handle_watch_command(command)
921+
# Execute -c commands first, if any
922+
if has_commands:
923+
for command in self.commands:
924+
logger.debug("Running command: %s", command)
925+
self.handle_watch_command(command)
926+
927+
# Then execute commands from files, if provided
928+
# Multiple -f options are executed sequentially
929+
if has_input_files:
930+
for input_file in self.input_files:
931+
logger.debug("Reading commands from file: %s", input_file)
932+
with open(input_file, 'r', encoding='utf-8') as f:
933+
file_content = f.read()
934+
935+
# Execute the entire file content as a single command
936+
# This matches psql behavior where the file is treated as one unit
937+
if file_content.strip():
938+
logger.debug("Executing commands from file: %s", input_file)
939+
self.handle_watch_command(file_content)
940+
922941
except PgCliQuitError:
923942
# Normal exit from quit command
924943
sys.exit(0)
@@ -1297,8 +1316,10 @@ def is_too_tall(self, lines):
12971316
return len(lines) >= (self.prompt_app.output.get_size().rows - 4)
12981317

12991318
def echo_via_pager(self, text, color=None):
1300-
# Disable pager for -c/--command mode and \watch command
1301-
if self.pgspecial.pager_config == PAGER_OFF or self.watch_command or (hasattr(self, 'commands') and self.commands):
1319+
# Disable pager for -c/--command mode, -f/--file mode, and \watch command
1320+
has_commands = hasattr(self, 'commands') and self.commands
1321+
has_input_files = hasattr(self, 'input_files') and self.input_files
1322+
if self.pgspecial.pager_config == PAGER_OFF or self.watch_command or has_commands or has_input_files:
13021323
click.echo(text, color=color)
13031324
elif self.pgspecial.pager_config == PAGER_LONG_OUTPUT and self.table_format != "csv":
13041325
lines = text.split("\n")
@@ -1453,6 +1474,14 @@ def echo_via_pager(self, text, color=None):
14531474
multiple=True,
14541475
help="run command (SQL or internal) and exit. Multiple -c options are allowed.",
14551476
)
1477+
@click.option(
1478+
"-f",
1479+
"--file",
1480+
"input_files",
1481+
multiple=True,
1482+
type=click.Path(exists=True, readable=True, dir_okay=False),
1483+
help="execute commands from file, then exit. Multiple -f options are allowed.",
1484+
)
14561485
@click.argument("dbname", default=lambda: None, envvar="PGDATABASE", nargs=1)
14571486
@click.argument("username", default=lambda: None, envvar="PGUSER", nargs=1)
14581487
def cli(
@@ -1482,6 +1511,7 @@ def cli(
14821511
init_command: str,
14831512
log_file: str,
14841513
commands: tuple,
1514+
input_files: tuple,
14851515
):
14861516
if version:
14871517
print("Version:", __version__)
@@ -1545,6 +1575,9 @@ def cli(
15451575
# Store commands for -c option (can be multiple)
15461576
pgcli.commands = commands if commands else None
15471577

1578+
# Store file paths for -f option (can be multiple)
1579+
pgcli.input_files = input_files if input_files else None
1580+
15481581
# Choose which ever one has a valid value.
15491582
if dbname_opt and dbname:
15501583
# work as psql: when database is given as option and argument use the argument as user

tests/features/command_option.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Feature: run the cli with -c/--command option,
33
and exit
44

55
Scenario: run pgcli with -c and a SQL query
6-
When we run pgcli with -c "SELECT 1 as test_column"
6+
When we run pgcli with -c "SELECT 1 as test_diego_column"
77
then we see the query result
88
and pgcli exits successfully
99

tests/features/file_option.feature

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
Feature: run the cli with -f/--file option,
2+
execute commands from file,
3+
and exit
4+
5+
Scenario: run pgcli with -f and a SQL query file
6+
When we create a file with "SELECT 1 as test_diego_column"
7+
and we run pgcli with -f and the file
8+
then we see the query result
9+
and pgcli exits successfully
10+
11+
Scenario: run pgcli with --file and a SQL query file
12+
When we create a file with "SELECT 'hello' as greeting"
13+
and we run pgcli with --file and the file
14+
then we see the query result
15+
and pgcli exits successfully
16+
17+
Scenario: run pgcli with -f and a file with special command
18+
When we create a file with "\dt"
19+
and we run pgcli with -f and the file
20+
then we see the command output
21+
and pgcli exits successfully
22+
23+
Scenario: run pgcli with -f and a file with multiple statements
24+
When we create a file with "SELECT 1; SELECT 2"
25+
and we run pgcli with -f and the file
26+
then we see both query results
27+
and pgcli exits successfully
28+
29+
Scenario: run pgcli with -f and a file with an invalid query
30+
When we create a file with "SELECT invalid_column FROM nonexistent_table"
31+
and we run pgcli with -f and the file
32+
then we see an error message
33+
and pgcli exits successfully
34+
35+
Scenario: run pgcli with both -c and -f options
36+
When we create a file with "SELECT 2 as second"
37+
and we run pgcli with -c "SELECT 1 as first" and -f with the file
38+
then we see both query results
39+
and pgcli exits successfully

tests/features/steps/command_option.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def step_see_query_result(context):
6767
# Check for common query result indicators
6868
assert any([
6969
"SELECT" in output,
70-
"test_column" in output,
70+
"test_diego_column" in output,
7171
"greeting" in output,
7272
"hello" in output,
7373
"+-" in output, # table border
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""
2+
Steps for testing -f/--file option behavioral tests.
3+
Reuses common steps from command_option.py
4+
"""
5+
6+
import subprocess
7+
import tempfile
8+
import os
9+
from behave import when
10+
11+
12+
@when('we create a file with "{content}"')
13+
def step_create_file_with_content(context, content):
14+
"""Create a temporary file with the given content."""
15+
# Create a temporary file that will be cleaned up automatically
16+
temp_file = tempfile.NamedTemporaryFile(
17+
mode='w',
18+
delete=False,
19+
suffix='.sql'
20+
)
21+
temp_file.write(content)
22+
temp_file.close()
23+
context.temp_file_path = temp_file.name
24+
25+
26+
@when('we run pgcli with -f and the file')
27+
def step_run_pgcli_with_f(context):
28+
"""Run pgcli with -f flag and the temporary file."""
29+
cmd = [
30+
"pgcli",
31+
"-h", context.conf["host"],
32+
"-p", str(context.conf["port"]),
33+
"-U", context.conf["user"],
34+
"-d", context.conf["dbname"],
35+
"-f", context.temp_file_path
36+
]
37+
try:
38+
context.cmd_output = subprocess.check_output(
39+
cmd,
40+
cwd=context.package_root,
41+
stderr=subprocess.STDOUT,
42+
timeout=5
43+
)
44+
context.exit_code = 0
45+
except subprocess.CalledProcessError as e:
46+
context.cmd_output = e.output
47+
context.exit_code = e.returncode
48+
except subprocess.TimeoutExpired as e:
49+
context.cmd_output = b"Command timed out"
50+
context.exit_code = -1
51+
finally:
52+
# Clean up the temporary file
53+
if hasattr(context, 'temp_file_path') and os.path.exists(context.temp_file_path):
54+
os.unlink(context.temp_file_path)
55+
56+
57+
@when('we run pgcli with --file and the file')
58+
def step_run_pgcli_with_file(context):
59+
"""Run pgcli with --file flag and the temporary file."""
60+
cmd = [
61+
"pgcli",
62+
"-h", context.conf["host"],
63+
"-p", str(context.conf["port"]),
64+
"-U", context.conf["user"],
65+
"-d", context.conf["dbname"],
66+
"--file", context.temp_file_path
67+
]
68+
try:
69+
context.cmd_output = subprocess.check_output(
70+
cmd,
71+
cwd=context.package_root,
72+
stderr=subprocess.STDOUT,
73+
timeout=5
74+
)
75+
context.exit_code = 0
76+
except subprocess.CalledProcessError as e:
77+
context.cmd_output = e.output
78+
context.exit_code = e.returncode
79+
except subprocess.TimeoutExpired as e:
80+
context.cmd_output = b"Command timed out"
81+
context.exit_code = -1
82+
finally:
83+
# Clean up the temporary file
84+
if hasattr(context, 'temp_file_path') and os.path.exists(context.temp_file_path):
85+
os.unlink(context.temp_file_path)
86+
87+
88+
@when('we run pgcli with -c "{command}" and -f with the file')
89+
def step_run_pgcli_with_c_and_f(context, command):
90+
"""Run pgcli with both -c and -f flags."""
91+
cmd = [
92+
"pgcli",
93+
"-h", context.conf["host"],
94+
"-p", str(context.conf["port"]),
95+
"-U", context.conf["user"],
96+
"-d", context.conf["dbname"],
97+
"-c", command,
98+
"-f", context.temp_file_path
99+
]
100+
try:
101+
context.cmd_output = subprocess.check_output(
102+
cmd,
103+
cwd=context.package_root,
104+
stderr=subprocess.STDOUT,
105+
timeout=5
106+
)
107+
context.exit_code = 0
108+
except subprocess.CalledProcessError as e:
109+
context.cmd_output = e.output
110+
context.exit_code = e.returncode
111+
except subprocess.TimeoutExpired as e:
112+
context.cmd_output = b"Command timed out"
113+
context.exit_code = -1
114+
finally:
115+
# Clean up the temporary file
116+
if hasattr(context, 'temp_file_path') and os.path.exists(context.temp_file_path):
117+
os.unlink(context.temp_file_path)

0 commit comments

Comments
 (0)