Background process in shell script receives EOF

Viewed 383

It's hard to explain this behavior, so here's a reproducible example (tested on macOS).

First, I have the following C file. The details aren't important, but I essentially use the read system call to read 16 bytes from standard input, or until an EOF is encountered. Note that read will return 0 on an EOF.

// test.c

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    char buf[1];
    for (int i = 0; i < 16; i++) {
        if (read(STDIN_FILENO, buf, 1) == 0) {
            printf("EOF encountered! Number of bytes read: %d\n", i);
            return 0;
        }
    }
    printf("Read all 16 bytes\n");
    return 0;
}

Suppose I compile this file into a binary file called test. Here's the program in action:

$ echo 'sixteen bytes!!' | ./test
Read all 16 bytes

$ echo '' | ./test
EOF encountered! Number of bytes read: 1

$ ./test # Waits for user input

Makes sense right? I can even take the last command and run it as a background process (although this is quite useless):

$ ./test &
[1] 19204

[1]  + suspended (tty input)  ./test

Suppose I take this command and put it in the following shell script (called huh.sh):

#!/bin/sh

./test &

When I run this shell script, here's the output:

$ ./huh.sh

EOF encountered! Number of bytes read: 0

This means read immediately encountered an EOF, and this only happens in the context of a shell script.

I see similar behavior if I replace test with another program that is sensitive to EOFs. For instance, if I instead run node & directly in a terminal, I'll be able to see the node process in the output of ps. However, if I run node & in a shell script, it immediately exits.

Can someone explain this?

2 Answers

In Bash, background commands in a shell script are run differently from background commands typed at the terminal. See the Bash manual on Lists of Commands, where it says:

If a command is terminated by the control operator ā€˜&’, the shell executes the command asynchronously in a subshell. This is known as executing the command in the background, and these are referred to as asynchronous commands. The shell does not wait for the command to finish, and the return status is 0 (true). When job control is not active (see Job Control), the standard input for asynchronous commands, in the absence of any explicit redirections, is redirected from /dev/null.

Job control is not normally active in a script.

Note that this behaviour in Bash is consistent with the requirements of the POSIX shell, as noted by Joseph Sible.

Job control is enabled by default in interactive shells, but disabled by default in shell scripts. Here's a relevant quote from POSIX:

If job control is disabled (see set, -m), the standard input for an asynchronous list, before any explicit redirections are performed, shall be considered to be assigned to a file that has the same properties as /dev/null. This shall not happen if job control is enabled.

Related