On BASH that has no &!, and old tricks suddenly rediscovered
TL;DR: don’t want to spoil the fun, so please check Summary at the end.
A friend of mine just posted a
recipe
describing how to avoid long Emacs startup delays using Emacs daemon. Being kind
of a portability advocate, I pointed out to him that &!
works only in
ZSH and thus should not be used in a seemingly shell-agnostic
post.
“It’s specific to ZSH and won’t work in BASH”, I said.
“Yet it does. I checked”, was his answer.
So I checked too: I opened a terminal window, fired up BASH (had to do it manually because I’m ZSH user myself) and ran the first GUI application that came to my mind:
% bash # percent prompt indicates ZSH
$ easytag &! # dollar sign prompt means BASH
That gave me some output, which I ignored. A moment later, EasyTag appeared on
the screen. I then quitted BASH by pressing Ctrl-D
, and was really surprised
when EasyTag didn’t die. It didn’t even die when I closed the window, which
surprised me even further.
“That’s not right”, I thought. “Wait, was was that output I ignored?..”
So yeah, in BASH, &!
prints this:
[1] 15878
Looks familiar, isn’t it? That’s because it is familiar:
$ easytag &
[1] 15926
Can it be so that BASH ignores trailing exclamation mark? Let’s write a command list and see what happens:
$ echo a & echo b
[1] 16010
b
a
$ echo a &! echo b
[2] 16012
b
a
[1] Done echo a
Seems logical to take one more step:
$ echo a & ! echo b
[3] 16033
b
[2] Done echo a
a
So indeed, BASH parses ampersand and exclamation mark as two separate operators.
It’s obvious what ampersand does, but what about !
? Intuition suggests that it
should have something to do with negation, but there’s no variable to negate
here. Or is there?
$ true ; echo $?
0
$ false ; echo $?
1
That’s right: each command returns an integer called an exit code, and we can negate it with exclamation mark.
$ ! true ; echo $?
1
$ ! false ; echo $?
0
As you probably figured out already, $?
variable holds exit code of the last
executed command.
So picture just got clearer: ampersand sends a process into background, while
exclamation mark negates exit code of the following command. Except that in the
original line, there was no following command. So what does that !
operator
negate, then?
$ echo $?
0
$ !
$ echo $?
1
$ !
$ echo $?
1
Without argument, negation returns positive integer, indicating false. Logic suggests that empty command should return 0:
% bash -c '' ; echo $?
0
Good.
So we effectively established that in out case exclamation mark does nothing and can be safely ignored. But why, then, EasyTag won’t die when we exit the shell that started it?
This one is simple: it’s because I’m ZSH user :)
When I fire up a terminal, an instance of ZSH starts up automatically. I then
run bash
inside of it. That means that ZSH forks and runs bash
in the child
process, waiting for it to finish. I then run easytag
inside of BASH, which
again means forking and waiting. As I use &
at the end of the command, EasyTag
is being put into foreground, returning control to me and thus enabling me to
exit shell — which I do.
At this point, a lot of interesting things happen. First, as I close bash
, its
children (only one in this case, i.e. EasyTag) are being left orphans. init
,
the first process, adopts them. That means that EasyTag is no longer tied to
bash
instance from which it originated. There’s no connection to ZSH either,
so I can exit it too — EasyTag would keep running right until init
kills it
(probably upon shutdown) or user quits it.
As for bash
instance that I quit, its exit code is collected by ZSH.
This pattern is called double forking and was described in Stevens’ “Advanced
Programming in the UNIX Environment” long time ago. It is used to spawn
processes that should run independently of their parents — exactly what disown
does.
And that is the solution to the whole mystery of the test where BASH that has
no &! somehow disowns commands when &!
is used.
Summary
Both BASH and ZSH have a nice builtin called disown
. It enables user to detach
process from the shell in a way much similar to that of nohup
. ZSH has a nice
sugar for it: you can put &!
at the end of the command, and it would be
detached. BASH doesn’t have such sugar, but in my tests the same &!
worked
nonetheless. It confused me at first, but after a little investigation it turned
out to be a two-faced problem.
First of all, BASH parses &!
not as a single operator but as a two separate
ones. &
puts a job into background, while !
negates exit code of the next
command. When there’s no argument to negation operator, it simply returns 1
(false). If one doesn’t care about exit codes, &!
is equivalent to simple &
.
Second, I tested this while running bash inside ZSH, as the latter is my default shell. By chance, that led me to performing an old systems programming trick known as a “double fork” (first described in W. Richard Stevens’ “Advanced Programming in the UNIX Environment”). That means that I effectively disowned by hand.
So with a little effort, I yet again ensure that I’m sane, machines are fine and world hasn’t gone crazy. Till next time, then!
Update (04.05.2013): rewrite a few sentences.
Your thoughts are welcome by email
(here’s why my blog doesn’t have a comments form)