![Bash 重用进程替换文件](https://linux22.com/image/17230/Bash%20%E9%87%8D%E7%94%A8%E8%BF%9B%E7%A8%8B%E6%9B%BF%E6%8D%A2%E6%96%87%E4%BB%B6.png)
我有一个大脚本,它接受一个文件作为输入并用它做各种事情。这是一个测试版本:
echo "cat: $1"
cat $1
echo "grep: $1"
grep hello $1
echo "sed: $1"
sed 's/hello/world/g' $1
我希望我的脚本能够使用进程替换,但只有第一个命令 ( cat
) 有效,而其余命令则无效。我认为这是因为它是一个管道。
$ myscript.sh <(echo hello)
应该打印:
cat: /dev/fd/63
hello
grep: /dev/fd/63
hello
sed: /dev/fd/63
world
这可能吗?
答案1
该<(…)
构造创建一个管道。管道通过类似 的文件名传递/dev/fd/63
,但这是一种特殊类型的文件:打开它实际上意味着复制文件描述符 63。(请参阅结尾这个答案以获得更多解释。)
从管道读取是一种破坏性操作:一旦捕获了一个字节,就无法将其扔回去。因此您的脚本需要保存管道的输出。您可以使用临时文件(如果输入较大则首选)或变量(如果输入较小则首选)。使用临时文件:
tmp=$(mktemp)
cat <"$1" >"$tmp"
cat <"$tmp"
grep hello <"$tmp"
sed 's/hello/world/g' <"$tmp"
rm -f "$tmp"
(您可以将两个调用合并为cat
as tee <"$1" -- "$tmp"
。)使用变量:
tmp=$(cat)
printf "%s\n"
printf "%s\n" "$tmp" | grep hello
printf "%s\n" "$tmp" | sed 's/hello/world/g'
请注意,命令替换$(…)
会截断命令输出末尾的所有换行符。为了避免这种情况,请添加一个额外的字符,然后再将其删除。
tmp=$(cat; echo a); tmp=${tmp%a}
printf "%s\n"
printf "%s\n" "$tmp" | grep hello
printf "%s\n" "$tmp" | sed 's/hello/world/g'
顺便一提,不要忘记变量替换周围的双引号。
答案2
当您使用一个文件时,您可以多次读取其数据。当您使用命名管道(实际上是通过进程替换创建的)时,您只能读取它一次。因此grep
和sed
命令接收空输入。
(如何理解管道可能是一本很好的读物。)
为了实现进程替换的目的,您可以编写如下内容:
cat $1 | tee >(echo "cat: $1"; cat) | tee >(echo "grep: $1"; grep hello) | (echo "sed: $1"; sed 's/hello/world/g')
但在这种情况下,第二个cat
、grep
和sed
将并行运行,并且它们的输出交错。这可能更有用:
cat $1 | tee >(cat > cat.txt) | tee >(grep hello > grep.txt) | sed 's/hello/world/g' > sed.txt
答案3
通常的方法是使$1
参数可选。然后,可以多次定义FILE=${1-/dev/stdin}
和使用。FILE
然而,在管道上多次读取将按顺序读取,数据不会重复。
解决此问题的最简单的方法是使用一些临时文件。
if [ -z "$1" ] ; then FILE=$(mktemp); cat >FILE; else FILE=$1; fi
如果您希望显式传递一些文件名(最终/dev/fd/x
),可以使用相同的临时文件技巧:
FILE=$(mktemp); cat "$1" >FILE
您还可以复杂地使用tee
将输入从 stdin 文件描述符复制到其他几个文件描述符。但最后这个方法会相当繁重。
答案4
通过进程替换获得的 I 文件是不可查找的,具体取决于底层实现,因此您不能多次读取它。