Bash:将'times'内置的输出分配给变量

时间:2012-11-13 22:55:03

标签: bash

bash脚本中,我想将times内置函数的输出分配给数组变量,但我发现没有比

更好的方法了。
tempnam=/tmp/aaa_$$_$RANDOM
times > ${tempnam}
mapfile -t times_a < ${tempnam}

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

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

4 个答案:

答案 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中的timesmapfile进行通信。

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中运行timesmapfile命令,并且没有使用临时文件,尽管我们确实使用临时进程作为两个命令之间的管道。

请注意,这是一场微妙的比赛。如果逐个执行这些命令,而不是全部作为一个命令执行,则最后一个命令失败;因为当你关闭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保留一个文件描述符,并能够从中读取文件。


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

  1. 创建一个仅执行cat的协同处理。

    coproc cat;
    
  2. times的{​​{1}}重定向到协同进程的stdout

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

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

    eval
  5. 从我们的临时文件描述符中读取一个变量。

    exec 3<&${COPROC[0]} {COPROC[1]}>&-;
    
  6. 关闭我们的临时文件描述符(不是必需的,但是是一种很好的做法)。

    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内置函数。