How does bash know about its parent's coprocess in this situation, and why does a shebang line change it?

outer.sh:

ls -l /proc/$$/exe
coproc cat
./inner.sh
kill $!

inner.sh:

ls -l /proc/$$/exe
set | grep COPROC || echo No match found
coproc cat
kill $!

When I run ./outer.sh, this gets printed:

lrwxrwxrwx 1 joe joe 0 Jun 16 22:47 /proc/147876/exe -> /bin/bash
lrwxrwxrwx 1 joe joe 0 Jun 16 22:47 /proc/147879/exe -> /bin/bash
No match found
./inner.sh: line 3: warning: execute_coproc: coproc [147878:COPROC] still exists

Since COPROC and COPROC_PID aren’t set in the child, how does it know about the one from the parent to be able to give me that warning?

Also, I discovered that if I add #!/bin/bash to the top of inner.sh, or if I call bash ./inner.sh instead of just ./inner.sh from outer.sh, then the warning goes away. Why does this change anything, since it’s getting ran with a bash subprocess either way?

A script without shebang is meant to be interpreted by a POSIX-compliant sh interpreter. That’s actually the POSIX way to write POSIX scripts, POSIX doesn’t specify shebangs, though in practice using shebangs is more portable / reliable, and here is a good example why.

The bash shell is such a POSIX sh interpreter. bash (some versions and in some custom builds and in some environments) is actually the only FLOSS shell that I know that has been certified as compliant when running as sh (not when running as bash).

When executing a shebang-less script, bash, when execve() returns ENOEXEC and after having checked that it doesn’t look like a binary file, interprets it in a child of his, simulating an execution by attempting to reset its state to the default.

That means however that that script when run from bash is interpreted as a bash script instead of a POSIX sh script unless bash is running in POSIX mode itself (such as when invoked as sh itself).

$ cat a
alias uname='echo hi'
uname
$ zsh -c ./a
hi
$ sh ./a
hi
$ bash -c ./a
Linux
$ (exec -a sh bash -c ./a)
hi

See how a was interpreted as bash language (ignoring aliases) instead of the sh languages when invoked by bash.

~$ strace -qqfe execve bash -c ./a
execve("/usr/bin/bash", ["bash", "-c", "./a"], 0x7fff0081a820 /* 66 vars */) = 0
execve("./a", ["./a"], 0x55b18b3a4660 /* 66 vars */) = -1 ENOEXEC (Exec format error)
[pid 123559] execve("/usr/bin/uname", ["uname"], 0x55b18b3a4660 /* 66 vars */) = 0
Linux
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=123559, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---

See how bash didn’t execute sh to interpret the script.

The fact that you get that warning: execute_coproc: coproc [147878:COPROC] still exists is a bug whereby bash fails to reset its state properly.

In any case, coproc is not a sh keyword so doesn’t have its place in a shebang-less script. coproc is from zsh (while coprocesses are from ksh), though bash‘s implementation is completely different, so you should have a #! /bin/bash - shebang here.

With bash ./inner.sh or with a shebang, there is a proper execution of a new interpreter instance, and execve() completely and correctly wipes the process memory.

Answered By: Stéphane Chazelas
Categories: Answers Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.