keyboard.c: Allow SIGINT to quit in batch mode, instead of exit

In terminal sessions, SIGINT is turned into a `quit` ELisp signal,
but in batch it has traditionally killed Emacs.  It can be very
useful to cause a `quit` from outside the process when running
in batch (e.g. for "batch" sessions that provide a REPL via stdin/out),
so add a new var 'kill-emacs-on-sigint' to control that behavior.
(bug#80942)

* src/keyboard.c (handle_interrupt_signal): Obey `kill_emacs_on_sigint`.
(init_keyboard): Use `deliver_interrupt_signal` for SIGINT also for
batch sessions.
(syms_of_keyboard): New variable `kill_emacs_on_sigint`.

* test/src/keyboard-tests.el (keyboard-sigint-to-quit): New test.

* doc/emacs/cmdargs.texi (Initial Options): Mention the effect of
`kill-emacs-on-sigint` in batch mode.
This commit is contained in:
Stefan Monnier 2026-05-01 13:17:20 -04:00
parent 9c0a699c59
commit a952324e9b
4 changed files with 67 additions and 13 deletions

View file

@ -260,9 +260,14 @@ on. To invoke a Lisp program, use the @samp{-batch} option in
conjunction with one or more of @samp{-l}, @samp{-f} or @samp{--eval}
(@pxref{Action Arguments}). @xref{Command Example}, for an example.
@vindex kill-emacs-on-sigint
In batch mode, Emacs does not display the text being edited, and the
standard terminal interrupt characters such as @kbd{C-z} and @kbd{C-c}
have their usual effect. Emacs functions that normally print a
have their usual effect: for @kbd{C-c} that effect is either to
exit Emacs or to signal @code{quit}, depending on the variable
@code{kill-emacs-on-sigint}.
Emacs functions that normally print a
message in the echo area will print to either the standard output
stream (@code{stdout}) or the standard error stream (@code{stderr})
instead. (To be precise, functions like @code{prin1}, @code{princ}

View file

@ -4593,6 +4593,14 @@ is a proper list, in which case the list will be returned as is,
otherwise the function will return the object wrapped in a
singleton list.
---
** In batch mode, 'C-c' (i.e. SIGINT) can either 'quit' or kill Emacs.
By default it kills Emacs, as before, but 'kill-emacs-on-sigint'
can be set to nil to change that.
The response to SIGINT in interactive sessions is unaffected,
e.g. in a normal GUI session it still kills Emacs whereas in a terminal
it causes 'quit' since it is used for 'C-g'.
* Changes in Emacs 31.1 on Non-Free Operating Systems

View file

@ -12543,11 +12543,17 @@ handle_interrupt_signal (int sig)
struct terminal *terminal = get_named_terminal (dev_tty);
if (!terminal)
{
/* If there are no frames there, let's pretend that we are a
well-behaving UN*X program and quit. We must not call Lisp
/* There are no frames, so either 'quit' or exit.
If 'kill-emacs-on-sigint', pretend that we are a
well-behaving UN*X program and exit. We must not call Lisp
in a signal handler, so tell maybe_quit to exit when it is
safe. */
Vquit_flag = Qkill_emacs;
Vquit_flag = (kill_emacs_on_sigint
/* Don't risk running ELisp code while shutting down
and limit the effect of 'kill_emacs_on_sigint'
to batch sessions. */
|| NILP (Vrun_hooks) || !noninteractive
? Qkill_emacs : Qt);
}
else
{
@ -13224,17 +13230,18 @@ init_keyboard (void)
it for the initial terminal since there is no window system there. */
init_kboard (current_kboard, Qnil);
/* Before multi-tty support, these handlers used to be installed
only if the current session was a tty session. Now an Emacs
session may have multiple display types, so we always handle
SIGINT. There is special code in handle_interrupt_signal to exit
Emacs on SIGINT when there are no termcap frames on the
controlling terminal. */
struct sigaction action;
emacs_sigaction_init (&action, deliver_interrupt_signal);
sigaction (SIGINT, &action, 0);
if (!noninteractive)
{
/* Before multi-tty support, these handlers used to be installed
only if the current session was a tty session. Now an Emacs
session may have multiple display types, so we always handle
SIGINT. There is special code in handle_interrupt_signal to exit
Emacs on SIGINT when there are no termcap frames on the
controlling terminal. */
struct sigaction action;
emacs_sigaction_init (&action, deliver_interrupt_signal);
sigaction (SIGINT, &action, 0);
#ifndef DOS_NT
/* For systems with SysV TERMIO, C-g is set up for both SIGINT and
SIGQUIT and we can't tell which one it will give us. */
@ -13401,6 +13408,12 @@ syms_of_keyboard (void)
doc: /* Message displayed by `normal-top-level'. */);
Vinternal__top_level_message = regular_top_level_message;
DEFVAR_BOOL ("kill-emacs-on-sigint", kill_emacs_on_sigint,
doc: /* If non-nil, a SIGINT event causes Emacs to exit.
If nil, a SIGINT event causes a `quit` signal instead.
This is effective only in `noninteractive' sessions. */);
kill_emacs_on_sigint = true;
/* Tool-bars. */
DEFSYM (QCimage, ":image");
DEFSYM (Qhelp_echo, "help-echo");

View file

@ -81,6 +81,34 @@
(should-error (read-event "foo: "))
(should-error (read-char-exclusive "foo: "))))
(ert-deftest keyboard-sigint-to-quit () ;; bug#80942
(with-temp-buffer
(let* ((exit-msg "Exit via Quit")
(proc
(make-process
:name "keyboard-sigint-to-quit"
:buffer (current-buffer)
:command
`(,(expand-file-name invocation-name invocation-directory)
"-Q" "--batch" "--eval"
,(prin1-to-string
`(progn (setq kill-emacs-on-sigint nil)
(message "Ready!")
(condition-case nil
(dotimes (_ 3) (sit-for 1))
(quit (message "%s" ,exit-msg)))))))))
(while (progn (accept-process-output proc 1.0)
(goto-char (point-min))
(not (re-search-forward "Ready!" nil t)))
) ;; (message "Waiting for subprocess to be ready")
;; (message "Subprocess is ready")
(interrupt-process proc)
(while (prog1 (memq (process-status proc) '(run))
(accept-process-output proc 1.0))
) ;; (message "Waiting for subprocess to exit")
(goto-char (point-min))
(should (re-search-forward exit-msg nil t)))))
;;; Tests for `read-key-sequence' code paths.
;;;; Helpers