如何使长管道与回声一起工作

如何使长管道与回声一起工作

我编写了以下for循环作为更大脚本的一部分,该脚本以文本文件作为参数,并对它们应用多种转换:

for i; do
echo "Filtering out lines with empty columns...";
awk -F: '$1!="" && $2!=""' "$i" > "${i%.txt}_noempties.txt";
echo "Removing all spaces in first column...";
awk -F: '{gsub(/ /, "", $1); print $1 FS $2}' "${i%.txt}_noempties.txt" > "${i%.txt}_nospaces.txt"; # assumes two columns delimited by :
echo "Filtering out lines that don't contain "@" or ":"..."; 
grep -aE ":|@" "${i%.txt}_nospaces.txt" > "${i%.txt}_sanitised.txt"; 
echo "Removing control characters...";
tr -d '\000-\011\013-\037' < "${i%.txt}_sanitised.txt" > "${i%.txt}_noctrls.txt"; # Removes all control characters apart from (Linux) newline
echo "Filtering out very short and long lines...";
awk 'length >=7 && length <=150' "${i%.txt}_noctrls.txt" > "${i%.txt}_lengths.txt";
echo "Sorting and removing duplicate lines...";
LC_ALL=C sort -u "${i%.txt}_lengths.txt" > "${i%.txt}_final.txt";
done;

它能工作但似乎效率很低,需要等待前一个命令的输出才能将其用作下一个命令的输入。我想完全停止使用中间文件并将整个东西转换为管道,但是当我使用类似这样的方法执行此操作时:

for i; do
echo "Filtering out lines with empty columns..." |
awk -F: '$1!="" && $2!=""' "$i" |
echo "Removing all spaces in first column..." |
awk -F: '{gsub(/ /, "", $1); print $1 FS $2}' | # assumes two columns delimited by :
echo "Filtering out lines that don't contain "@" or ":"..." |
grep -aE ":|@" |
echo "Removing control characters..." |
tr -d '\000-\011\013-\037' | # Removes all control characters apart from (Linux) newline
echo "Filtering out very short and long lines..." |
awk 'length >=7 && length <=150' |
echo "Sorting and removing duplicate lines..." |
LC_ALL=C sort -u > "${i%.txt}_final.txt";
done;

...脚本立即退出。它不会显示任何语法错误,bash -n但将其粘贴到壳牌检测echo给出有关不允许管道插入的错误。

同样的情况也发生在这个:

for i; do
echo "Filtering out lines with empty columns..." &&
awk -F: '$1!="" && $2!=""' "$i" |
echo "Removing all spaces in first column..." &&
awk -F: '{gsub(/ /, "", $1); print $1 FS $2}' | # assumes two columns delimited by :
echo "Filtering out lines that don't contain "@" or ":"..." && 
grep -aE ":|@" |
echo "Removing control characters..." &&
tr -d '\000-\011\013-\037' | # Removes all control characters apart from (Linux) newline
echo "Filtering out very short and long lines..." &&
awk 'length >=7 && length <=150' |
echo "Sorting and removing duplicate lines..." &&
LC_ALL=C sort -u > "${i%.txt}_final.txt";
done;

是否有可能将此代码转换为一个长管道同时保留 s echo?如果可以,如何操作?

答案1

分析

保留简单的 s 是没有意义的echo。管道的各个部分同时运行,而不是按顺序运行,因此即使您设法让每个部分都使用 向终端报告其任务的开始echo(这可以做到),所有回显都会在管道启动后立即报告,并且它们可能无法保持顺序。根据它们的打印方式和数量,它们的输出甚至可能会交错。示例:

{ echo "starting date" >&2; date; } | { echo "starting wc" >&2; wc -l; }

运行此行几次。您会发现 secho可以按任何顺序打印。echo "Doing everything..."在管道之前调用将以更清晰的方式为您提供相同的信息。

但仍然有一些情况是同时启动的工具实际上是按顺序工作的。考虑这个人造的例子(毫无用处cat,但这只是一个例子):

cat some_file | sort | uniq

如果some_file足够小然后退出sort时可能会一次性获取cat。出于同样的原因,退出uniq时可能会获取数据sort。在这种情况下,工具按顺序执行各自的工作,没有并发性。

如果some_file很大,sort将在退出之前开始读取、积累(并可能预处理)数据cat。这两个工具将并行工作一段时间。cat完成工作后,它会退出,然后sort才能生成输出并将其传递给uniq。这意味着uniq必须坐在那里等待,无法读取任何内容,直到cat退出。

结论:尽管它们开始的时间大致相同,catuniq实际上绝不会工作同时。

考虑到这一观察,我们可能希望在管道中的工具开始读取数据时生成一条消息。这是有道理的。作为概念验证,我编写了一个可以做到这一点的脚本。


脚本

#!/bin/bash

if [ "$1" = "-X" ]; then
   marker=1
   fd="$2"
   shift 2
else
   unset marker
   exec {fd}>&1
fi

if [ "$1" = "-m" ]; then
   message="$2"
   shift 2
else
   message="$1 ($$)"
fi

if [ "$marker" ] || [ -t 0 ]; then
   >&2 printf -- '+ %s\n' "$message"
   ( >&"$fd" "$@" )
   status="$?"
   >&2 printf -- '- %s: done [%s]\n' "$message" "$status"
