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

Mik*_*nen 30 performance shell-script posix

许多人使用 oneliners 和包含沿线代码的脚本

cat "$MYFILE" | command1 | command2 > "$OUTPUT"
Run Code Online (Sandbox Code Playgroud)

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

< "$MYFILE" command1 | command2 > "$OUTPUT"
Run Code Online (Sandbox Code Playgroud)

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

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

Kus*_*nda 52

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

修复cat脚本中“无用使用”的实例主要是手动修复脚本的源代码。像ShellCheck这样的工具可以通过指出明显的情况来帮助解决这个问题:

$ cat script.sh
#!/bin/sh
cat file | cat
Run Code Online (Sandbox Code Playgroud)
$ shellcheck script.sh

In script.sh line 2:
cat file | cat
    ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead.
Run Code Online (Sandbox Code Playgroud)

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

外壳不一定知道是什么cat。它可能是来自你的任何地方的任何命令$PATH,或者是一个函数。

如果它是一个内置的命令(它可能在某些贝壳),其要重组的管道,因为它会知道它的内置语义的能力cat命令。在此之前,它还必须对管道中的下一个命令做出假设,在原始cat.

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

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

  • @MichaelHomer 是的。但也允许重载具有同名函数的标准命令。 (4认同)
  • @MichaelHomer 正如其他评论所说,当然,shell 完全符合*知道给定 OP 的输入,如果不执行它,就不可能知道 `cat` 命令实际上做了什么*。你(和 shell)都知道,OP 在她的路径中有一个命令 `cat`,它是一个交互式的猫模拟,“myfile”只是存储的游戏状态,而 `command1` 和 `command2` 正在对一些统计数据进行后处理关于当前的播放会话... (3认同)
  • @PhilipCouling 只要知道管道命令都不关心它,它就绝对符合要求。shell 特别允许用内置函数或 shell 函数替换实用程序,并且它们没有执行环境限制,因此只要外部结果无法区分,就允许使用。对于您的情况,`cat /dev/tty` 是有趣的,它 * 将 * 与 `&lt;` 不同。 (2认同)

mos*_*svy 36

因为不是没用。

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

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

  • 不能以有意义的方式select(2)编辑或poll(2)编辑常规文件;它select(2)上的a将始终返回“就绪”。像epoll(2)Linux这样的高级接口根本无法处理常规文件。

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

由于cat使用如此之多,它可以作为内置的 shell 来实现,这将避免额外的进程,但是一旦你开始使用这条路径,大多数命令都可以完成同样的事情——将 shell 转换成一个更慢、更笨拙的程序perlpython。使用易于使用的类似管道的语法来编写另一种脚本语言来代替延续可能会更好;-)

