我编写了以下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
退出。
结论:尽管它们开始的时间大致相同,cat
但uniq
实际上绝不会工作同时。
考虑到这一观察,我们可能希望在管道中的工具开始读取数据时生成一条消息。这是有道理的。作为概念验证,我编写了一个可以做到这一点的脚本。
脚本
#!/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
(来自moreutils
Debian 中的软件包)。代码必须是可执行脚本(我的意思是不要尝试使其成为 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
,第一个grep
和sort
实际上是同时工作的。生成较少的数据(例如40000
或4000
代替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 是否为空,脚本都会运行所需的命令;为此我需要ifne
和ifne -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'