子 shell 关闭后,子 shell 内的重定向仍然存在

子 shell 关闭后,子 shell 内的重定向仍然存在

没有和有空运行时,下面的脚本分别按 A 和 B 的预期执行。 A 的空运行时出现问题:输出被抑制,就好像1>/dev/null在空运行后仍然存在一样。有人可以解释并解决问题吗?

function baz() {
    local file="$1"; shift

    # dry run
    local err=$(source "$file" "$@" 2>&1 1>/dev/null)
    [[ -z $err ]] || { echo "Exiting"; return 1; }

    # live run
    local output=$(source "$file" "$@")
    [[ -z $output ]] || echo "$output"
}

baz <(cat <<EOF
echo "I was given $# argument(s):"  # A
# eecho "I was given $# argument(s):" # B
printf "%s " "$@"
EOF
) 'foo' 'bar'

其他:

$ uname  -a
6.7.3-arch1-2 #1 SMP PREEMPT_DYNAMIC Fri, 02 Feb 2024 17:03:55 +0000 x86_64 GNU/Linux
$ bash --version
GNU bash, version 5.2.26(1)-release (x86_64-pc-linux-gnu)

答案1

您的代码中有两个单独的问题:

  1. 您忘记了引号,EOF因此在解析此处文档时$#/将在此处文档中展开。$@
  2. 您尝试从命名管道(又名 fifo)读取两次直到文件末尾

对于 1,比较:

$ bash --norc -s {1..42}
bash-5.3$ cat << EOF
> echo "$#"
> EOF
echo "42"
bash-5.3$ cat << 'EOF'
> echo "$#"
> EOF
echo "$#"

引用heredoc的分隔符(这里是with,'EOF'但你也可以只引用它的一部分,就像相同的效果\EOF一样'E'OF)会导致内部的扩展不被执行。

对于 2,一个更简单的重现器是:

myfunction() {
  local file="$1"
  stat -Lc '$file (%n) is a %F.' -- "$file"
  echo First round:
  cat -- "$file"
  echo Second round:
  cat -- "$file"
  echo Done.
}
myfunction <(echo some text)

(这里假设 的 GNU 或类似 GNU 的实现stat)。

这使:

bash-5.3$ myfunction <(echo some text)
$file (/dev/fd/63) is a fifo.
First round:
some text
Second round:
Done.

<(cmd)扩展为命名管道或在具有/dev/fd.并且您只能读取一次管道的内容。

In cmd1 <(cmd2),cmd1cmd2像 in 一样同时运行cmd2 | cmd1,区别在于, in cmd2 | cmd1,cmd2的输出可以在cmd1的 fd 0 上轻松获得,而 in cmd1 <(cmd2),cmd1需要打开其第一个参数(扩展到上面的/dev/fd/63那个<(cmd2))以获取读取输出的 fd从。

无论哪种情况,一旦cmd2读取了 的输出,就无法再次读取。

使用旧版本的 bash(5.0 或更低版本),您可能会逃脱:

bash-5.0$ myfunction /dev/fd/3 3<< 'EOF'
> echo "$#"
> EOF
$file (/dev/fd/3) is a regular file.
First round:
echo "$#"
Second round:
echo "$#"
Done.

那时,here-documents仍然被执行为已删除 常规文件就像在原始的 Bourne shell 实现中一样,或者就像它们仍然在其他 shell(例如 zsh)中一样。在较新的版本中,当here-doc足够小以至于可以使用管道而不会出现死锁时,bash已切换为使用管道:

bash-5.3$ myfunction /dev/fd/3 3<< 'EOF'
> echo "$#"
> EOF
$file (/dev/fd/3) is a fifo.
First round:
echo "$#"
Second round:
Done.
bash-5.3$ myfunction /dev/fd/3 3<< EOF | grep -e 9999 -e '[[:alpha:]]'
> $(seq 40000)
> EOF
$file (/dev/fd/3) is a regular file.
First round:
9999
19999
29999
39999
Second round:
9999
19999
29999
39999
Done.

