为什么Shell无法自动修复“猫的无用使用”?[关闭]


28

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

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

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

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

因为shell只需启动command1,只需将其stdin指向给定文件即可。

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


22
这些命令实际上不是等效的,因为在一种情况下stdin是一个文件,而在另一种情况下则是一个管道,因此这并不是严格安全的转换。不过,您可以创建一个做到这一点的系统。
Michael Homer

14
您无法想象用例并不意味着不允许应用程序无用地依赖于指定的行为。从错误中获取lseek仍然是已定义的行为,并且可能导致不同的结果,不同的阻止行为可能在语义上是有意义的,等等。如果您知道其他命令是什么并且知道它们不在乎,则可以进行更改,或者如果您只是不关心该级别的兼容性,但是好处很小。我确实认为,缺乏收益比合规成本更能推动局势。
Michael Homer

3
cat不过,绝对允许外壳实现自身或任何其他实用程序。还可以知道属于系统的其他实用程序的工作方式(例如,它可以知道系统随附的外部grep实现行为方式)。这是完全可行的,因此想知道为什么他们不这样做是完全公平的。
Michael Homer

6
@MichaelHomer 例如,它可以知道系统附带的外部grep实现的行为, 因此shell现在依赖于的行为grep。和sed。和awk。和du。还有几百个甚至数千个其他实用程序?
Andrew Henle

19
为我编辑命令对我的shell来说是很酷的事情。
Azor Ahai

Answers:


25

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

cat <file that doesn't exist> | less 会产生一个空流,该空流将传递给管道程序...这样,您最终将得到一个不显示任何内容的显示。

< <file that doesn't exist> less 将无法打开栏,然后再完全打开。

尝试将前者更改为后者可能会破坏希望运行带有潜在空白输入的程序的任何脚本。


1
我将您的回答标记为已接受,因为我认为这是两种语法之间最重要的区别。具有的变量cat将始终执行管道中的第二个命令,而具有输入重定向的变量将在输入文件丢失的情况下完全不执行命令。
Mikko Rantalainen

但是,请注意<"missing-file" grep foo | echo 2将不会执行,grep但是会执行echo
Mikko Rantalainen

51

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

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

$ 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自动执行此操作。脚本的执行方式取决于从其父进程继承的环境,并取决于可用外部命令的特定实现。

外壳不一定知道是什么cat。它可能是来自您中任何位置的任何命令,也可能是$PATH函数。

如果它是一个内置命令(可能在某些外壳中),则它将具有重组管道的能力,因为它将知道其内置cat命令的语义。在执行此操作之前,还必须对原始命令之后的管道中的下一个命令进行假设cat

请注意,将标准输入连接到管道和将其连接到文件时,其读取行为会稍有不同。管道是不可搜索的,因此根据管道中的下一条命令的作用,如果重新布置了管道,它的行为可能会或可能不会有所不同(它会检测输入是否可搜索,并决定是否是可输入的,或者是否决定是否做其他事情并非如此,无论如何它的行为都会有所不同)。

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


shell知道是什么cat和管道中的其他命令(按规则)并相应地执行行为,这是完全一致的,因为它们没有意义且太难了,所以它们不在这里。
Michael Homer

4
@MichaelHomer是的。但是,也允许重载具有相同功能的标准命令。
Kusalananda

2
@PhilipCouling只要知道没有管道命令在乎,这是绝对符合的。专门允许外壳程序用内置函数或外壳程序函数替换实用程序,并且这些程序没有执行环境限制,因此,只要外部结果不可区分即可。对于你的情况,cat /dev/tty是有趣的一个是不同的<
Michael Homer

1
@MichaelHomer ,只要外部结果无法区分就可以,这意味着以这种方式优化的整个实用程序集的行为永远不会改变。那一定是最终的依赖地狱。
Andrew Henle

3
@MichaelHomer就像其他评论所说的那样,对于外壳程序来说,知道给定OP的输入,cat如果不执行命令就无法分辨出命令的实际作用,这是完全合规的。就您所知(和外壳程序)而言,OP cat在其路径中有一个命令,它是一个交互式猫模拟,“ myfile”只是存储的游戏状态,command1并且command2正在对有关当前游戏会话的一些统计信息进行后处理...
alephzero

34

因为它不是没有用的。

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

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

  • 常规文件不能以有意义的方式进行select(2)编辑poll(2);一个select(2)关于它总是返回“准备就绪”。像epoll(2)Linux上的高级界面将无法使用常规文件。

  • 在Linux上有系统调用(splice(2)vmsplice(2)tee(2)),其仅在管道上的工作[1]

