为什么 shell 不自动修复“cat 无用的使用”?

为什么 shell 不自动修复“cat 无用的使用”?

许多人使用包含代码的单行代码和脚本

cat "$MYFILE" | command1 | command2 > "$OUTPUT"

第一个cat通常被称为“cat 的无用使用”,因为从技术上讲,它需要启动一个新进程(通常/usr/bin/cat),如果命令已执行,则可以避免这种情况。

< "$MYFILE" command1 | command2 > "$OUTPUT"

因为这样 shell 只需要启动command1并简单地将其指向stdin给定的文件。

为什么 shell 不自动执行此转换?我觉得“无用的猫的使用”语法更容易阅读,并且 shell 应该有足够的信息来自动摆脱无用的猫。这cat是在 POSIX 标准中定义的,因此应该允许 shell 在内部实现它,而不是在路径中使用二进制文件。 shell 甚至可以只包含一个参数版本的实现,并回退到路径中的二进制文件。

答案1

“无用的使用cat”更多的是关于如何编写代码,而不是关于执行脚本时实际运行的内容。这是一种设计反模式,一种可能以更有效的方式完成某件事的方法。未能理解如何最好地组合给定工具来创建新工具。我认为,在管道中将多个sed和/或awk命令串在一起有时也可以说是同一反模式的症状。

修复脚本中“无用使用cat”的情况主要是手动修复脚本的源代码。一个工具,例如外壳检查可以通过指出明显的案例来帮助解决这个问题:

$ cat script.sh
#!/bin/sh
cat file | cat
$ shellcheck script.sh

In script.sh line 2:
cat file | cat
    ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead.

由于 shell 脚本的性质,让 shell 自动执行此操作会很困难。脚本的执行方式取决于从其父进程继承的环境以及可用外部命令的具体实现。

shell 不一定知道cat是什么。它可能是任何$PATH来自或函数中任何位置的命令。

如果它是一个内置命令(可能在某些 shell 中),那么它能够重新组织管道,因为它知道其内置cat命令的语义。在此之前,它还必须对管道中原始命令之后的下一个命令做出假设cat

请注意,当连接到管道和连接到文件时,从标准输入读取的行为略有不同。管道是不可搜索的,因此根据管道中下一个命令的作用,如果重新排列管道,它的行为可能会或可能不会有所不同(它可能会检测输入是否可搜索,并决定以不同的方式执行操作,如果是或如果它不是,无论如何它都会表现不同)。

这个问题是类似的(在非常一般意义上)到“是否有编译器尝试自行修复语法错误?“(在软件工程 StackExchange 站点),尽管这个问题显然是关于语法错误,而不是无用的设计模式。不过,根据意图自动更改代码的想法基本上是相同的。

答案2

因为它并非无用。

在 的情况下cat file | cmd,fd 0(stdin)cmd将是一个管道,在 的情况下,cmd <file它可能是常规文件、设备等。

管道与常规文件具有不同的语义,其语义是不是常规文件的子集:

  • 常规文件无法以有意义的方式select(2)编辑或编辑; poll(2)aselect(2)总是会返回“ready”。 Linux等高级界面epoll(2)根本无法处理常规文件。

  • 在 Linux 上,有一些系统调用(splice(2)vmsplice(2)tee(2))仅适用于管道 [1]

由于cat使用率很高,因此可以将其实现为内置 shell,这将避免额外的过程,但是一旦您开始走这条路,大多数命令都可以完成同样的事情 - 将 shell 转换为更慢且更笨重的 shellperl或者python。最好编写另一种具有易于使用的类似管道语法的脚本语言延续反而 ;-)

[1] 如果您想要一个不是临时编造的简单示例,您可以查看我的“exec binary from stdin”git要旨以及评论中的一些解释这里。如果在其内部实现cat,以便在没有 UUoC 的情况下也能正常工作,则其大小会增大 2 到 3 倍。

答案3

这两个命令并不等效:考虑错误处理:

cat <file that doesn't exist> | less将产生一个空流,该流将被传递到管道程序...因此,您最终会看到什么都不显示的显示。

< <file that doesn't exist> less将无法打开酒吧,然后根本不打开。

尝试将前者更改为后者可能会破坏任意数量的希望以可能为空的输入运行程序的脚本。

答案4

长话短说:Shell 不会自动执行此操作,因为成本超过了可能的收益。

其他答案指出了 stdin 作为管道和文件之间的技术差异。记住这一点,shell 可以执行以下操作之一:

  1. cat作为内置实现,仍然保留文件与管道的区别。这将节省执行人员的成本,甚至可能节省分叉的成本。
  2. 了解用于查看文件/管道是否重要的​​各种命令,对管道进行全面分析,然后据此采取行动。

接下来,您必须考虑每种方法的成本和收益。好处很简单:

  1. 无论哪种情况,都要避免 exec (of cat)
  2. 在第二种情况下,当可以进行重定向替换时,可以避免分叉。
  3. 如果您必须使用管道,则可以可能有时可以避免 fork/vfork,但通常不能。这是因为 cat 等效项需要与管道的其余部分同时运行。

因此,您可以节省一点 CPU 时间和内存,特别是如果您可以避免分叉。当然,只有在实际使用该功能时,您才可以节省时间和内存。而且你只是真正节省了 fork/exec 时间;对于较大的文件,时间主要是 I/O 时间(即 cat 从磁盘读取文件)。所以你必须问:cat在性能真正重要的 shell 脚本中使用(无用)的频率如何?将它与其他常见的 shell 内置函数进行比较test——很难想象它的使用频率(无用)甚至是重要地方使用cat频率的十分之一。test这是一个猜测,我还没有测量过,这是你在尝试实施之前想要做的事情。 (或者类似地,要求其他人在例如功能请求中实现。)

接下来你会问:费用是多少。我想到的两个成本是:(a) shell 中的额外代码,这会增加其大小(因此可能会使用内存),需要更多的维护工作,是另一个出现 bug 的地方,等等; (b) 向后兼容性令人惊讶,POSIXcat省略了许多功能,例如 GNU coreutils cat,因此您必须仔细注意cat内置函数将实现什么。

  1. 额外的内置选项可能并没有那么糟糕——在已经存在一堆内置选项的地方再添加一个。如果您的分析数据表明它会有所帮助,您可能可以说服您最喜欢的 shell 的作者添加它。

  2. 至于分析管道,我认为 shell 目前不会做类似的事情(一些 shell 可以识别管道的末端并可以避免分叉)。本质上,您将向 shell 添加一个(原始)优化器;优化器通常会成为复杂的代码,并且是许多错误的根源。这些错误可能会令人惊讶 - shell 脚本中的微小变化最终可能会避免或触发错误。

后记:您可以对 cat 的无用用途应用类似的分析。优点:更容易阅读(尽管如果 command1 将文件作为参数,则可能不会)。成本:额外的 fork 和 exec (如果 command1 可以将文件作为参数,可能会出现更令人困惑的错误消息)。如果您的分析告诉您使用 cat 毫无用处,那么就继续吧。

相关内容