【问题标题】:Saving values in BASH shell variables while using |tee使用 |tee 时将值保存在 BASH shell 变量中
【发布时间】:2021-11-19 04:25:29
【问题描述】:

我正在尝试计算一个非常大的文件中的行匹配数,并仅使用 BASH shell 命令将它们存储在变量中。

目前,我正在扫描一个非常大的文件的结果两次,并且每次都使用单独的 grep 语句,如下所示:

$ cat test.txt 
first example line one
first example line two
first example line three
second example line one
second example line two
$ FIRST=$( cat test.txt | grep 'first example'  | wc --lines ; ) ;  ## first run
$ SECOND=$(cat test.txt | grep 'second example' | wc --lines ; ) ;  ## second run

我最终得到了这个:

$ echo $FIRST
3
$ echo $SECOND
2

希望,我只想扫描一次大文件。而且我从来没有使用过 Awk 并且宁愿不使用它!

|tee 选项对我来说是新的。似乎将结果传递给两个单独的 grep 语句可能意味着我们只需要扫描一次大文件。

理想情况下,我也希望能够做到这一点,而不必创建任何临时文件,并且随后必须记住删除它们。

我尝试了多种方法,如下所示:

FIRST=''; SECOND='';
cat  test.txt                                                   \
    |tee  >(FIRST=$( grep 'first example'  | wc --lines ;);)    \
          >(SECOND=$(grep 'second example' | wc --lines ;);)    \
          >/dev/null        ;

并使用read:

FIRST=''; SECOND='';
cat  test.txt                                                       \
   |tee  >(grep 'first example'   | wc --lines | (read FIRST);  );  \
         >(grep 'second example'  | wc --lines | (read SECOND); );  \
         > /dev/null                   ;



cat  test.txt                                                           \
      | tee  <( read FIRST  < <(grep 'first example'  | wc --lines ))   \
             <( read SECOND < <(grep 'sedond example' | wc --lines ))   \
             >    /dev/null             ;

并带有大括号:

FIRST=''; SECOND='';
cat test.txt                                                     \
  |tee   >(FIRST={$( grep 'first example'  | wc --lines ;)} )    \
         >(SECOND={$(grep 'second example' | wc --lines ;)} )    \
         >/dev/null                           ;
   

但这些都不允许我将行数保存到变量 FIRSTSECOND 中。 p>

这有可能吗?

【问题讨论】:

  • "$( cat test.txt | grep 'first example' | wc --lines ; )" 你不需要 cat,只需要 "grep 'first example' test.txt" 你会避免额外的过程
  • 你会找到答案here
  • 为什么不喜欢使用awk?使用一个相对简单的awk 脚本,您可以替换所有当前代码并且只扫描文件一次
  • &gt;(command) 进程替换在子 shell 中运行命令。您在子 shell 中设置的变量不会保留在运行脚本的父 shell 中。抱歉,这种方法无法如您所愿。
  • @edwardsmarkf : tee 不是一个选项,它是一个可执行命令 - 参见 man tee

标签: linux bash shell sh


【解决方案1】:

tee 没有保存任何工作。每个grep 仍将对文件进行全面扫描。无论哪种方式,您都通过了三个文件:两个greps 和一个Useless Use of Cat。事实上tee 实际上只是添加了第四个循环整个文件的程序。

由于一个致命缺陷:variable assignments don't work in pipelines,您尝试的各种 | tee 调用都不起作用。也就是说,它们在为变量赋值的情况下“工作”,只是该值几乎立即丢失。为什么?因为变量在子shell中,而不是在父shell中。

| 管道中的每个命令都在不同的进程中执行,这是 Linux 系统的一个基本事实,即进程彼此隔离并且不共享变量分配。

根据经验,您可以写variable=$(foo | bar | baz),其中变量位于外部。没问题。但是不要尝试foo | variable=$(bar) | baz 它在里面的地方。它不会起作用,你会很难过。

但不要失去希望!有很多方法可以给这只猫剥皮。让我们来看看其中的几个。

两个grep

摆脱cat 产生:

first=$(grep 'first example' test.txt | wc -l)
second=$(grep 'second example' test.txt | wc -l)

这实际上非常好,通常会足够快。 Linux 在 RAM 中维护一个大的页面缓存。每当您读取文件时,Linux 都会将内容存储在内存中。多次读取文件通常会命中缓存而不是磁盘,这非常快。即使是多 GB 文件也可以轻松放入现代计算机的 RAM 中,尤其是当您在缓存页面仍然新鲜的情况下进行背靠背读取时。

一个 grep

您可以通过使用搜索两个字符串的单个 grep 调用来改进这一点。如果您实际上不需要单个计数而只需要总数,它可能会起作用:

total=$(grep -e 'first example' -e 'second example' test.txt | wc -l)

或者如果匹配的行很少,您可以使用它将大文件过滤成一小组匹配行,然后使用原始 grep 提取单独的计数:

matches=$(grep -e 'first example' -e 'second example' test.txt)
first=$(grep 'first example' <<< "$matches" | wc -l)
second=$(grep 'second example' <<< "$matches" | wc -l)

