Skip to content

Commit 57e174e

Browse files
committed
Merge PR dbcli#1530: Add support for multiple -c/--command parameters
2 parents 1723391 + 309ffc2 commit 57e174e

File tree

5 files changed

+266
-1
lines changed

5 files changed

+266
-1
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ Contributors:
144144
* Jay Knight (jay-knight)
145145
* fbdb
146146
* Charbel Jacquin (charbeljc)
147+
* Diego
147148

148149
Creator:
149150
--------

changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ Features:
99
* Support dsn specific init-command in the config file
1010
* Add suggestion when setting the search_path
1111
* Allow per dsn_alias ssh tunnel selection
12+
* Add support for `single-command` to run a SQL command and exit.
13+
* Command line option `-c` or `--command`.
14+
* You can specify multiple times.
1215

1316
Internal:
1417
---------

pgcli/main.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,25 @@ 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:
917+
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)
922+
except PgCliQuitError:
923+
# Normal exit from quit command
924+
sys.exit(0)
925+
except Exception as e:
926+
logger.error("Error executing command: %s", e)
927+
logger.error("traceback: %r", traceback.format_exc())
928+
click.secho(str(e), err=True, fg="red")
929+
sys.exit(1)
930+
# Exit successfully after executing all commands
931+
sys.exit(0)
932+
914933
history_file = self.config["main"]["history_file"]
915934
if history_file == "default":
916935
history_file = config_location() + "history"
@@ -1278,7 +1297,8 @@ def is_too_tall(self, lines):
12781297
return len(lines) >= (self.prompt_app.output.get_size().rows - 4)
12791298

