Add new "$_shell(CMD)" internal function

For testing a following patch, I wanted a way to send a SIGINT to GDB
from a breakpoint condition.  And I didn't want to do it from a Python
breakpoint or Python function, as I wanted to exercise non-Python code
paths.  So I thought I'd add a new $_shell internal function, that
runs a command under the shell, and returns the exit code.  With this,
I could write:

  (gdb) b foo if $_shell("kill -SIGINT $gdb_pid") != 0 || <other condition>

I think this is generally useful, hence I'm proposing it here.

Here's the new function in action:

 (gdb) p $_shell("true")
 $1 = 0
 (gdb) p $_shell("false")
 $2 = 1
 (gdb) p $_shell("echo hello")
 hello
 $3 = 0
 (gdb) p $_shell("foobar")
 bash: line 1: foobar: command not found
 $4 = 127
 (gdb) help function _shell
 $_shell - execute a shell command and returns the result.
 Usage: $_shell (command)
 Returns the command's exit code: zero on success, non-zero otherwise.
 (gdb)

NEWS and manual changes included.

Approved-By: Andrew Burgess <aburgess@redhat.com>
Approved-By: Tom Tromey <tom@tromey.com>
Approved-By: Eli Zaretskii <eliz@gnu.org>
Change-Id: I7e36d451ee6b428cbf41fded415ae2d6b4efaa4e
This commit is contained in:
Pedro Alves 2023-02-08 16:06:23 +00:00
parent 751495be92
commit 91265a7d7c
5 changed files with 198 additions and 5 deletions

View File

@ -68,6 +68,16 @@ maintenance info frame-unwinders
List the frame unwinders currently in effect, starting with the highest
priority.
* New convenience function "$_shell", to execute a shell command and
return the result. This lets you run shell commands in expressions.
Some examples:
(gdb) p $_shell("true")
$1 = 0
(gdb) p $_shell("false")
$2 = 1
(gdb) break func if $_shell("some command") == 0
* MI changes
** mi now reports 'no-history' as a stop reason when hitting the end of the

View File

