如果在 bash 中使用 while 读取循环进行文本处理很糟糕……那我该怎么办?

gab*_*abt 16 shell bash shell-script

我想这可能是一个幼稚的问题,但我无法理解,所以我想问......我正在寻找问题的一些解决方案,当我发现这篇非常有趣的帖子关于为什么[while|for]在 bash 中使用循环被考虑不好的做法。帖子中有一个很好的解释(请参阅所选答案),但我找不到任何可以解决所讨论问题的内容。

我进行了广泛的搜索:我用谷歌搜索(或duckduckgo-ed)how to read a file in bash并且我得到的所有结果都指向一个解决方案,根据上述帖子,该解决方案绝对是非 bash 风格并且应该避免的东西。特别是,我们有这个:

while read line; do
  echo $line | cut -c3
done
Run Code Online (Sandbox Code Playgroud)

和这个:

for line in `cat file`; do
  foo=`echo $line | awk '{print $2}'`
  echo whatever $foo
done
Run Code Online (Sandbox Code Playgroud)

这被认为是非常糟糕的 shell 脚本示例。在这一点上,我想知道,这是实际的问题:如果应该避免发布的 while 循环,因为它们是不好的做法等等……我应该怎么做?

编辑:我看到我已经有评论/问题来解决while循环的确切问题,所以我想扩大一下问题。基本上,我的理解是我需要更深入地研究 bash 命令,这才是我真正应该做的事情。但是,当人们四处搜索时,在一般情况下,人们似乎以不正确的方式使用和教授 bash(根据我的 google-ing)。

ter*_*don 36

您链接到的帖子的重点是解释使用 bash 解析文本文件通常是一个坏主意。它不是专门关于使用循环的,并且在其他上下文中 shell 循环没有本质上的错误。没有人说 shell 脚本while有点糟糕。另一篇文章说你不应该尝试使用 shell 解析文本文件,而应该使用其他工具。

澄清一下,当我说“使用外壳”时,我的意思是使用外壳的内部工具打开文件、提取数据并解析它。例如这样的事情:

while read number; do
  if [ $number -gt 10 ]; then
    echo "The number '$number' is greater than 10"
  else
    echo "The number '$number' is less than or equal to 10"
done < numbers.txt
Run Code Online (Sandbox Code Playgroud)

请阅读为什么使用 shell 循环来处理被认为是不好的做法的文本?有关为什么这种事情是个坏主意的详细信息。在这里,我只想澄清一下,那篇文章一般不是反对 shell 循环,而是反对使用 shell 循环(或 shell)来解析文件。

您没有找到使用 bash 执行此操作的更好方法的建议的原因是,没有使用 bash 或任何其他 shell 执行此操作的好方法。无论您做什么,使用 shell 解析文本都会缓慢、繁琐且容易出错。

Shell 主要设计为一种输入要由计算机运行的命令的方式。它们可以用作脚本语言,但同样,它们在给定命令运行时处于最佳状态,而不是用于代替旨在处理文本解析的命令时。

Shell 是工具,就像任何其他工具一样,它们应该用于其设计目的。问题是很多人都学了一点shell脚本,所以他们有了一个工具,一个“锤子”。因为他们只知道一把锤子,所以他们遇到的每个问题对他们来说都像是钉子,他们试图用锤子敲击这颗钉子。可悲的是,解析文本不是 shell 设计用来处理的,它不是“钉子”,所以使用“锤子”并不是一个好主意。

