As far as I know, process substitution <(...) / >(...) creates the fd
and stores the output of commands in parentheses into the generated fd.
Therefore, these two commands are equivalent
$ ls -al
$ cat <(ls -al)
Here, my question is, how long the generated file descriptors remain?
I've read this article, but seems my understanding is wrong.
If a process substitution is expanded as an argument to a function, expanded to an environment variable during calling of a function, or expanded to any assignment within a function, the process substitution will be "held open" for use by any command within the function or its callees, until the function in which it was set returns. If the same variable is set again within a callee, unless the new variable is local, the previous process substitution is closed and will be unavailable to the caller when the callee returns.
In essence, process substitutions expanded to variables within functions remain open until the function in which the process substitution occured returns - even when assigned to locals that were set by a function's caller. Dynamic scope doesn't protect them from closing.
My best guess, after reading it, was that the created fd will not be closed until it is used.
From this, I wrote a very dumb code like below
#!/bin/bash
test_subs () {
echo "Inside a function"
FD2=<(ls -al)
cat $FD1
cat $FD2
}
FD1=<(ls -al)
test_subs
Result======================================
Inside a function
cat: /dev/fd/63: No such file or directory
cat: /dev/fd/63: No such file or directory
It seems that the newly opened fd close just right after one line of command run.
How long does the generated fd maintained, and then what is the scope of process substitution?
There seems to be no documentation and therefore no guarantee on the scope of process substitution <(...)
. I assume the only safe way to keep process substitutions in scope is to define them directly as arguments cmd <(...)
, on-the-fly exported variables VAR=<(...) cmd
, or redirection cmd < <(...)
. Process substitution defined in this manner remains in scope while cmd
is running.
I interpreted the quoted article from Bash Hackers Wiki like you did. Likewise, I came to the same conclusion that declaring variables for process substitution inside a function does not guarantee that they stay open. On some systems there are many other ways to keep them open, especially with command groups like subshells (...)
and contextes {...}
. However, these tricks still fail on some systems.
I could not find any documentation of this apart from the wrong comments in the linked Bash Hackers Wiki. Even bash's manual does not talk about the scope of process substitution. So we are stuck with experimenting (or reading bash
's source code, which I did not).
The following script creates some scenarios to check when process substitution <(...)
remains in scope. Note that there are very subtle differences. For instance: It makes a difference whether you write two commands in the same line using ;
or each command in its own line. Of course this list is not complete. Feel free to extend it.
#! /usr/bin/env bash
echo 'define, use'
a=<(echo ok);
cat "$a"; unset a
echo 'define and use in same line'
a=<(echo ok); cat "$a"; unset a
echo 'define and use in subshell'
(a=<(echo ok);
cat "$a")
echo 'define and use in context'
{ a=<(echo ok)
cat "$a"; }; unset a
echo 'define and use in && chain'
a=<(echo ok) &&
cat "$a"; unset a
echo 'define in context and use in || chain'
{ a=<(echo ok); false; } || cat "$a"; unset a
echo 'define and use in for loop body'
for i in 1; do
a=<(echo ok)
cat "$a"
done
echo 'define and use in while loop head'
while
a=<(echo ok)
cat "$a"
false
do true; done; unset a
echo 'define and use in same case'
case x in
x)
a=<(echo ok)
cat "$a"
;;
esac; unset a
echo 'define in case, use in fall-through'
case x in
x)
a=<(echo ok)
;&
y)
cat "$a"
;;
esac; unset a
echo 'define and use inside function in same line'
f() { a=<(echo ok); cat "$a"; }; f; unset a f
echo 'define local and use inside function in same line'
f() { local a=<(echo ok); cat "$a"; }; f; unset a f
echo 'define, use as function argument'
f() { cat "$1"; }; a=<(echo ok)
f "$a"; unset a f
echo 'define, use as function argument in same line'
f() { cat "$1"; }; a=<(echo ok); f "$a"; unset a f
echo 'on-the-fly export, use in different shell'
a=<(echo ok) dash -c 'cat "$a"'
echo 'export, use in different shell'
export a=<(echo ok)
dash -c 'cat "$a"'; unset a
echo 'define in command substitution, use in parent in same line'
a=$(echo <(echo ok)); cat "$a"; unset a
echo 'read from here-string, use in parent in same line'
read a <<< <(echo ok); cat "$a"; unset a
echo 'read from process substitution, use in parent in same line'
read a < <(echo <(echo ok)); cat $a; unset a
echo 'read from pipe and use in same line'
shopt -s lastpipe; # TODO add `set +m` when running interactively
echo <(echo ok) | read -r a; cat "$a"
shopt -u lastpipe; unset a
echo 'define, unrelated read from file, use in same line'
a=<(echo ok); read < /etc/passwd; cat "$a"; unset a
echo 'define, unrelated read from process substitution, use in same line'
a=<(echo ok); read < <(echo unused); cat "$a"; unset a
echo 'define, unrelated cat from process substitution, use in same line'
a=<(echo ok); cat <(echo unused) > /dev/null; cat "$a"; unset a
echo 'define, unrelated read ... in subshell, use in same line'
a=<(echo ok); (read < <(echo unused)); cat "$a"; unset a b
echo 'define, unrelated read ... in command substitution, use in same line'
a=<(echo ok); b=$(read < <(echo unused)); cat "$a"; unset a b
# output can be prettified using
# ./script 2> /dev/null |
# awk 'p!="ok"{if($0=="ok")print "yes " p;else print "no " p}{p=$0}'
These are the (prettyfied) outputs for my systems
In scope on bash 5.0.17 on Arch Linux (kernel 5.6.15-arch1-1)
| In scope on bash 5.0.3 on Debian 10 Buster inside WSL 1
| | In scope on bash 4.3.48 on Ubuntu 16.04.6 LTS
↓ ↓ ↓
no no no define, use
yes yes no define and use in same line
yes yes no define and use in subshell
yes yes no define and use in context
yes yes no define and use in && chain
yes yes no define in context and use in || chain
yes yes no define and use in for loop body
yes yes no define and use in while loop head
yes yes no define and use in same case
yes yes no define in case, use in fall-through
no no no define and use inside function in same line
no no no define local and use inside function in same line
no no no define, use as function argument
yes yes no define, use as function argument in same line
yes yes yes on-the-fly export, use in different shell
no no no export, use in different shell
no no no define in command substitution, use in parent in same line
no no no read from here-string, use in parent in same line
no no no read from process substitution, use in parent in same line
no no no read from pipe and use in same line
yes yes no define, unrelated read from file, use in same line
yes no no define, unrelated read from process substitution, use in same line
yes yes no define, unrelated cat from process substitution, use in same line
no no no define, unrelated read ... in subshell, use in same line
yes yes no define, unrelated read ... in command substitution, use in same line
For my interpretation of these results see the TL;DR at the beginning of this answer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With