[1] 如果你想要一个不适合这种场合的简单例子,你可以看看我的“来自标准输入的exec二进制文件”git gist,并在此处的评论中做一些解释。cat在它内部实施以使其在没有 UUoC 的情况下工作会使它大 2 到 3 倍。

  • `cat /dev/urandom | cpu_bound_program` 在单独的进程中运行 `read()` 系统调用。例如,在 Linux 上,生成更多随机数(当池为空时)的实际 CPU 工作是在该系统调用中完成的,因此使用单独的进程可以让您利用单独的 CPU 内核来生成随机数据作为输入。例如在[生成包含随机数字的 1 GB 文本文件的最快方法是什么?](//unix.stackexchange.com/posts/comments/571509) (4认同)
  • 更重要的是,在大多数情况下,这意味着 `lseek` 将不起作用。`猫foo.mp4 | mpv -` 会工作,但你不能向后寻找比 mpv 或 mplayer 的缓存缓冲区更远的地方。但是通过从文件重定向输入,您可以。`猫| mpv -` 是一种检查 MP4 在文件开头是否有它的 `moov` 原子的方法,因此它可以在不寻找结尾和返回的情况下播放(即它是否适合流式传输)。很容易想象其他情况,您希望通过在 /dev/stdin 上使用 `cat` 与重定向来测试不可搜索文件的程序。 (4认同)
  • 实际上,ksh93 **确实**在内部实现了一些外部命令,例如`cat`。 (2认同)

UKM*_*key 26

这 2 个命令是不等价的:考虑错误处理:

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

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

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


Jos*_*hua 17

因为检测无用的猫真的很难。

我有一个我写的shell脚本

cat | (somecommand <<!
...
/proc/self/fd/3
...
!) 0<&3
Run Code Online (Sandbox Code Playgroud)

如果cat由于通过su -c 'script.sh' someuser. 显然是多余的cat导致标准输入的所有者更改为正在运行脚本的用户,以便通过/proc工作重新打开它。


der*_*ert 13

tl; dr:炮弹不会自动执行此操作,因为成本超过了可能的收益。

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

  1. 实现cat为内置,仍然保留文件与管道的区别。这将节省 exec 的成本,可能还有一个 fork。
  2. 对管道进行全面分析,了解用于查看文件/管道是否重要的​​各种命令,然后根据该信息采取行动。

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

  1. 在任何一种情况下,避免 exec (of cat)
  2. 在第二种情况下,当重定向替换是可能的时,避免分叉。
  3. 在那里你必须使用管道的情况下,它可能是可能有时避免叉/ vfork的,但往往没有。那是因为 cat 等效项需要与管道的其余部分同时运行。

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

接下来你问:成本是多少。想到的两个成本是 (a) shell 中的额外代码,这增加了它的大小(因此可能会使用内存),需要更多的维护工作,是另一个错误点等;和 (b) 向后兼容性惊喜,POSIXcat省略了许多功能,例如 GNU coreutils cat,所以你必须小心cat内置函数将实现什么。

  1. 额外的内置选项可能还不错——在已经存在一堆的地方再添加一个内置选项。如果您有分析数据显示它会有所帮助,您可能可以说服您最喜欢的 shell 的作者添加它。

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

后记:你可以对你对 cat 的无用使用应用类似的分析。优点:更易于阅读(尽管如果 command1 将文件作为参数,则可能不会)。成本:额外的 fork 和 exec(如果 command1 可以将文件作为参数,可能会导致更混乱的错误消息)。如果您的分析告诉您无用地使用 cat,那么继续。


roa*_*ima 11

cat命令可以接受-作为stdin的标记。(POSIX,“如果文件是 '-',则 cat 实用程序应从序列中那个点的标准输入读取。 ”)这允许简单处理文件或标准输入,否则将不允许这样做。

考虑这两个微不足道的选择,其中 shell 参数$1-

cat "$1" | nl    # Works completely transparently
nl < "$1"        # Fails with 'bash: -: No such file or directory'
Run Code Online (Sandbox Code Playgroud)

另一个cat有用的地方是故意将其用作 no-op 只是为了维护 shell 语法:

file="$1"
reader=cat
[[ $file =~ \.gz$ ]] && reader=zcat
[[ $file =~ \.bz2$ ]] && reader=bzcat
"$reader" "$file"
Run Code Online (Sandbox Code Playgroud)

最后,我相信 UUOC 可以真正被正确调用的唯一时间cat是与已知为常规文件(即不是设备或命名管道)的文件名一起使用,并且没有为命令提供标志:

cat file.txt
Run Code Online (Sandbox Code Playgroud)

在任何其他情况下,cat可能需要其本身的操作。


TSJ*_*117 6

cat 命令可以执行shell 不一定能执行的操作(或者至少,不能轻松执行)。例如,假设您要打印可能不可见的字符,例如制表符、回车符或换行符。*可能*有一种方法可以只使用shell内置命令来做到这一点,但我想不出任何办法。GNU 版本的 cat 可以使用-A参数或-v -E -T参数(尽管我不知道其他版本的 cat)。您还可以使用行号为每一行添加前缀-n(如果非 GNU 版本可以这样做,则再次使用IDK)。

cat 的另一个优点是可以轻松读取多个文件。为此,只需键入cat file1 file2 file3。用 shell 做同样的事情,事情会变得棘手,尽管精心设计的循环很可能达到相同的结果。也就是说,当存在如此简单的替代方案时,您真的想花时间编写这样的循环吗?我不!

使用 cat 读取文件可能比 shell 使用更少的 CPU,因为 cat 是一个预编译程序(明显的例外是任何具有内置 cat 的 shell)。在读取大量文件时,这可能会变得很明显,但我从未在我的机器上这样做过,所以我不能确定。

cat 命令也可用于强制命令在可能不接受的情况下接受标准输入。考虑以下:

echo 8 | sleep

“sleep”命令将不接受数字“8”,因为它从来没有真正打算接受标准输入。因此, sleep 将忽略该输入,抱怨缺乏参数,然后退出。但是,如果一种类型:

echo 8 | sleep $(cat)

许多 shell 会将此扩展为sleep 8,并且 sleep 会在退出前等待 8 秒。你也可以用 ssh 做类似的事情:

command | ssh 1.2.3.4 'cat >> example-file'

此命令在机器上附加示例文件,地址为 1.2.3.4 以及从“命令”输出的任何内容。

这(可能)只是触及表面。如果我愿意,我相信我可以找到更多关于 cat 有用的例子,但这篇文章已经足够长了。所以,我最后要说的是:要求 shell 预测所有这些场景(以及其他几个场景)并不是真正可行的。