因此,“我应该如何在 bash 中读取文件”的答案非常简单“您不应该使用 bash,而应使用适合该工作的工具”。

  • 我发现这个答案令人困惑并且有点误导。`cut -c3 &lt; file` 和 `while read line; 做回声 $line | 切-c 3; done` 是 bash 代码的两个示例(bash 是一个 shell,一个旨在运行其他工具的工具),第一个运行一个文本实用程序的调用,第二个运行 2 个笨拙工具的 3 次错误调用和文本处理循环中文件每一行的实用程序。Shell 旨在运行一些擅长处理文本的工具,这里讨论的是如何以及哪些工具以及如何,而不是是否应该使用 bash 来调用它们。 (6认同)
  • @StéphaneChazelas 抱歉,哪个答案?这个?OP 误解了你的答案 [here](https://unix.stackexchange.com/questions/169716/why-is-using-a-shell-loop-to-process-text-thinked-bad-practice/169720)说使用 shell 循环通常是不好的。我澄清说不,你的回答的重点是使用 shell 来解析文本文件是一个坏主意,循环不是问题。当然,使用 shell 调用 _other_ 工具是可以的。 (4认同)
  • 对不起,我的意思是“这个”答案(你的)。问题不在于使用 shell 来处理文本,而是以这种方式使用 shell(循环调用许多笨重工具的调用,这里是错误的),而不是一次调用文本处理工具(或在管道中协作的多个工具) )。 (3认同)
  • @StéphaneChazelas 是的。也完全使用贝壳。我的意思是像 `if [[ $line =~ $regex ]]` 之类的东西,以及 `while read` 等的陷阱。使用 shell 调用可以更好地处理这类事情的其他工具要好得多。我正在向想要使用 shell 作为脚本语言编写解析器的人致辞,而不是使用诸如 awk 或 sed 或 perl 或 python 之类的东西。 即使在最好的情况下,正如您在答案中解释得很好,shell 方法与其他任何东西相比都会慢得可怕。 (3认同)
  • @terdon:StéphaneChazelas 反对您使用诸如“使用 bash 解析文本文件”之类的短语的方式:您使用它们来表示“while read line”;做回声 $line | 切-c 3; done` 与类似 `cut -c3 &lt; file` 的东西相反,但他认为这样的短语*不*区分这些例子,因为两者都是使用 Bash 和 `cut` 的例子。他并不反对你的观点,其中一个是坏主意,其中一个是好主意,他只是反对你用来表达这一点的措辞。 (3认同)

cas*_*cas 13

不要使用为每一行调用一次的 shellwhilefor循环,只需运行 awk 一次,将文件名作为参数。例如awk

awk '{print "whatever " $2}' file
Run Code Online (Sandbox Code Playgroud)

cut

cut -c3 file
Run Code Online (Sandbox Code Playgroud)

如果您需要在 bash 中对 awk 返回的每一行进行进一步处理,最好的选择是使用命令替换来填充数组。

myarray=( $(awk '{print $2}' file) )
Run Code Online (Sandbox Code Playgroud)

重要的是不要在这里用双引号引用命令替换,因为我们希望shell 发生分词——数组的每个元素都是一个“词”,并且由于 awk 的输入是空格分隔的,它只打印一个字段,它每行将输出一个“单词”。

或者,您可以将 bash 内置readarrayakamapfile进程替换一起使用:

mapfile -t myarray < <(awk '{print $2}' file)
Run Code Online (Sandbox Code Playgroud)

mapfile/readarray如果输入包含glob模式就像是必需的变种*在2 $,否则shell会尝试展开水珠。

将数据放入数组后,您可以使用 for 循环对其进行迭代,例如:

for i in "${myarray[@]}"; do do_something_with "$i"; done
Run Code Online (Sandbox Code Playgroud)

或将其作为 args 传递给另一个程序或内置程序:

printf "whatever %s\n" "${myarray[@]}"
Run Code Online (Sandbox Code Playgroud)

但是请注意,在 awk 中进行任何额外处理几乎总是更好。这可能意味着重新设计和重新编写您的 bash 脚本,以便大部分工作在 awk 中完成。或者,如果事实证明不需要 bash,则将整个内容重写为 awk 脚本。perl 也是如此。和蟒蛇。和其他语言。

shell 是一种很好的语言,用于编排其他程序来处理数据和执行实际工作,但在处理数据本身方面却很糟糕——几乎任何其他语言都比 shell 处理数据更好。

如果您发现自己在 shell 和 awk 或其他语言之间来回移动数据,这是一个好兆头,表明您需要用 awk(或其他语言)重写整个过程。

  • @gabt shell 是一个很好的操作文件的工具,是的。通常不是它们的内容,而是文件本身(想想复制、移动、组织到目录等)。但它是_解析_文件或对字符串或字段执行任何操作的可怕工具。 (9认同)
  • 此外,虽然 terdon 所说的关于 bash 非常适合操作文件的说法是正确的,但大多数 bash 也会通过获取其他程序(如 `mv`、`cp`、`tar` 以及更多,包括 `grep`)来实现这一点。 、`cut`、`awk`、`perl`等等等等)来做实际的工作。bash 的首要任务是通过命令行参数或通过设置从一个到另一个的管道将数据提供给其他程序。 (6认同)
  • 没错。循环无关紧要,关键是 _shell_ 不是正确的工具。Grep、cut、awk、perl、sed 等不是 shell 的一部分。而且,正如 cas 正确指出的那样,`mv`、`cp` 和它们的同类都不是。关键在于,正如 cas 所说的那样,shell 旨在编排其他程序来处理数据和执行实际工作,而不擅长执行工作本身。 (4认同)
  • grep、cut 和 tr ** 是专为执行特定工作而设计的**工具。例如,您使用 grep 而不是编写 bash while-read 循环来对文件的每一行进行正则表达式或固定字符串匹配。`grep 模式文件` 是这样做的方法,**不是** `while read line ; do [[ $line =~ pattern ]] &amp;&amp; echo $line ; 完成&lt;文件` (3认同)
  • @gabt - 是的,terdon 说的。 (2认同)

Aus*_*arn 13

在您的示例中要避免的不是循环,而是多次调用命令的无意义使用。碰巧的是,循环是 shell 脚本中命令无用调用的最常见原因之一(另一个重要原因是不记得只使用重定向)。

启动一个新进程是几乎所有系统上可能的最昂贵的操作之一,因此高效的脚本(以及通常的高效代码)将进程总数保持在最低限度。这种效率限制是为什么inetd已经失宠的很大一部分原因,以及为什么许多 Web 服务器默认启动一堆长期存在的进程并根据需要将连接传递给它们,而不是按需为每个连接生成一个进程。

您的两个示例都可以简化为为整个操作启动单个流程。因此,第一个将成为:

cut -c3
Run Code Online (Sandbox Code Playgroud)

第二个是:

awk '{print $2}'` < file
Run Code Online (Sandbox Code Playgroud)

