8

在一个bash脚本中,我想将times内置函数的输出分配给一个数组变量,但我发现没有比这更好的方法了

tempnam=/tmp/aaa_$$_$RANDOM
times > ${tempnam}
mapfile -t times_a < ${tempnam}

我将输出写入临时文件并在数组 times_a 中读回它,因为管道或$(times)将在子外壳中执行并返回错误的值。

没有临时文件有更好的解决方案吗?

4

4 回答 4

7

您需要解决的基本问题是如何time在同一个 shell 中同时执行和分配变量,而不需要临时文件。几乎 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]}(standard out of cat) 和${co[1]}(standard in of cat)。命令在子shell中执行,因此我们不能在其中执行任何一个目标(运行times或读取变量),但我们可以使用cat简单地将输入传递到输出,然后使用该管道进行对话到timesmapfile在当前 shell 中。

times >&${co[1]};

运行times,将其标准输出重定向到cat命令的标准输入。

eval "exec ${co[1]}>&-"

关闭命令的输入端cat。如果我们不这样做,cat将继续等待输入,保持其输出打开,并mapfile继续等待,导致您的 shell 挂起。exec,当不传递任何命令时,只需将其重定向应用到当前 shell;重定向到-关闭 FD。我们需要使用eval,因为 Bash 似乎有问题exec ${co[1]}>&-,将 FD 解释为命令而不是重定向的一部分;usingeval允许先替换该变量,然后再执行。

mapfile -tu ${co[0]} times_a

最后,我们实际上从协程中读取了标准中的数据。我们已经设法在这个 shell 中同时运行timesmapfile命令,并且没有使用临时文件,尽管我们确实使用了一个临时进程作为两个命令之间的管道。

请注意,这有一个微妙的比赛。如果你一个一个地执行这些命令,而不是全部作为一个命令,最后一个失败;因为当您关闭cats 标准时,它会退出,导致协进程退出并关闭 FD。看起来,当全部在一行上执行时,mapfile执行速度足够快,以至于协同进程在运行时仍然处于打开状态,因此它可以从管道中读取;但我可能会走运。我还没有想出解决这个问题的好方法。

总而言之,写出临时文件要简单得多。我会mktemp用来生成一个文件名,如果你在一个脚本中,添加一个陷阱以确保你在退出之前清理你的临时文件:

tempnam=$(mktemp)
trap "rm '$tempnam'" EXIT
times > ${tempnam}
mapfile -t times_a < ${tempnam}
于 2012-11-14T01:20:09.207 回答
2

Brian 的回答让我对这个问题非常感兴趣,导致这个解决方案没有竞争条件:

coproc cat;
times >&${COPROC[1]};
{
  exec {COPROC[1]}>&-;
  mapfile -t times_a;
} <&${COPROC[0]};

这在底层结构上与 Brian 的解决方案非常相似,但有一些关键区别确保不会因时间问题而发生有趣的事情。正如 Brian 所说,他的解决方案通常有效,因为 bash 解释器mapfile在协进程的文件描述符完全关闭和清理之前开始运行命令,因此之前或期间的任何意外延迟mapfile都会破坏它。


本质上,stdout在我们关闭协同进程的stdin文件描述符后,协同进程的文件描述符就会关闭。我们需要一种方法来保存协同进程' stdout

pipe 的手册页中,我们发现:

如果所有引用管道读取端的文件描述符都已关闭,则 write(2) 将导致为调用进程生成 SIGPIPE 信号。

因此,我们必须为协进程保留至少一个文件描述符stdout使用Redirections很容易做到这一点。我们可以做一些事情,比如exec 3<&${COPROC[0]}-将协同进程的stdout文件描述符移动到新创建的fd 31。当协同进程终止时,我们仍然拥有它的文件描述符stdout并能够从中读取。


现在,我们可以执行以下操作:

  1. 创建一个除了cat.

    coproc cat;
    
  2. 重定向到协同times进程。stdoutstdin

    times >&${COPROC[1]};
    
  3. stdout获取协同进程文件描述符的临时副本。

  4. 关闭协同进程的stdin文件描述符。(如果使用 Bash-4.3 之前的版本,您可以使用evalBrian 使用的技巧。)

    exec 3<&${COPROC[0]} {COPROC[1]}>&-;
    
  5. 从我们的临时文件描述符中读取到一个变量中。

    mapfile -tu 3 times_a;
    
  6. 关闭我们的临时文件描述符(不是必需的,但很好的做法)。

    exec 3<&-;
    

我们完成了!但是,我们仍然有一些机会进行重组以使事情更整洁。由于重定向语法的性质,这段代码:

coproc cat;
times >&${COPROC[1]};
exec 3<&${COPROC[0]} {COPROC[1]}>&-;
mapfile -tu 3 times_a;
exec 3<&-;

行为与此代码相同:

coproc cat;
times >&${COPROC[1]};
{  
  exec {COPROC[1]}>&-;
  mapfile -tu 3 times_a;
} 3<&${COPROC[0]};

从这里,我们可以完全删除临时文件描述符,从而得到我们的解决方案:

重击 4.3+:

coproc cat; times >&${COPROC[1]}; { exec {COPROC[1]}>&-; mapfile -t times_a; } <&${COPROC[0]}

重击 4.0+:

coproc cat; times >&${COPROC[1]}; { eval "exec ${COPROC[1]}>&-"; mapfile -t times_a; } <&${COPROC[0]}

1我们不需要在此处关闭原始文件描述符,只需复制它即可,因为当标准输入描述符关闭时,原始文件描述符也会关闭

于 2020-07-25T07:55:21.423 回答
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

但是,是的,我看不出没有文件或命名管道如何做到这一点。

于 2012-11-14T00:55:44.060 回答
0

做类似事情的一种可能性是

times > >(other_command)

它是与进程替换相结合的输出重定向。这种方式times在当前 shell 中执行,输出重定向到一个新的子进程。因此,这样做mapfile并没有多大意义,因为该命令不会在同一个 shell 中执行,这可能不是您想要的。这种情况有点棘手,因为您不能调用在子shell中执行的shell内置函数。

于 2012-11-13T23:50:28.240 回答