Description:

When jobs are stopped it normally takes two attempts to exit with no
intervening commands. However, if there is exactly one intervening
command, ^D exits immediately instead of printing a warning. (The
"exit" builtin works correctly on the other hand.)

Repeat-By:

$ cat
^Z
[1]+  Stopped                 cat
$ ^D
exit
There are stopped jobs.
$ echo intervening command
intervening command
$ ^D
exit
<terminates>
# expected behaviour: print "There are stopped jobs" instead of exiting

Cause:

The exit_or_logout function in builtins/exit.def uses
last_shell_builtin to check if the last command was also an exit
attempt. However, ^D bypasses the command parser so last_shell_builtin
will actually be the second to last command.

In the example above, when the second ^D is typed, this_shell_builtin
is the "echo" command and last_shell_builtin is "exit" from the first
^D. So exit_or_logout thinks this is the second exit in a row when it
isn't.

Fix:

Patch attached. It makes exit_or_logout check that this_shell_builtin
is also an exit attempt.

(The code already sets both this_shell_builtin and last_shell_builtin
to "exit" on the first exit attempt, so typing ^D twice in a row will
exit immediately like it should.)

Configuration Information:
Machine: x86_64
OS: linux-gnu
Compiler: gcc
Compilation CFLAGS: -g -O2 -Wno-parentheses -Wno-format-security
uname output: Linux debian 4.9.0-8-amd64 #1 SMP Debian 4.9.144-3.1 (2019-02-19) 
x86_64 GNU/Linux
Machine Type: x86_64-pc-linux-gnu

Bash Version: 5.0
Patch Level: 2
Release Status: release

Regards,
Tom Levy
>From c4fdfa2874685685a0ba46516173abf2b5ba43ef Mon Sep 17 00:00:00 2001
From: Tom Levy <>
Date: Thu, 7 Mar 2019 04:58:18 +0000
Subject: [PATCH] fix ^D: don't exit while jobs are stopped after intervening
 command

---
 builtins/exit.def | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/builtins/exit.def b/builtins/exit.def
index 5167b2e0..193a6a59 100644
--- a/builtins/exit.def
+++ b/builtins/exit.def
@@ -51,6 +51,7 @@ $END
 extern int check_jobs_at_exit;
 
 static int exit_or_logout __P((WORD_LIST *));
+static int is_exit_attempt __P((sh_builtin_func_t *));
 static int sourced_logout;
 
 int
@@ -103,9 +104,8 @@ exit_or_logout (list)
   int exit_immediate_okay, stopmsg;
 
   exit_immediate_okay = (interactive  == 0 ||
-			 last_shell_builtin == exit_builtin ||
-			 last_shell_builtin == logout_builtin ||
-			 last_shell_builtin == jobs_builtin);
+			 (is_exit_attempt (this_shell_builtin) &&
+			  is_exit_attempt (last_shell_builtin)));
 
   /* Check for stopped jobs if the user wants to. */
   if (exit_immediate_okay == 0)
@@ -155,6 +155,15 @@ exit_or_logout (list)
   /*NOTREACHED*/
 }
 
+static int
+is_exit_attempt (function)
+     sh_builtin_func_t *function;
+{
+  return (function == exit_builtin ||
+	  function == logout_builtin ||
+	  function == jobs_builtin);
+}
+
 void
 bash_logout ()
 {
-- 
2.11.0

Reply via email to