@ -39,6 +39,7 @@
#include "gdbsupport/filestuff.h"
#include "location.h"
#include "block.h"
#include "valprint.h"
#include "ui-out.h"
#include "interps.h"
@ -873,6 +874,9 @@ exit_status_set_internal_vars (int exit_status)
clear_internalvar (var_code);
clear_internalvar (var_signal);
/* Keep the logic here in sync with shell_internal_fn. */
if (WIFEXITED (exit_status))
set_internalvar_integer (var_code, WEXITSTATUS (exit_status));
#ifdef __MINGW32__
@ -893,8 +897,11 @@ exit_status_set_internal_vars (int exit_status)
warning (_("unexpected shell command exit status %d"), exit_status);
}
static void
shell_escape (const char *arg, int from_tty)
/* Run ARG under the shell, and return the exit status. If ARG is
NULL, run an interactive shell. */
static int
run_under_shell (const char *arg, int from_tty)
{
#if defined(CANT_FORK) || \
(!defined(HAVE_WORKING_VFORK) && !defined(HAVE_WORKING_FORK))
@ -915,7 +922,7 @@ shell_escape (const char *arg, int from_tty)
the shell command we just ran changed it. */
chdir (current_directory);
#endif
exit_status_set_internal_vars (rc);
return rc;
#else /* Can fork. */
int status, pid;
@ -942,10 +949,21 @@ shell_escape (const char *arg, int from_tty)
waitpid (pid, &status, 0);
else
error (_("Fork failed"));
exit_status_set_internal_vars (status);
return status;
#endif /* Can fork. */
}
/* Escape out to the shell to run ARG. If ARG is NULL, launch and
interactive shell. Sets $_shell_exitcode and $_shell_exitsignal
convenience variables based on the exits status. */
static void
shell_escape (const char *arg, int from_tty)
{
int status = run_under_shell (arg, from_tty);
exit_status_set_internal_vars (status);
}
/* Implementation of the "shell" command. */
static void
@ -2417,6 +2435,63 @@ gdb_maint_setting_str_internal_fn (struct gdbarch *gdbarch,
return str_value_from_setting (*show_cmd->var, gdbarch);
}
/* Implementation of the convenience function $_shell. */
static struct value *
shell_internal_fn (struct gdbarch *gdbarch,
const struct language_defn *language,
void *cookie, int argc, struct value **argv)
{
if (argc != 1)
error (_("You must provide one argument for $_shell."));
value *val = argv[0];
struct type *type = check_typedef (val->type ());
if (!language->is_string_type_p (type))
error (_("Argument must be a string."));
value_print_options opts;
get_no_prettyformat_print_options (&opts);
string_file stream;
value_print (val, &stream, &opts);
/* We should always have two quote chars, which we'll strip. */
gdb_assert (stream.size () >= 2);
/* Now strip them. We don't need the original string, so it's
cheaper to do it in place, avoiding a string allocation. */
std::string str = stream.release ();
str[str.size () - 1] = 0;
const char *command = str.c_str () + 1;
int exit_status = run_under_shell (command, 0);
struct type *int_type = builtin_type (gdbarch)->builtin_int;
/* Keep the logic here in sync with
exit_status_set_internal_vars. */
if (WIFEXITED (exit_status))
return value_from_longest (int_type, WEXITSTATUS (exit_status));
#ifdef __MINGW32__
else if (WIFSIGNALED (exit_status) && WTERMSIG (exit_status) == -1)
{
/* See exit_status_set_internal_vars. */
return value_from_longest (int_type, exit_status);
}
#endif
else if (WIFSIGNALED (exit_status))
{
/* (0x80 | SIGNO) is what most (all?) POSIX-like shells set as
exit code on fatal signal termination. */
return value_from_longest (int_type, 0x80 | WTERMSIG (exit_status));
}
else
return value::allocate_optimized_out (int_type);
}
void _initialize_cli_cmds ();
void
_initialize_cli_cmds ()
@ -2606,6 +2681,19 @@ Some integer settings accept an unlimited value, returned\n\
as 0 or -1 depending on the setting."),
gdb_maint_setting_internal_fn, NULL);
add_internal_function ("_shell", _("\
$_shell - execute a shell command and return the result.\n\
\n\
Usage: $_shell (COMMAND)\n\
\n\
Arguments:\n\
\n\
COMMAND: The command to execute. Must be a string.\n\
\n\
Returns:\n\
The command's exit code: zero on success, non-zero otherwise."),
shell_internal_fn, NULL);
add_cmd ("commands", no_set_class, show_commands, _("\
Show the history of commands you typed.\n\
You can supply a command number to start with, or a `+' to start after\n\

View File

@ -1621,7 +1621,7 @@ just use the @code{shell} command.
@cindex shell escape
@item shell @var{command-string}
@itemx !@var{command-string}
Invoke a standard shell to execute @var{command-string}.
Invoke a shell to execute @var{command-string}.
Note that no space is needed between @code{!} and @var{command-string}.
On GNU and Unix systems, the environment variable @env{SHELL}, if it
exists, determines which shell to run. Otherwise @value{GDBN} uses
@ -1629,6 +1629,10 @@ the default shell (@file{/bin/sh} on GNU and Unix systems,
@file{cmd.exe} on MS-Windows, @file{COMMAND.COM} on MS-DOS, etc.).
@end table
You may also invoke shell commands from expressions, using the
@code{$_shell} convenience function. @xref{$_shell convenience
function}.
The utility @code{make} is often needed in development environments.
You do not have to use the @code{shell} command for this purpose in
@value{GDBN}:
@ -12977,6 +12981,60 @@ Like the @code{$_gdb_setting_str} function, but works with
Like the @code{$_gdb_setting} function, but works with
@code{maintenance set} variables.
@anchor{$_shell convenience function}
@findex $_shell@r{, convenience function}
@item $_shell (@var{command-string})
Invoke a shell to execute @var{command-string}. @var{command-string}
must be a string. The shell runs on the host machine, the machine
@value{GDBN} is running on. Returns the command's exit status. On
Unix systems, a command which exits with a zero exit status has
succeeded, and non-zero exit status indicates failure. When a command
terminates on a fatal signal whose number is @var{N}, @value{GDBN}
uses the value 128+@var{N} as the exit status, as is standard in Unix
shells. Note that @var{N} is a host signal number, not a target
signal number. If you're native debugging, they will be the same, but
if cross debugging, the host vs target signal numbers may be
completely unrelated. Please consult your host operating system's
documentation for the mapping between host signal numbers and signal
names. The shell to run is determined in the same way as for the
@code{shell} command. @xref{Shell Commands, ,Shell Commands}.
@smallexample
(@value{GDBP}) print $_shell("true")
$1 = 0
(@value{GDBP}) print $_shell("false")
$2 = 1
(@value{GDBP}) p $_shell("echo hello")
hello
$3 = 0
(@value{GDBP}) p $_shell("foobar")
bash: line 1: foobar: command not found
$4 = 127
@end smallexample
This may also be useful in breakpoint conditions. For example:
@smallexample
(@value{GDBP}) break function if $_shell("some command") == 0
@end smallexample
In this scenario, you'll want to make sure that the shell command you
run in the breakpoint condition takes the least amount of time
possible. For example, avoid running a command that may block
indefinitely, or that sleeps for a while before exiting. Prefer a
command or script which analyzes some state and exits immediately.
This is important because the debugged program stops for the
breakpoint every time, and then @value{GDBN} evaluates the breakpoint
condition. If the condition is false, the program is re-resumed
transparently, without informing you of the stop. A quick shell
command thus avoids significantly slowing down the debugged program
unnecessarily.
Note: unlike the @code{shell} command, the @code{$_shell} convenience
function does not affect the @code{$_shell_exitcode} and
@code{$_shell_exitsignal} convenience variables.
@end table
The following functions require @value{GDBN} to be configured with

View File

@ -606,6 +606,7 @@ set show_conv_list \
{$_cimag = <internal function _cimag>} \
{$_creal = <internal function _creal>} \
{$_isvoid = <internal function _isvoid>} \
{$_shell = <internal function _shell>} \
{$_gdb_maint_setting_str = <internal function _gdb_maint_setting_str>} \
{$_gdb_maint_setting = <internal function _gdb_maint_setting>} \
{$_gdb_setting_str = <internal function _gdb_setting_str>} \

View File

@ -41,6 +41,42 @@ if { ! [ishost *-*-mingw*] } {
gdb_test "p \$_shell_exitsignal" " = 2" "shell interrupt exitsignal"
}
# Test the $_shell convenience function.
with_test_prefix "\$_shell convenience function" {
# Simple commands, check the result code.
gdb_test "p \$_shell(\"true\")" " = 0"
gdb_test "p \$_shell(\"false\")" " = 1"
# Test command with arguments.
gdb_test "p \$_shell(\"echo foo\")" "foo\r\n\\$${decimal} = 0"
# Check the type of the result.
gdb_test "ptype \$_shell(\"true\")" "type = int"
# Test passing a non-literal string as command name.
gdb_test "p \$cmd = \"echo bar\"" " = \"echo bar\""
gdb_test "p \$_shell(\$cmd)" "bar\r\n\\$${decimal} = 0"
# Test executing a non-existing command. The result is
# shell-dependent, but most (all?) POSIX-like shells return 127 in
# this case.
gdb_test "p \$_shell(\"non-existing-command-foo-bar-qux\")" " = 127"
gdb_test "p \$_shell" \
" = <internal function _shell>"
gdb_test "ptype \$_shell" \
"type = <internal function>"
# Test error scenarios.
gdb_test "p \$_shell()" \
"You must provide one argument for \\\$_shell\\\."
gdb_test "p \$_shell(\"a\", \"b\")" \
"You must provide one argument for \\\$_shell\\\."
gdb_test "p \$_shell(1)" \
"Argument must be a string\\\."
}
# Define the user command "foo", used to test "pipe" command.
gdb_test_multiple "define foo" "define foo" {
-re "End with" {