在bash
脚本中,我想将times
内置函数的输出分配给数组变量,但我发现没有比
tempnam=/tmp/aaa_$$_$RANDOM
times > ${tempnam}
mapfile -t times_a < ${tempnam}
我将输出写入临时文件并在数组times_a中读回,因为管道或$(times)
将在子shell中执行并返回错误的值。
没有临时文件的更好的解决方案吗?
答案 0 :(得分:6)
您需要解决的基本问题是如何同时执行time
和变量赋值,而不需要临时文件。几乎每个方法Bash都提供了一个东西到另一个东西的输出,或者捕获命令的输出,让一方在子shell中工作。
这是一种没有临时文件就可以做到的方法,但我会警告你,它不漂亮,它不能移植到其他shell,它至少需要Bash 4:
coproc co { cat; }; times 1>&${co[1]}; eval "exec ${co[1]}>&-"; mapfile -tu ${co[0]} times_a
我会为你解决这个问题:
coproc co { cat; }
这会创建一个协同进程;在后台运行的进程,但是您可以使用管道与其标准输入和标准输出进行通信,这些FD是${co[0]}
(标准cat
}和${co[1]}
(标准)在cat
)。命令在子shell中执行,因此我们不能在那里执行任何一个目标(运行times
或读入变量),但我们可以使用cat
简单地将输入传递给输出,然后使用该管道与当前shell中的times
和mapfile
进行通信。
times >&${co[1]};
运行times
,将其标准重定向到cat
命令的标准。
eval "exec ${co[1]}>&-"
关闭cat
命令的输入端。如果我们不这样做,cat
将继续等待输入,保持其输出打开,mapfile
将继续等待,导致您的shell挂起。 exec
,当没有命令传递时,只是将其重定向应用于当前shell;重定向到-
会关闭FD。我们需要使用eval
,因为Bash似乎与exec ${co[1]}>&-
有问题,将FD解释为命令而不是重定向的一部分;使用eval
允许首先替换该变量,然后执行。
mapfile -tu ${co[0]} times_a
最后,我们实际上从协同处理中读取了标准中的数据。我们设法在这个shell中运行times
和mapfile
命令,并且没有使用临时文件,尽管我们确实使用临时进程作为两个命令之间的管道。
请注意,这是一场微妙的比赛。如果逐个执行这些命令,而不是全部作为一个命令执行,则最后一个命令失败;因为当你关闭cat
标准时,它会退出,导致协同进程退出并关闭FD。看来,当在一行上执行时,mapfile
执行得足够快,以便协同进程在运行时仍然打开,因此它可以从管道中读取;但我可能会很幸运。我还没有找到一个很好的解决方法。
总而言之,写出临时文件要简单得多。我会使用mktemp
生成文件名,如果你在脚本中,添加一个陷阱以确保在退出之前清理你的临时文件:
tempnam=$(mktemp)
trap "rm '$tempnam'" EXIT
times > ${tempnam}
mapfile -t times_a < ${tempnam}
答案 1 :(得分:2)
Brian's answer使我对这个问题非常感兴趣,从而导致了没有竞争条件的解决方案:
coproc cat;
times >&${COPROC[1]};
{
exec {COPROC[1]}>&-;
mapfile -t times_a;
} <&${COPROC[0]};
这在底层结构上与Brian的解决方案非常相似,但是存在一些关键差异,以确保不会因计时问题而发生滑稽的事情。如Brian所述,他的解决方案通常是有效的,因为bash解释器在完全关闭并清除协处理器的文件描述符之前开始运行mapfile
命令,因此mapfile
之前或期间的任何意外延迟都会破坏它
从本质上讲,关闭协处理器的stdout
文件描述符后,协处理器的stdin
文件描述符会立即关闭。我们需要一种方法来保留协同处理的stdout
。
在the man pages for pipe中,我们发现:
如果所有引用管道读取端的文件描述符都已关闭,则write(2)将导致为调用过程生成SIGPIPE信号。
因此,我们必须为协同进程的stdout
保留至少一个文件描述符。这很容易完成,Redirections。我们可以执行类似exec 3<&${COPROC[0]}-
的操作,将协同处理的stdout
文件描述符移动到新创建的fd 3
1 中。当协同进程终止时,我们将为其stdout
保留一个文件描述符,并能够从中读取文件。
现在,我们可以执行以下操作:
创建一个仅执行cat
的协同处理。
coproc cat;
将times
的{{1}}重定向到协同进程的stdout
。
stdin
获取协同进程的times >&${COPROC[1]};
文件描述符的临时副本。
关闭协同进程的stdout
文件描述符。 (如果使用Bash-4.3之前的版本,则可以使用Brian使用的stdin
技巧。)
eval
从我们的临时文件描述符中读取一个变量。
exec 3<&${COPROC[0]} {COPROC[1]}>&-;
关闭我们的临时文件描述符(不是必需的,但是是一种很好的做法)。
mapfile -tu 3 times_a;
我们完成了!但是,我们仍然有一些重组的机会,可以使事情变得更加整洁。由于重定向语法的本质,此代码:
exec 3<&-;
行为与以下代码相同:
coproc cat;
times >&${COPROC[1]};
exec 3<&${COPROC[0]} {COPROC[1]}>&-;
mapfile -tu 3 times_a;
exec 3<&-;
从这里,我们可以完全删除临时文件描述符,从而找到解决方案:
重击4.3 +:
coproc cat;
times >&${COPROC[1]};
{
exec {COPROC[1]}>&-;
mapfile -tu 3 times_a;
} 3<&${COPROC[0]};
Bash 4.0 +:
coproc cat; times >&${COPROC[1]}; { exec {COPROC[1]}>&-; mapfile -t times_a; } <&${COPROC[0]}
1 我们不需要在这里关闭原始文件描述符,而可以复制它,因为当stdin描述符关闭时原始文件也将关闭
答案 2 :(得分:1)
改进就是使用mktemp,这样你就不会依赖随机性来保持文件的唯一性。
TMPFILE=$(mktemp aaa_XXXXXXXXXX)
times > "$TMPFILE"
mapfile -t times_a < ${tempnam}
rm "$TMPFILE"
另外,我使用for而不是mapfile(因为我没有mapfile)。
a=0; for var in $(cat "$TMPFILE"); do ((a++)); TIMES_A[$a]=$var; done
但是,是的,我没有看到没有文件或命名管道你怎么做。
答案 3 :(得分:0)
做类似事情的一种可能性是
times > >(other_command)
这是一个与进程替换相结合的输出重定向。这种方式times
在当前shell中执行,输出重定向到新的子进程。因此,使用mapfile
执行此操作没有多大意义,因为该命令不会在同一个shell中执行,这可能不是您想要的。这种情况有点棘手,因为你不能调用在子shell中执行的shell内置函数。