else
   ifne -n "$0" -X "$fd" -m "$message" "$@" | ifne "$0" -X "$fd" -m "$message" "$@"
fi

它用ifne(来自moreutilsDebian 中的软件包)。代码必须是可执行脚本(我的意思是不要尝试使其成为 shell 函数),因为它通过调用自身ifne(并且ifne无法运行函数)。

从现在开始,我假设该脚本位于某处$PATH,其名称是report


用法

基本用法:

$ report echo foo | report wc -l
+ echo (17956)
- echo (17956): done [0]
+ wc (17957)
1
- wc (17957): done [0]

这意味着echo工作并以退出状态退出0。然后wc工作并以退出状态退出0。括号中的数字是各个report进程的 PID(注意:不是echo也不是wc);它们旨在区分同一工具的多个实例(如果管道中出现多个实例)。

可以观察到实际并发的更高级示例:

$ report seq 1 100000 | report grep 9 | report sort -R | report head -n 50 | report grep 00 | report wc -l
+ seq (20602)
+ grep (20603)
+ sort (20604)
- seq (20602): done [0]
- grep (20603): done [0]
+ head (20605)
- head (20605): done [0]
- sort (20604): done [141]
+ grep (20606)
- grep (20606): done [1]
+ wc (20607)
0
- wc (20607): done [0]

(退出状态为grep (20606)1因为偶然grep发现没有匹配项;wc -l确认这一点后,它会打印0。如果运行管道几次,可能会出现匹配项。随机性来自sort -R。)

观察结果:

  • seq,第一个grepsort实际上是同时工作的。生成较少的数据(例如400004000代替100000),您将能够观察到更多连续的行为。
  • head在 之后开始处理数据sort(显然,因为它从 读取sort),但它在 之前终止sort。然后sort退出141 因为它得到SIGPIPE。这是预料之中的,这就是管道的工作方式。

report接受自定义消息。然后让我们完善我们的管道:

$ report -m 'generating data' seq 1 100000 |
  report -m  filtering        grep 9       |
  report -m  sorting          sort -R      |
  report -m  reducing         head -n 50   |
  report -m 'finding 00s'     grep 00      |
  report -m  counting         wc -l
+ generating data
+ filtering
+ sorting
- generating data: done [0]
- filtering: done [0]
+ reducing
- reducing: done [0]
- sorting: done [141]
+ finding 00s
- finding 00s: done [0]
+ counting
1
- counting: done [0]

-m message是唯一的选择。(嗯,有,-X但仅供内部使用。)


粗略解释

该脚本 (滥用) 使用ifne。此工具运行某些操作,当且仅当其 stdin 不为空;或者它 ( ifne -n) 运行某些操作,当且仅当其 stdin 为空。要知道其 stdin 是否为空,它必须等待一些数据或 EOF。这使得它在我们的例子中很有用:… | report foo用于ifne延迟运行,foo直到有数据或 EOF。请注意,它确实会延迟foo,因此在某些(相当奇特?)情况下,它的行为可能与 不完全相同… | foo

例外情况是当 stdin 是终端时(使用 检测[ -t 0 ])。report cat在交互式 shell 中运行,您会看到它不会等待数据,它会立即报告cat正在运行。我认为这是一种合理的方法。

无论 stdin 是否为空,脚本都会运行所需的命令;为此我需要ifneifne -n。困难的部分是最终通过两种变体将 stdin 传递给所需的命令,而命令的输出不应到达ifne。额外的文件描述符使这成为可能。事实证明,当report调用自身(通过ifne)而不是调用bash -c 'some obscure code that handles the redirection' …via时,代码更优雅ifne


笔记:

  • 消息被打印到 stderr。来自不同report进程的消息可以交错但如果它们不是太长,那就真的不应该。坦率地说,我不知道“太长”是多长,我认为“相当短”应该是“不太长”。:) 无论如何,如果有问题,应该是微不足道的。

  • trap ':' INT TERM在 shebang 之后使用一个陷阱,使其report不受Ctrl+C和普通 的影响kill。这个想法是Ctrl+C终止内部命令并report仍然报告这一点。陷阱使用':',而不是'',因为后者会使脚本spawned 命令会忽略指定的信号,而前者只会影响脚本。我个人更喜欢report没有 trap 的。

  • report能跑

    • 外部可执行文件,
    • 从 Bash 导出的 shell 函数
    • 或 Bash 内置命令。如果是内置命令,请记住内置命令不能影响您当前的 shell,因为代码report在单独的 shell 中运行。此外,内置命令还report故意"$@"在另一个 shell(子 shell)中运行,因此内置命令也不能影响report自身。
  • 如果您使用这个技巧alias report='report ';它们之所以能工作,并不是因为report会突然理解它们,而是因为调用的 shellreport会先扩展它们。

  • for要在单个命令中运行管道(或任何需要 shell 的东西,例如循环) report,您需要明确调用其他 shell。在 shell 中,您可以运行更多report命令。尝试以下示例:

    report seq 1 100000 |
    report -m preprocessing sh -c 'report grep 9  | report sort -R | report head -n 50' |
    report -m    processing sh -c 'report grep 00 | report wc -l'
    

相关内容