I've got code similar to the following, using readline:
#include <errno.h>
#include <error.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <readline/readline.h>
#include <readline/history.h>
void handle_signals(int signo) {
if (signo == SIGINT) {
printf("You pressed Ctrl+C\n");
}
}
int main (int argc, char **argv)
{
//printf("path is: %s\n", path_string);
char * input;
char * shell_prompt = "i-shell> ";
if (signal(SIGINT, handle_signals) == SIG_ERR) {
printf("failed to register interrupts with kernel\n");
}
//set up custom completer and associated data strucutres
setup_readline();
while (1)
{
input = readline(shell_prompt);
if (!input)
break;
add_history(input);
//do something with the code
execute_command(input);
}
return 0;
}
I've got it set up to intercept SIGINT
(i.e. user pressing Ctrl+C
), so I can tell that the signal handler handle_signals()
is working. However, when control returns to readline()
, it's using the same line of text it was using prior to the input. What I'd like to happen is for readline to "cancel" the current line of text and give me a new line, much like the BASH shell. Something like so:
i-shell> bad_command^C
i-shell> _
Any chance of getting this to work? Something on a mailing list I read mentioned using longjmp(2)
, but that really doesn't seem like a good idea.
Call
rl_clear_signals()
.This will disable the signal handlers
libreadline
installed. The one that handlesSIGINT
is responsible for the observed behaviour of restoring the prompt.More details on how to manage
readline()
s signal handling can be read here.I was confused at first by jancheta's answer, until I discovered that the purpose of
siglongjmp
is to unblock the received signal in the signal mask, before doing the jump. The signal is blocked at the entry of the signal handler so that the handler doesn't interrupt itself. We don't want to leave the signal blocked when we resume normal execution, and that's why we usesiglongjmp
instead oflongjmp
. AIUI, this is just shorthand, we could also callsigprocmask
followed bylongjmp
, which seems to be what glibc is doing insiglongjmp
.I thought it might be unsafe to do a jump because
readline()
callsmalloc
andfree
. If the signal is received while some async-signal-unsafe function likemalloc
orfree
is modifying global state, some corruption could result if we were to then jump out of the signal handler. But Readline installs its own signal handlers which are careful about this. They just set a flag and exit; when the Readline library gets control again (usually after an interrupted 'read()' call) it callsRL_CHECK_SIGNALS()
which then forwards any pending signal to the client application usingkill()
. So it is safe to usesiglongjmp()
to exit a signal handler for a signal which interrupted a call toreadline()
- the signal is guaranteed not to have been received during an async-signal-unsafe function.Actually, that's not entirely true because there are a few calls to
malloc()
andfree()
withinrl_set_prompt()
, whichreadline()
calls just beforerl_set_signals()
. I wonder if this calling order should be changed. In any case the probability of race condition is very slim.I looked at the Bash source code and it seems to jump out of its SIGINT handler.
Another Readline interface you can use is the callback interface. That is used by applications such as Python or R which need to listen on multiple file descriptors at once, for instance to tell if a plot window is being resized while the command line interface is active. They'll do this in a
select()
loop.Here is a message from Chet Ramey which gives some ideas of what to do to obtain Bash-like behavior upon receiving SIGINT in the callback interface:
https://lists.gnu.org/archive/html/bug-readline/2016-04/msg00071.html
The messages suggests that you do something like this:
When your SIGINT is received, you could set a flag, and later check the flag in your
select()
loop - since theselect()
call will get interrupted by the signal witherrno==EINTR
. If you find that the flag has been set, execute the above code.My opinion is that Readline should run something like the above fragment in its own SIGINT handling code. Currently it more or less executes just the first two lines, which is why stuff like incremental-search and keyboard macros are cancelled by ^C, but the line isn't cleared.
Another poster said "Call rl_clear_signals()", which still confuses me. I haven't tried it but I don't see how it would accomplish anything given that (1) Readline's signal handlers forward the signal to you anyway, and (2)
readline()
installs the signal handlers upon entry (and clears them when it exits), so they won't normally be active outside of Readline code.Creating a jump seems hacky and error-prone to me. The shell implementation I was adding this support to didn't allow for this change.
Luckily,
readline
has a clearer, alternative solution. MySIGINT
handler looks like this:This took no other additional code elsewhere to get this working — no global variables, no setting jumps.
You are correct in your line of thinking to use longjmp. But because the longjmp would be in a signal handler, you need to use sigsetjmp/siglongjmp.
As a quick example using your code as a base:
siglongjmp returns a value other than 0 (in this case a 1) to sigsetjmp so the while loop calls sigsetjmp again (a successful return value of sigsetjmp is 0) and will then call readline again.
it may also be helpful to set
rl_catch_signals = 1
and then callrl_set_signals()
so that the readline signal handling cleans up any variables it needs to before passing the signal to your program where you will then jump back to call readline a second time.