使用 tee 时如何将多个 shell 命令的输出分配给变量?

How to assign output of multiple shell commmands to variable when using tee?

我想 tee 并从管道中连接的多个 shell 命令中获取结果。我做了一个简单的例子来解释这一点。假设我想计算 'a'、'b' 和 'c'.

的数量
echo "abcaabbcabc" | tee >(tr -dc 'a' | wc -m) >(tr -dc 'b' | wc -m) >(tr -dc 'c' | wc -m) > /dev/null

然后我尝试将每个计数的结果分配给一个 shell 变量,但它们最终都是空的。

echo "abcaabbcabc" | tee >(A=$(tr -dc 'a' | wc -m)) >(B=$(tr -dc 'b' | wc -m)) >(C=$(tr -dc 'c' | wc -m)) > /dev/null && echo $A $B $C

正确的做法是什么?

使用文件。它们是唯一最可靠的解决方案。任何命令可能需要不同的时间 运行。没有简单的方法来同步命令重定向。那么最可靠的方法是使用单独的"entity"来收集所有数据:

tmpa=$(mktemp) tmpb=$(mktemp) tmpc=$(mktemp)
trap 'rm "$tmpa" "$tmpb" "$tmpc"' EXIT

echo "abcaabbcabc" | 
     tee >(tr -dc 'a' | wc -m > "$tmpa") >(tr -dc 'b' | wc -m > "$tmpb") | 
     tr -dc 'c' | wc -m > "$tmpc"
A=$(<"$tmpa")
B=$(<"$tmpb")
C=$(<"$tmpc")

rm "$tmpa" "$tmpb" "$tmpc"
trap '' EXIT

第二种方式:

您可以在每个流的数据前加上自定义前缀。然后对前缀上的所有行(基本上,缓冲它们)进行排序,然后读取它们。示例脚本将从每个进程替换中仅生成一个数字,因此很容易做到:

read -r A B C < <(
  echo "abcaabbcabc" | 
  tee >(
    tr -dc 'a' | wc -m | sed 's/^/A /'
  ) >(
    tr -dc 'b' | wc -m | sed 's/^/B /'
  ) >(
    tr -dc 'c' | wc -m | sed 's/^/C /'
  ) >/dev/null |
  sort |
  cut -d' ' -f2 |
  paste -sd' '
)
echo A="$A" B="$B" C="$C"

使用 flock 的临时文件来同步子进程的输出可能如下所示:

tmpa=$(mktemp) tmpb=$(mktemp) tmpc=$(mktemp)
trap 'rm "$tmpa" "$tmpb" "$tmpc"' EXIT

echo "abcaabbcabc" | 
(
  flock 3
  flock 4
  flock 5

  tee >(
    tr -dc 'a' | wc -m | 
    { sleep 0.1; cat; } > "$tmpa"

    # unblock main thread
    flock -u 3
  ) >(
    tr -dc 'b' | wc -m | 
    { sleep 0.2; cat; } > "$tmpb"

    # unblock main thread
    flock -u 4
  ) >(
    tr -dc 'c' | wc -m | 
    { sleep 0.3; cat; } > "$tmpc"

    # unblock main thread
    flock -u 5
  ) >/dev/null

  # wait for subprocesses to finish
  # need to re-open the files to block on them
  (
    flock 3
    flock 4
    flock 5
  ) 3<"$tmpa" 4<"$tmpb" 5<"$tmpc"
) 3<"$tmpa" 4<"$tmpb" 5<"$tmpc"

A=$(<"$tmpa")
B=$(<"$tmpb")
C=$(<"$tmpc")

declare -p A B C

你可以使用这个特色字母频率分析

#!/usr/bin/env bash

declare -A letter_frequency
while read -r v k; do
    letter_frequency[$k]="$v"
done < <(
  grep -o '[[:alnum:]]' <<<"abcaabbcabc" |
    sort |
      uniq -c
)
for k in "${!letter_frequency[@]}"; do
  printf '%c = %d\n' "$k" "${letter_frequency[$k]}"
done

输出:

c = 3
b = 4
a = 4

或者仅分配 $A$B$C,如您的示例所示:

#!/usr/bin/env bash

{
  read -r A _
  read -r B _
  read -r C _
}< <(
  grep -o '[[:alnum:]]' <<<"abcaabbcabc" |
    sort |
      uniq -c
)
printf 'a=%d\nb=%d\nc=%d\n' "$A" "$B" "$C"
  • grep -o '[[:alnum:]]': 将每个字母数字字符拆分成一行
  • sort: 排序字符行
  • uniq -c:计算每个实例并输出每个实例的计数和字符
  • < <( command group; ):这个命令组的输出是针对之前
  • 命令组的stdin

如果您需要计算不可打印字符、换行符、空格、制表符的出现次数,您必须使所有这些命令输出并处理空分隔列表。使用这些工具的 GNU 版本肯定可以完成。我把它作为练习交给你。


count任意字符除null的解决办法:

如图所示,也适用于 Unicode。

#!/usr/bin/env bash
declare -A character_frequency
declare -i v
while read -d '' -r -N 8 v && read -r -d '' -N 1 k; do
    character_frequency[$k]="$v"
done < <(
  grep --only-matching --null-data . <<<$'a¹bc✓   ✓\n\t\t\u263A☺ ☺   aabbcabc' |
    head --bytes -2 | # trim the newline added by grep
      sort --zero-terminated | # sort null delimited list
        uniq --count --zero-terminated # count occurences of char (null delim)
)
for k in "${!character_frequency[@]}"; do
  printf '%q = %d\n' "$k" "${character_frequency[$k]}"
done

输出:

$'\n' = 1
$'\t' = 2
☺ = 3
\  = 7
✓ = 2
¹ = 1
c = 3
b = 4
a = 4