由于cat已被广泛使用,因此可以将其实现为内置的shell,这样可以避免额外的过程,但是一旦您在该路径上启动,大多数命令都可以完成相同的工作-将shell转换为较慢且笨重的命令perlpython。最好使用易于使用的类似管道的语法编写另一种脚本语言作为延续;-)

[1]如果您不希望为此举一个简单的例子,可以在注释中查看我的“来自stdin的exec二进制文件” git gist并提供一些解释。为了在没有UUoC的情况下在其内部实现,将使其增大2或3倍。cat


2
实际上,ksh93 确实实现了一些外部命令,例如cat内部。
jrw32982支持Monica

3
cat /dev/urandom | cpu_bound_programread()在一个单独的进程中运行系统调用。例如,在Linux上,生成更多随机数(当池为空时)的实际CPU工作是在该系统调用中完成的,因此使用单独的进程可让您利用单独的CPU内核来生成随机数据作为输入。例如,用什么最快的方法来生成包含随机数字的1 GB文本文件?
Peter Cordes

4
更重要的是,在大多数情况下,这意味着lseek将无法使用。 cat foo.mp4 | mpv -可以使用,但是您无法向后搜索mpv或mplayer的缓存缓冲区。但是,通过从文件重定向输入,您可以。 cat | mpv -一种检查MP4是否moov在文件开头的原子的方法,因此可以在不寻求结尾和结尾的情况下播放它(即,是否适合流式传输)。很容易想象在其他情况下,您想通过与重定向/dev/stdin一起运行来测试程序中的非可搜索文件cat
彼得·科德斯

使用时更是如此xargs cat | somecmd。如果文件路径超出命令缓冲区限制,则xargs可以cat多次运行,从而导致连续流,而xargs somecmd直接使用常常失败,因为somecmd无法多次运行以实现无缝结果。
tasket

17

因为检测无用的猫真的非常困难。

我写了一个shell脚本

cat | (somecommand <<!
...
/proc/self/fd/3
...
!) 0<&3

如果将shell脚本cat删除,则该脚本将在生产中失败,因为该脚本是通过调用的su -c 'script.sh' someuser。显然是多余的,cat导致标准输入的所有者将脚本的运行方式更改为用户,以便通过/proc工作重新打开它。


这种情况非常容易,因为它显然没有遵循简单的模型,即仅cat跟随一个参数,因此外壳程序应使用实际的cat可执行文件而不是优化的快捷方式。但是,对于实际过程,可能使用不同的凭据或非标准的stdin是个好方法。
Mikko Rantalainen

13

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