12801299
def echo_via_pager(self, text, color=None):
1281-
if self.pgspecial.pager_config == PAGER_OFF or self.watch_command:
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):
12821302
click.echo(text, color=color)
12831303
elif self.pgspecial.pager_config == PAGER_LONG_OUTPUT and self.table_format != "csv":
12841304
lines = text.split("\n")
@@ -1426,6 +1446,13 @@ def echo_via_pager(self, text, color=None):
14261446
type=str,
14271447
help="SQL statement to execute after connecting.",
14281448
)
1449+
@click.option(
1450+
"-c",
1451+
"--command",
1452+
"commands",
1453+
multiple=True,
1454+
help="run command (SQL or internal) and exit. Multiple -c options are allowed.",
1455+
)
14291456
@click.argument("dbname", default=lambda: None, envvar="PGDATABASE", nargs=1)
14301457
@click.argument("username", default=lambda: None, envvar="PGUSER", nargs=1)
14311458
def cli(
@@ -1454,6 +1481,7 @@ def cli(
14541481
ssh_tunnel: str,
14551482
init_command: str,
14561483
log_file: str,
1484+
commands: tuple,
14571485
):
14581486
if version:
14591487
print("Version:", __version__)
@@ -1514,6 +1542,9 @@ def cli(
15141542
log_file=log_file,
15151543
)
15161544

1545+
# Store commands for -c option (can be multiple)
1546+
pgcli.commands = commands if commands else None
1547+
15171548
# Choose which ever one has a valid value.
15181549
if dbname_opt and dbname:
15191550
# work as psql: when database is given as option and argument use the argument as user
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
Feature: run the cli with -c/--command option,
2+
execute a single command,
3+
and exit
4+
5+
Scenario: run pgcli with -c and a SQL query
6+
When we run pgcli with -c "SELECT 1 as test_column"
7+
then we see the query result
8+
and pgcli exits successfully
9+
10+
Scenario: run pgcli with --command and a SQL query
11+
When we run pgcli with --command "SELECT 'hello' as greeting"
12+
then we see the query result
13+
and pgcli exits successfully
14+
15+
Scenario: run pgcli with -c and a special command
16+
When we run pgcli with -c "\dt"
17+
then we see the command output
18+
and pgcli exits successfully
19+
20+
Scenario: run pgcli with -c and an invalid query
21+
When we run pgcli with -c "SELECT invalid_column FROM nonexistent_table"
22+
then we see an error message
23+
and pgcli exits successfully
24+
25+
Scenario: run pgcli with -c and multiple statements
26+
When we run pgcli with -c "SELECT 1; SELECT 2"
27+
then we see both query results
28+
and pgcli exits successfully
29+
30+
Scenario: run pgcli with multiple -c options
31+
When we run pgcli with multiple -c options
32+
then we see all command outputs
33+
and pgcli exits successfully
34+
35+
Scenario: run pgcli with mixed -c and --command options
36+
When we run pgcli with mixed -c and --command
37+
then we see all command outputs
38+
and pgcli exits successfully
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"""
2+
Steps for testing -c/--command option behavioral tests.
3+
"""
4+
5+
import subprocess
6+
from behave import when, then
7+
8+
9+
@when('we run pgcli with -c "{command}"')
10+
def step_run_pgcli_with_c(context, command):
11+
"""Run pgcli with -c flag and a command."""
12+
cmd = [
13+
"pgcli",
14+
"-h", context.conf["host"],
15+
"-p", str(context.conf["port"]),
16+
"-U", context.conf["user"],
17+
"-d", context.conf["dbname"],
18+
"-c", command
19+
]
20+
try:
21+
context.cmd_output = subprocess.check_output(
22+
cmd,
23+
cwd=context.package_root,
24+
stderr=subprocess.STDOUT,
25+
timeout=5
26+
)
27+
context.exit_code = 0
28+
except subprocess.CalledProcessError as e:
29+
context.cmd_output = e.output
30+
context.exit_code = e.returncode
31+
except subprocess.TimeoutExpired as e:
32+
context.cmd_output = b"Command timed out"
33+
context.exit_code = -1
34+
35+
36+
@when('we run pgcli with --command "{command}"')
37+
def step_run_pgcli_with_command(context, command):
38+
"""Run pgcli with --command flag and a command."""
39+
cmd = [
40+
"pgcli",
41+
"-h", context.conf["host"],
42+
"-p", str(context.conf["port"]),
43+
"-U", context.conf["user"],
44+
"-d", context.conf["dbname"],
45+
"--command", command
46+
]
47+
try:
48+
context.cmd_output = subprocess.check_output(
49+
cmd,
50+
cwd=context.package_root,
51+
stderr=subprocess.STDOUT,
52+
timeout=5
53+
)
54+
context.exit_code = 0
55+
except subprocess.CalledProcessError as e:
56+
context.cmd_output = e.output
57+
context.exit_code = e.returncode
58+
except subprocess.TimeoutExpired as e:
59+
context.cmd_output = b"Command timed out"
60+
context.exit_code = -1
61+
62+
63+
@then("we see the query result")
64+
def step_see_query_result(context):
65+
"""Verify that the query result is in the output."""
66+
output = context.cmd_output.decode('utf-8')
67+
# Check for common query result indicators
68+
assert any([
69+
"SELECT" in output,
70+
"test_column" in output,
71+
"greeting" in output,
72+
"hello" in output,
73+
"+-" in output, # table border
74+
"|" in output, # table column separator
75+
]), f"Expected query result in output, but got: {output}"
76+
77+
78+
@then("we see both query results")
79+
def step_see_both_query_results(context):
80+
"""Verify that both query results are in the output."""
81+
output = context.cmd_output.decode('utf-8')
82+
# Should contain output from both SELECT statements
83+
assert "SELECT" in output, f"Expected SELECT in output, but got: {output}"
84+
# The output should have multiple result sets
85+
assert output.count("SELECT") >= 2, f"Expected at least 2 SELECT results, but got: {output}"
86+
87+
88+
@then("we see the command output")
89+
def step_see_command_output(context):
90+
"""Verify that the special command output is present."""
91+
output = context.cmd_output.decode('utf-8')
92+
# For \dt we should see table-related output
93+
# It might be empty if no tables exist, but shouldn't error
94+
assert context.exit_code == 0, f"Expected exit code 0, but got: {context.exit_code}"
95+
96+
97+
@then("we see an error message")
98+
def step_see_error_message(context):
99+
"""Verify that an error message is in the output."""
100+
output = context.cmd_output.decode('utf-8')
101+
assert any([
102+
"does not exist" in output,
103+
"error" in output.lower(),
104+
"ERROR" in output,
105+
]), f"Expected error message in output, but got: {output}"
106+
107+
108+
@then("pgcli exits successfully")
109+
def step_pgcli_exits_successfully(context):
110+
"""Verify that pgcli exited with code 0."""
111+
assert context.exit_code == 0, f"Expected exit code 0, but got: {context.exit_code}"
112+
# Clean up
113+
context.cmd_output = None
114+
context.exit_code = None
115+
116+
117+
@then("pgcli exits with error")
118+
def step_pgcli_exits_with_error(context):
119+
"""Verify that pgcli exited with a non-zero code."""
120+
assert context.exit_code != 0, f"Expected non-zero exit code, but got: {context.exit_code}"
121+
# Clean up
122+
context.cmd_output = None
123+
context.exit_code = None
124+
125+
126+
@when("we run pgcli with multiple -c options")
127+
def step_run_pgcli_with_multiple_c(context):
128+
"""Run pgcli with multiple -c flags."""
129+
cmd = [
130+
"pgcli",
131+
"-h", context.conf["host"],
132+
"-p", str(context.conf["port"]),
133+
"-U", context.conf["user"],
134+
"-d", context.conf["dbname"],
135+
"-c", "SELECT 'first' as result",
136+
"-c", "SELECT 'second' as result",
137+
"-c", "SELECT 'third' as result"
138+
]
139+
try:
140+
context.cmd_output = subprocess.check_output(
141+
cmd,
142+
cwd=context.package_root,
143+
stderr=subprocess.STDOUT,
144+
timeout=10
145+
)
146+
context.exit_code = 0
147+
except subprocess.CalledProcessError as e:
148+
context.cmd_output = e.output
149+
context.exit_code = e.returncode
150+
except subprocess.TimeoutExpired as e:
151+
context.cmd_output = b"Command timed out"
152+
context.exit_code = -1
153+
154+
155+
@when("we run pgcli with mixed -c and --command")
156+
def step_run_pgcli_with_mixed_options(context):
157+
"""Run pgcli with mixed -c and --command flags."""
158+
cmd = [
159+
"pgcli",
160+
"-h", context.conf["host"],
161+
"-p", str(context.conf["port"]),
162+
"-U", context.conf["user"],
163+
"-d", context.conf["dbname"],
164+
"-c", "SELECT 'from_c' as source",
165+
"--command", "SELECT 'from_command' as source"
166+
]
167+
try:
168+
context.cmd_output = subprocess.check_output(
169+
cmd,
170+
cwd=context.package_root,
171+
stderr=subprocess.STDOUT,
172+
timeout=10
173+
)
174+
context.exit_code = 0
175+
except subprocess.CalledProcessError as e:
176+
context.cmd_output = e.output
177+
context.exit_code = e.returncode
178+
except subprocess.TimeoutExpired as e:
179+
context.cmd_output = b"Command timed out"
180+
context.exit_code = -1
181+
182+
183+
@then("we see all command outputs")
184+
def step_see_all_command_outputs(context):
185+
"""Verify that all command outputs are present."""
186+
output = context.cmd_output.decode('utf-8')
187+
# Should contain output from all commands
188+
assert "first" in output or "from_c" in output, f"Expected 'first' or 'from_c' in output, but got: {output}"
189+
assert "second" in output or "from_command" in output, f"Expected 'second' or 'from_command' in output, but got: {output}"
190+
# For the 3-command test, also check for third
191+
if "third" in output or "result" in output:
192+
assert "third" in output, f"Expected 'third' in output for 3-command test, but got: {output}"

0 commit comments

Comments
 (0)