纯 bash

您还可以构建一个仅使用 Bash 的解决方案,该解决方案只执行一次传递并且不调用外部程序。分叉进程很慢,因此仅使用 read[[ 等内置命令可以提供很好的加速。

首先,让我们从while read loop开始逐行处理文件:

while IFS= read -r line; do
   ...
done < test.txt

您可以使用双方括号[[ 和字符串相等== 来计算匹配项,它接受* 通配符:

first=0
second=0

while IFS= read -r line; do
    [[ $line == *'first example'* ]] && ((++first))
    [[ $line == *'second example'* ]] && ((++second))
done < test.txt

echo "$first"   ## should display 3
echo "$second"  ## should display 2

另一种语言

如果这些都不够快,那么您应该考虑使用“真正的”编程语言,例如 Python、Perl,或者,真的,任何您喜欢的语言。 Bash 不是速度恶魔。我喜欢它,它确实被低估了,但即使我承认高性能数据处理是not its wheelhouse

【讨论】:

  • 非常有趣。通配符应该是这样的吗? '*first example*' 引号内有星号?
  • 我知道,这看起来很有趣。如果它们被引用,那么它们就是字面星号。它们需要不加引号才能被视为通配符。
  • “我知道,它看起来很有趣。” - 我可以说整个操作系统。 “伯克利最著名的两个产品是 LSD 和 Unix。我认为这不是巧合。”
  • 最后一个愚蠢的问题 - 如果不是直接从 test.txt 读取,我实际上想使用来自另一个命令(例如 find)的结果会发生什么?我尝试更改:done &lt; $(cat test.txt;),但出现“模糊重定向”错误。
  • 使用process substitution:done &lt; &lt;(cat test.txt)
【解决方案2】:

如果你打算做这样的事情,我真的建议你熟悉awk;这并不可怕,而且 IMO 与您正在查看的怪异管道配件相比,用它来做这样的复杂事情要容易得多。这是一个简单的awk 程序,可以同时计算两种模式的出现次数:

awk '/first example/ {first++}; /second example/ {second++}; END {print first, second}' test.txt

解释:/first example/ {first++} 表示对于匹配正则表达式模式“第一个示例”的每一行,增加 first 变量。 /second example/ {second++} 对第二种模式执行相同的操作。然后END {print first second} 表示最后应该打印两个变量。很简单。

但是有一件棘手的事情:将它打印的两个数字分成两个不同的变量。你可以用read来做到这一点:

bothcounts=$(awk '/first example/ {first++}; /second example/ {second++}; END {print first, second}' test.txt)
read first second <<<"$bothcounts"

(注意:我建议使用小写或混合大小写的变量名,以避免与许多具有特殊功能的全大写名称冲突。)

另一种选择是跳过bothcounts 变量,方法是使用进程替换将awk 的输出直接馈送到read

read first second < <(awk '/first example/ {first++}; /second example/ {second++}; END {print first, second}' test.txt)

【讨论】:

  • 这是我要使用的解决方案。很有趣,我最初打算在回答中提到它,但想,“不,我不想讨论如何从一个命令中提取两个值。”然后我的回答迅速膨胀成一本查尔斯狄更斯的小说,我忘了绕回去把它放进去。
  • @JohnKugelman 我想知道你为什么没有报道它。顺便说一句,感谢您的编辑;我仔细发现并修复了该问题,然后仔细复制并粘贴了错误的版本。哦!
  • 恐怕我不会经常做这样的事情来学习 awk。我曾经使用 perl,直到世界似乎远离它,我厌倦了等待 perl-6(就像等待共和党总统候选人纳税申报表一样)
【解决方案3】:

">" 是关于重定向到文件/设备,而不是管道中的下一个命令。所以 tee 只会允许你将管道重定向到多个文件,而不是多个命令。 所以试试这个:

FIRST=$(grep 'first example' test.txt| wc --lines)
SECOND=$(grep 'second example' test.txt| wc --lines)

【讨论】:

  • &gt;(command) 实际上并不是重定向,而是process substitution
  • ">" 是重定向,我认为 $(command) 是进程替换,请纠正我
  • $(command)command substitution&lt;(command)&gt;(command)process substitution。 (我承认,其中一个或两个名字都不好。)
【解决方案4】:

可以在单次传递中获取匹配项并对其进行计数,然后从结果中获取每个匹配项的计数。

matches="$(grep -e 'first example' -e 'second example' --only-matching test.txt | sort | uniq -c | tr -s ' ')"

FIRST=$(grep -e 'first example' <<<"$matches" | cut -d ' ' -f 2)
echo $FIRST

结果:

3

使用awk 是我认为的最佳选择。

【讨论】:

    猜你喜欢
    • 2019-05-01
    • 2015-10-07
    • 2012-10-27
    • 2011-10-21
    • 2020-05-03
    • 1970-01-01
    • 2011-09-08
    • 2022-10-19
    • 2011-12-26
    相关资源
    最近更新 更多