为了能够多次读取该文件,您需要它是常规的文件,所以选项是

  • 切换到zsh例如并使用该/dev/fd/3 3<< 'EOF'方法

  • 切换到zsh并使用其=(...)进程替换形式,该形式使用临时文件(并负责清理本身)而不是 fifo(以及cmd2cmd1中顺序运行cmd1 =(cmd2)):

    zsh% myfunction =(<<'EOF'
    cmdsubst¹ heredoc> echo "$#"
    cmdsubst¹ heredoc> EOF
    cmdsubst¹> )
    $file (/tmp/zsh7xSwLQ) is a regular file.
    First round:
    echo "$#"
    Second round:
    echo "$#"
    Done.
    
  • mktemp或者,如果您必须使用 bash,请使用例如在具有 bash 的系统(目前大多数)上手动进行临时文件处理。这可以通过提前删除它来完成,就像原始的此处文档一样,这样您就不必担心清理问题:

    file=$(mktemp) || exit
    <<'EOF' cat > "$file"
    echo "$#"
    EOF
    {
      rm -f -- "$file" && myfunction /dev/fd/3
    } 3< "$file"
    

    虽然这种方法仅适用于 Linux 或 Cygwin,但/dev/fd/n它们在哪里神奇的符号链接到原始文件。在其他系统上,打开的/dev/fd/n行为就像dup(n)这样,每次打开文件时它不会移回到文件的开头。

或者在您的情况下,您可以使用eval代替source并传递代码以在内存中而不是文件/fifos中运行:

myfunction() {
  local code="$1"; shift
  echo First round:
  eval -- "$code"
  echo Second round:
  eval -- "$code"
}

myfunction "$(cat <<'EOF'
echo "I got $# argument${2+s}${1+: $@}"
EOF
)" more args

注意$(...)命令替换(需要加引号以防止 split+glob(仅在 zsh 中分割))而不是<(...)/=(...)进程替换。

这使:

First round:
I got 2 arguments: more args
Second round:
I got 2 arguments: more args

注意到尽管这cmdsubst可能表明什么, 那是一个流程替代在这里,不命令替换

答案2

下面的部分创建一个套接字管道流描述符,而不是文件描述符:

cat <<EOF
...
EOF

第一次读取该流时(在该# dry run部分中),描述符的读取指针到达文件结尾。

# live run部分仅看到 EOF(即文件结尾,而不是字符串EOF),因此该部分中实际上没有运行任何内容# live run

在这种情况下我通常使用的解决方案是使用mktemp如下:

TS="$(mktemp /tmp/subscript-XXXXXX.sh)"

cat >$TS <__EOF
...
__EOF

bash -x $TS fee fi fo fum

rm -f $TS

编辑:正如OP所指出的, 的用法mktemp非常灵活,不需要遵循我上面的确切示例。该示例是从我的一个用例中选取的,并不是针对 OP 的场景量身定制的。

答案3

该函数需要一个文件,但在示例中,它被传递了<(cat << EOF ...),换句话说,是进程替换中的此处文档。正如关于后者所指出的,“第一次读取该流时,描述符的读取指针到达文件末尾。”因此,为了容纳此类文件对象,解决方案是将其内容复制到临时文件,然后使用它:

script.sh:

function baz() {
    local file="$1"; shift

    temp=$(mktemp)
    trap '{ rm -f "$temp"; }' EXIT ERR
    cat "$file" > "$temp"

    # dry run
    local err=$(source "$temp" "$@" 2>&1 1>/dev/null)
    [[ -z $err ]] || { echo "Exiting"; return 1; }

    # live run
    local output=$(source "$temp" "$@")
    [[ -z $output ]] || echo "$output"
}

baz <(cat << 'EOF'
# echo "I was given $# argument(s):"  # A
eecho "I was given $# argument(s):" # B
printf "%s " "$@"
EOF
) 'foo' 'bar'

在A下:

$ source script.sh 'foo' 'bar'
I was given 2 argument(s):
foo bar 

B下:

$ source script.sh 'foo' 'bar´
Exiting

额外的:

  • 如前所述,“如果[前导] EOF 周围没有引号,则在解析此处文档时,位置参数会在此处展开​​。”已相应更正。

相关内容