Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bash: What is the scope of the process substitution?

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?

like image 572
ruach Avatar asked Oct 10 '17 06:10

ruach


1 Answers

TL;DR

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.

The Long Story

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.

like image 52
Socowi Avatar answered Sep 30 '22 00:09

Socowi