没有和有空运行时,下面的脚本分别按 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
您的代码中有两个单独的问题:
- 您忘记了引号,
EOF
因此在解析此处文档时$#
/将在此处文档中展开。$@
- 您尝试从命名管道(又名 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)
,cmd1
和cmd2
像 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(以及cmd2
在cmd1
中顺序运行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 周围没有引号,则在解析此处文档时,位置参数会在此处展开。”已相应更正。