这些不仅效率更高,而且可读性更强。

这并不是说循环一般是不好的,只是你可能在其他语言中使用它的很多东西在 shell 脚本中不需要它,因为所涉及的工具固有地处理多行或文件。的东西一个很好的例子这是有效的使用它在做什么(假设“东西”不支持固有的重试)被处理多次尝试的。

  • @gabt 我个人的建议是使用 Python。它的设计目的是比 PERL 更通用,比 shell 脚本强大得多,而且比两者都更容易学习。 (3认同)
  • 所以,这里的想法是原始问题中的`while` 循环为每个循环调用许多命令,即`grep` 或`echo`...。虽然(!),如果我使用 `awk` 然后将它的输出通过管道传输到 `grep`、`sed` 或其他任何东西,这会更有效率(以及可读性和一切)。 (2认同)
  • @gabt 基本上是的,无论是在涉及的进程数量方面,还是在内存使用方面(尽管这不太清楚,而且实际上取决于许多其他因素)。一般来说,几乎所有传统的 UNIX 命令行工具都设计为一次处理整个文件,逐行操作,因此在 shell 脚本中需要循环来实际执行此类操作的情况相对较少. (2认同)
  • @gabt 您还可以考虑编写一个 `perl` 脚本,该脚本在循环中逐行读取文件,执行与 `sed`、`awk` 或其他任何操作相同的转换,但是 _using perl 内部函数而不是通过调用外部程序_并将修改后的行写回输出文件。Perl 是一种专为处理文本文件而设计的语言。如果需要,您还可以从 Perl 中调用系统命令,例如 `cp`。 (2认同)

小智 5

其他答案是正确的,我只是想知道他们所传达的概念是否很难理解。所以我想给你一个比喻。希望这是有道理的:

您可以将 shell 脚本视为导体。指挥指向不同的音乐家,指示他们应该开始或停止演奏,演奏得更大声或更轻柔,更快或更慢。指挥本身并没有下到管弦乐队开始演奏乐器;指挥也不会通过告诉音乐家如何翻页来“微观管理”音乐家。指挥相信音乐家可以自己完成这项工作。指挥只是指导音乐家何时演奏以及如何演奏。

你的shell脚本是这样的。它正在编排许多其他命令。它根本不直接操作文件。即使您使用像mv或 之类的命令cp,这些命令也正在执行实际工作。他们就像音乐家。shell 脚本对mv“音乐家”说——把它移到这里——它就这样做了。shell 脚本不会移动文件本身。它让mv命令来做。

此外,就像指挥不会告诉音乐家何时翻页一样,shell 脚本不需要将文件一次一行地提供给命令。它可以将整个文件交给命令并告诉它如何处理它。因此不需要循环。循环不应该用于对命令进行微观管理,它应该用于在多个文件上编排多个命令。

不要忘记,shell 脚本中的“命令”与其他语言中的命令不同——它们不是构成 shell 脚本语言的关键字。相反,它们都是可以从 shell 运行的独立程序。cp,例如是它自己的程序。该程序复制文件。它有自己的手册和可以传递给它的参数列表。您可以cp从 shell(在脚本之外)运行,而您所做的只是调用一个名为cp. 以同样的方式,您可以创建自己的程序(通过创建 shell 脚本)并从您的 shell 脚本中调用它们。

希望有助于解释一下。