其他答案也指出了stdin是管道和文件之间的技术区别。请记住,shell可以执行以下操作之一:

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

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

  1. 无论哪种情况,都请避免执行(cat
  2. 在第二种情况下,当可以进行重定向替换时,请避免使用fork。
  3. 在那里你必须使用管道的情况下,它可能是可能有时避免叉/ vfork的,但往往没有。这是因为等效于猫的代码需要与其余管道同时运行。

因此,您可以节省一点CPU时间和内存,尤其是在可以避免分叉的情况下。当然,只有在实际使用该功能时,您才保存此时间和内存。而且,您实际上只是节省了fork / exec时间。对于较大的文件,时间主要是I / O时间(即,猫从磁盘读取文件)。因此,您必须问:cat在性能确实很重要的shell脚本中,有多少次(无用)使用?可以将其与其他常见的shell内置程序进行比较test-很难想象cat(无用的)test使用频率是重要位置使用频率的十分之一。我没有测算这是一个猜测,这是您在尝试实施之前要执行的操作。(或类似地,要求其他人在功能请求中实施)。

接下来,您问:费用是多少。我想到的两个成本是:(a)shell中的附加代码,这会增加其大小(并因此可能导致内存使用),需要更多的维护工作,是bug的另一个发源地,等等。(b)向后兼容的惊人之处,POSIX cat省略了GNU coreutils等许多功能cat,因此您必须非常小心cat内置将实现的功能。

  1. 附加的内置选项可能还不错-在已经存在一堆的情况下再添加一个内置选项。如果您有分析数据显示它会有所帮助,则可以说服您最喜欢的Shell的作者添加它。

  2. 至于分析管道,我不认为shell当前会做任何这样的事情(一些人意识到管道的末端并可以避免派生)。本质上,您将在外壳中添加一个(原始)优化器;优化器通常是复杂的代码和许多错误的来源。这些错误可能令人惊讶-Shell脚本中的微小更改可能会避免或触发该错误。

后记:您可以对猫的无用使用进行类似的分析。好处:更易于阅读(尽管如果command1将文件作为参数,可能不会)。成本:额外的fork和exec(如果command1可以将文件作为参数,则可能是更令人困惑的错误消息)。如果您的分析告诉您不要使用cat,请继续。


10

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

考虑这两个简单的选择,其中shell参数$1-

cat "$1" | nl    # Works completely transparently
nl < "$1"        # Fails with 'bash: -: No such file or directory'

另一个cat有用的时间是故意将其用作无操作,只是为了维护shell语法:

file="$1"
reader=cat
[[ $file =~ \.gz$ ]] && reader=zcat
[[ $file =~ \.bz2$ ]] && reader=bzcat
"$reader" "$file"

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

cat file.txt

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


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

数字“ 8”将不会被“睡眠”命令接受,因为它从未真正接受标准输入。因此,睡眠会无视该输入,抱怨缺乏论据,然后退出。但是,如果一种类型:

echo 8 | sleep $(cat)

许多shell会将其扩展为sleep 8,然后睡眠将等待8秒钟,然​​后退出。您还可以使用ssh做类似的事情:

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

该命令在计算机上带有地址为1.2.3.4的附加example-file以及“命令”输出的任何内容。

而且(可能)只是在刮擦表面。我敢肯定,如果我愿意的话,我可以找到更多关于cat的例子,但是这篇文章足够长了。因此,我将以这样的结论来结束:让shell预测所有这些场景(以及其他几种场景)实际上是不可行的。


我将以“不容易可行”结束最后一句话
Basile Starynkevitch

3

请记住,用户可以有一个cat在他$PATH这是不完全POSIX cat(但也许一些变体,它可以登录一些地方)。在这种情况下,您不希望外壳将其删除。

PATH 可以动态改变,那么cat 是不是你认为它是什么。编写一个shell来实现您梦dream以求的优化将是非常困难的。

另外,在实践中,这cat 是一个非常快速的程序。没有什么实际的理由(美学除外)可以避免这种情况。

另请参阅Yann Regis-Gianas在FOSDEM2018上出色的解析POSIX演讲。它提供了其他一些很好的理由来避免尝试在shell中执行您梦dream以求的事情。

如果性能确实是shell的问题,那么有人会提出一个shell,该shell使用复杂的整个程序编译器优化,静态源代码分析和即时编译技术(这三个领域都有数十年的发展和科学出版物,会议,例如SIGPLAN)。令人遗憾的是,即使作为一个有趣的研究主题,该研究目前也不由研究机构或风险资本家资助,而且我推断这根本不值得付出努力。换句话说,优化外壳可能没有重要的市场。如果您有100万欧元可用于此类研究,那么您会很容易找到有人去做,我相信这会带来有价值的结果。

从实际的角度来看,为了提高其性能,通常会使用任何更好的脚本语言(Python,AWK,Guile等)编写一个小的(几百行)shell脚本。而且(出于许多软件工程方面的原因)编写大型的Shell脚本是不合理的:当您编写超过一百行的Shell脚本时,您确实需要考虑使用一些更合适的语言来重写它(即使出于可读性和维护性的考虑)。 :作为一种编程语言,shell是一种非常糟糕的语言。但是,有很多大型的生成的 Shell脚本,并且有充分的理由(例如,GNU autoconf生成的configure脚本)。

对于巨大的文本文件,将它们cat作为单个参数传递不是一个好习惯,并且大多数系统管理员都知道(当运行任何Shell脚本要花费一分钟以上的时间时,您就开始考虑对其进行优化)。对于大型千兆字节的文件,cat从来没有处理它们的好工具。


3
“避免这种情况的实际原因很少”-任何等待cat some-huge-log | tail -n 5运行的人(tail -n 5 some-huge-log可能会直接跳到最后,而cat只能前后读取)会不同意。
Charles Duffy

Comment会检出^ cat数十GB范围内的大型文本文件(该文件是为测试而创建的),需要花费很长时间。不推荐。
Sergiy Kolodyazhnyy

1
顺便说一句,再说一遍:“没有一个优化外壳的重要市场”-ksh93 一个优化外壳,并且是一个相当不错的外壳。一段时间以来,它成功地作为商业产品出售。(可悲的是,获得商业许可还使它成为一个足够的利基市场,以至于编写不佳的克隆和其他能力较差但免费的继任者接管了愿意支付许可费用的站点之外的世界,导致了我们今天)。
查尔斯·达菲

(不使用您注意到的特定技术,但坦率地说,在过程模型中这些技术没有意义;它所应用的技术确实很好地被应用并且效果良好)。
查尔斯·达菲

2

除了@Kusalananda答案(和@alephzero评论)之外,cat可能是任何东西:

alias cat='gcc -c'
cat "$MYFILE" | command1 | command2 > "$OUTPUT"

要么

echo 'echo 1' > /usr/bin/cat
cat "$MYFILE" | command1 | command2 > "$OUTPUT"

没有理由说cat(单独)或系统上的/ usr / bin / cat实际上是cat串联工具。


3
除了catPOSIX定义的行为以外,不应有太大的不同。
roaima

2
@roaima:PATH=/home/Joshua/bin:$PATH cat ...您确定现在知道cat吗?
约书亚

1
@Joshua并不重要。我们俩都知道cat可以被覆盖,但是我们都知道不应该用其他东西代替它。我的评论指出,POSIX强制要求可以合理预期存在的特定(子集)行为。有时,我编写了一个扩展标准实用程序行为的shell脚本。在这种情况下,shell脚本的行为和行为就像它所替换的工具一样,只是它具有其他功能。
roaima

@Joshua:在大多数平台上,shell知道(或可能知道)哪些目录包含实现POSIX命令的可执行文件。因此,您可以将替换操作推迟到别名扩展和路径解析之后再执行,仅对即可/bin/cat。(然后将其cat设为可以关闭的选项。)或者您可以内置一个shell(可能会返回/bin/cat多个args?),以便用户可以控制他们是否希望外部版本正常。方式,与enable cat。喜欢kill。(我当时以为bash command cat可以用,但是不能跳过内置函数)
Peter Cordes

如果提供别名,那么外壳程序将知道cat在该环境中不再引用通常的cat。显然,应该在处理别名之后执行优化。我认为外壳程序内置组件可以代表虚拟目录中的命令,该命令始终位于您的路径之前。如果要避免使用任何命令(例如test)的shell内置版本,则必须使用带有路径的变体。
Mikko Rantalainen

1

猫的两种“无用”用法:

sort file.txt | cat header.txt - footer.txt | less

...这里cat用于混合文件和管道输入。

find . -name '*.info' -type f | sh -c 'xargs cat' | sort

...这里xargs可以接受几乎无限数量的文件名,并可以cat根据需要运行任意多次,同时使其行为都像一个流。因此,这适用于xargs sort不能直接使用的大型文件列表。


如果cat仅使用一个参数调用外壳程序,则仅通过逐步内置外壳程序就可以避免这两种用例。尤其是在sh传递字符串并直接xargs调用的情况下,catshell无法使用它的内置实现。
Mikko Rantalainen

0

除了其他方面,cat-check还会增加额外的性能开销,并混淆使用catIMHO实际上是无用的,因为恕我直言,因为这样的检查可能效率低下,并在合法cat使用方面造成问题。

当命令处理标准流时,它们只需要关心对标准文件描述符的读写。命令可以知道stdin是否可搜索/可搜索,这表示管道或文件。

如果添加到混合检查中,实际上哪个进程提供了该stdin内容,我们将需要在管道的另一端找到该进程并应用适当的优化。如Kyle Jones 在SuperUser帖子中所示,这可以根据外壳本身来完成,也可以根据

(find /proc -type l | xargs ls -l | fgrep 'pipe:[20043922]') 2>/dev/null

如链接文章中所示。这是另外3个命令(因此,额外的fork()s和exec()s)和递归遍历(因此,大量readdir()调用)。

就C和shell源代码而言,shell已经知道子进程,因此不需要递归,但是我们如何知道何时优化以及何时cat实际上无用呢?实际上有cat的有用用法,例如

# adding header and footer to file
( cmd; cat file; cmd ) | cmd
# tr command does not accept files as arguments
cat log1 log2 log3 | tr '[:upper:]' '[:lower:]'

将这样的优化添加到外壳中可能会浪费和不必要的开销。正如Kusalanda的回答中已经提到的那样,UUOC的原因更多是用户自己缺乏对如何最佳组合命令以获得最佳结果的了解。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.