“cat <文件名”和“猫文件名”有什么区别?

Blu*_*ies 28 command-line redirect cat

显示文件内容的最简单方法是使用以下cat命令:

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

我可以使用输入重定向获得相同的结果:

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

那么,它们之间有什么区别呢?

gle*_*man 61

cat file
Run Code Online (Sandbox Code Playgroud)

cat程序将打开,读取和关闭文件。

cat < file
Run Code Online (Sandbox Code Playgroud)

您的 shell 将打开该文件并将内容连接到cat的标准输入。cat识别它没有文件参数,并将从标准输入读取。

  • 外壳将*不*读取它或将任何内容发送到任何地方。它将打开它并将其文件描述符 dup2() 传递给子进程的标准输入。子进程(在本例中为 cat)将从其 stdin 描述符进行读取。 (9认同)
  • 尽管在问题中没有提到,但在对只能由 root 读取的文件执行 `sudo cat` 时,这种差异尤其重要。 (7认同)
  • FWIW,如果参数之一是`-`, cat 也会从 stdin 读取,因此它不是“仅”没有参数的情况。希望没有人对此感到困惑,但是,好吧,它发生了。 (5认同)
  • 此外,您可以为 cat 提供多个文件名,它会尽职尽责地按顺序将它们全部连接起来:`cat file1 file2 ...`——您不能通过重定向来做到这一点。 (4认同)
  • 参数是写在“cat”之后的文件名。如果没有, cat 从 stdin 读取。或许这样就更清楚了。第一个命令有一个文件参数,第二个没有。 (2认同)

Pil*_*ot6 17

从用户的角度来看没有区别。这些命令做同样的事情。

从技术上讲,区别在于打开文件的cat程序:程序或运行它的外壳程序。重定向是由 shell 在运行命令之前设置的。

(所以在其他一些命令中——也就是说,不是问题中显示的命令——可能会有所不同。特别是,如果您无法访问file.txt但 root 用户可以访问,则sudo cat file.txt可以使用但sudo cat < file.txt不可以。)

您可以使用适合您情况的任何一种。

几乎总是有很多方法可以获得相同的结果。

cat接受来自参数的文件,或者stdin如果没有参数。

man cat

SYNOPSIS
       cat [OPTION]... [FILE]...

DESCRIPTION
       Concatenate FILE(s) to standard output.

       With no FILE, or when FILE is -, read standard input.
Run Code Online (Sandbox Code Playgroud)

  • 就输出而言没有区别,但打开文件的方式和人员有所不同。在这种情况下 cat 也可以,但是如果它是一个禁止读取 stdin 的程序 - 它的行为将非常不同 (21认同)
  • 也许是一个类比:在你的睡眠中平静地死去与在很长一段时间内被折磨致死有什么区别?只看前后没有区别。但是如果你在两者之间注意,就会发生很多不同的事情。:D 这种差异在几种情况下都很重要。考虑一个非常常见的例子,比如 `sudo cat /etc/shadow` v/s `sudo cat &lt; /etc/shadow`。还是一样的东西?不 - 因为两者之间发生的事情是不同的,有时理解很重要。:) (5认同)
  • 我只是因为绝对声明“没有区别”而投了反对票。正如我的回答(也只是被低估了)表明存在差异。如果您将答案更改为“几乎没有区别”,我很乐意收回我刚才的反对票。 (3认同)

Win*_*nix 14

一大不同

一个很大的区别是*, ?, 或[通配符(通配符)或 shell 可能扩展为多个文件名的任何其他字符。shell 扩展为两个或多个项目的任何内容,而不是视为单个文件名,都无法打开以进行重定向。

如果没有重定向(即 no <),shell 会将多个文件名传递给cat,从而一个接一个地输出文件的内容。例如这有效:

$ ls hello?.py
hello1.py  hello2.py

$ cat hello?.py

# Output for two files 'hello1.py' and 'hello2.py' appear on your screen
Run Code Online (Sandbox Code Playgroud)

但是使用重定向 ( <) 会出现错误消息:

$ ls < hello?.py
bash: hello?.py: ambiguous redirect

$ cat < hello?.py
bash: hello?.py: ambiguous redirect
Run Code Online (Sandbox Code Playgroud)

一个微小的差异

我认为重定向会更慢,但没有明显的时间差异:

$ time for f in * ; do cat "$f" > /dev/null ; done

real    0m3.399s
user    0m0.130s
sys     0m1.940s

$ time for f in * ; do cat < "$f" > /dev/null ; done

real    0m3.430s
user    0m0.100s
sys     0m2.043s
Run Code Online (Sandbox Code Playgroud)

笔记:

  • 在此测试中,差异约为 1/1000(千分之一)秒。在其他测试中,它是 1/100 秒,但仍然无法注意到。
  • 交替测试几次,以便尽可能多地将数据缓存到 RAM 中,并返回更一致的比较时间。另一种选择是在每次测试之前删除所有缓存。

  • `cat` 永远不会在扩展或其他特殊处理 `*` 的意义上“处理通配符”。如果当前目录有任何没有前导 `.` 的文件名,*shell* 会将 `*` 扩展为它们。当 `*` 出现在 `cat` 的参数出现的位置时,这些文件名将作为单独的参数传递给 `cat`。当 `*` 出现在重定向文件名将出现的位置时*并且如果 `*` 扩展为多个文件名*,这是没有意义的:多个文件不能在同一个文件描述符上打开同时。所以 bash 给出了“模糊重定向”错误。 (20认同)

小智 10

主要区别在于谁打开文件、shell 或 cat。他们可能在不同的许可制度下运作,所以

sudo cat /proc/some-protected-file
Run Code Online (Sandbox Code Playgroud)

可能同时工作

sudo cat < /proc/some-protected-file
Run Code Online (Sandbox Code Playgroud)

将失败。当只想使用echo简单的脚本时,这种许可制度可能有点棘手,因此有滥用的权宜之计,tee例如

echo level 7|sudo tee /proc/acpi/ibm/fan
Run Code Online (Sandbox Code Playgroud)

由于权限问题,使用重定向并不能真正起作用。


Ser*_*nyy 8

TL; DR 版本的答案:

  • cat file.txt的应用程序(在此情况下cat)接收的一个位置参数,执行打开它(2)系统调用,以及权限检查发生在应用程序内。

  • cat < file.txt所述壳将执行dup2()系统调用,使标准输入到文件描述符的对应于一个拷贝(通常下一个可用的一个,例如3) file.txt,靠近该文件描述符(例如3)。应用程序不对文件执行 open(2) 并且不知道文件的存在;它严格按照标准输入文件描述符运行。权限检查取决于 shell。打开的文件描述将与 shell 打开文件时的描述相同。

介绍

表面上cat file.txtcat < file.txt行为相同,但在幕后还有更多的事情发生在单一角色的差异上。这个<字符改变了 shell 的理解方式file.txt、打开文件的人以及文件在 shell 和命令之间传递的方式。当然,为了解释所有这些细节,我们还需要了解在 shell 中打开文件和运行命令是如何工作的,这就是我的回答旨在实现的目标 - 以最简单的方式教育读者,了解其中真正发生了什么这些看似简单的命令。在这个答案中,您会找到多个示例,包括那些使用strace命令来支持对幕后实际发生的事情的解释的示例。

由于 shell 和命令的内部工作原理是基于标准系统调用的,因此将其cat视为众多命令中的一个是很重要的。如果您是阅读此答案的初学者,请保持开放的心态,并注意这prog file.txt并不总是与prog < file.txt. 当两种形式应用于不同的命令时,它的行为可能完全不同,这取决于权限或程序的编写方式。我还请您暂缓判断,从不同用户的角度来看待这一点——对于一个临时的 shell 用户来说,需求可能与系统管理员和开发人员的需求完全不同。

execve() 可执行文件看到的系统调用和位置参数

Shell 通过使用fork(2)系统调用创建子进程并调用execve(2)系统调用来运行命令,该系统调用使用指定的参数和环境变量执行命令。内部调用的命令execve()将接管并替换进程;例如,当 shell 调用时cat,它将首先创建一个 PID 为 12345 的子进程,之后execve()PID 12345 变为cat.

这给我们带来了cat file.txt和之间的区别cat < file.txt。在第一种情况下,cat file.txt是使用一个位置参数调用的命令,shell 将execve()适当地组合在一起:

$ strace -e execve cat testfile.txt
execve("/bin/cat", ["cat", "testfile.txt"], 0x7ffcc6ee95f8 /* 50 vars */) = 0
hello, I am testfile.txt
+++ exited with 0 +++
Run Code Online (Sandbox Code Playgroud)

在第二种情况下,该<部分是 shell 操作符,它< testfile.txt告诉 shell 打开testfile.txt并将 stdin 文件描述符 0 转换为对应于testfile.txt. 这意味着< testfile.txt不会作为位置参数传递给命令本身:

$ strace -e execve cat < testfile.txt
execve("/bin/cat", ["cat"], 0x7ffc6adb5490 /* 50 vars */) = 0
hello, I am testfile.txt
+++ exited with 0 +++
$ 
Run Code Online (Sandbox Code Playgroud)

如果程序需要位置参数才能正常运行,这可能很重要。在这种情况下,cat如果没有提供与文件对应的位置参数,则默认接受来自 stdin 的输入。这也让我们进入下一个主题:标准输入和文件描述符。

STDIN 和文件描述符

谁打开文件cat或外壳?他们是怎么打开的?他们甚至有权打开它吗?这些是可以问的问题,但首先我们需要了解打开文件的工作原理。

当进程对文件执行open()或执行时openat(),这些函数为进程提供与打开的文件相对应的整数,然后程序可以通过引用该整数来调用read()seek()write()和无数其他系统调用。当然,系统(又名内核)将在内存中保存特定文件的打开方式、权限类型、模式类型 - 只读、只写、读/写 - 以及我们当前在文件中的位置- 在字节 0 或字节 1024 - 这被称为偏移量。这称为打开文件描述

在最基本的层面上,cat testfile.txtcat打开文件的位置,它将被下一个可用的文件描述符引用,即 3(注意read(2) 中的 3 )。

$ strace -e read -f cat testfile.txt > /dev/null
...
read(3, "hello, I am testfile.txt and thi"..., 131072) = 79
read(3, "", 131072)                     = 0
+++ exited with 0 +++
Run Code Online (Sandbox Code Playgroud)

相比之下,cat < testfile.txt将使用文件描述符 0(又名 stdin):

$ strace -e read -f cat < testfile.txt > /dev/null
...
read(0, "hello, I am testfile.txt and thi"..., 131072) = 79
read(0, "", 131072)                     = 0
+++ exited with 0 +++
Run Code Online (Sandbox Code Playgroud)

还记得早些时候我们了解到 shell 通过fork()首先然后exec() 是进程类型来运行命令吗?那么,原来怎样的文件是在与创建子进程打开龋齿fork()/exec()模式。引用open(2) 手册

当一个文件描述符被复制(使用 dup(2) 或类似的)时,重复引用与原始文件描述符相同的打开文件描述,两个文件描述符因此共享文件偏移量和文件状态标志。 这种共享也可能发生在进程之间:通过 fork(2) 创建的子进程继承其父进程的文件描述符的副本,这些副本引用相同的打开文件描述

这对cat file.txtvs意味着什么cat < file.txt?其实很多。在cat file.txtcat打开的文件,这意味着它是在文件的打开方式控制。在第二种情况下,shell 将打开file.txt子进程、复合命令和管道,它的打开方式将保持不变。我们目前在文件中的位置也将保持不变。

让我们以这个文件为例?

$ cat testfile.txt 
hello, I am testfile.txt and this is first line
line two
line three

last line
Run Code Online (Sandbox Code Playgroud)

看下面的例子。为什么line第一行的单词没有改变?

$ { head -n1; sed 's/line/potato/'; }  <  testfile.txt 2>/dev/null
hello, I am testfile.txt and this is first line
potato two
potato three

last potato
Run Code Online (Sandbox Code Playgroud)

答案在于上面open(2)手册中的引用:shell 打开的文件被复制到复合命令的 stdin 上,并且每个运行的命令/进程共享打开文件描述的偏移量。head只需将文件向前倒退一行,然后sed处理其余部分。更具体地说,我们会看到的2个序列dup2()/ fork()/execve()系统调用,并在每种情况下,我们得到的文件描述符的副本,它引用在打开相同的文件说明testfile.txt。使困惑 ?让我们举一个更疯狂的例子:

$ { head -n1; dd of=/dev/null bs=1 count=5; cat; }  <  testfile.txt 2>/dev/null
hello, I am testfile.txt and this is first line
two
line three

last line
Run Code Online (Sandbox Code Playgroud)

在这里,我们打印了第一行,然后将打开的文件描述倒回 5 个字节前(消除了单词line),然后只打印其余部分。我们是如何做到的?打开的文件描述testfile.txt保持不变,文件上有共享偏移量。

现在,除了编写像上面这样的疯狂复合命令之外,为什么这对理解很有用?作为开发人员,您可能希望利用或提防此类行为。假设cat您不是编写了一个 C 程序,该程序需要一个作为文件传递或从 stdin 传递的配置,然后您像myprog myconfig.json. 如果你跑了会怎样{ head -n1; myprog;} < myconfig.json?最好的情况是你的程序会得到不完整的配置数据,最坏的情况是 - 破坏程序。我们也可以利用它作为一个优势来生成子进程,让父进程回滚到子进程应该处理的数据。

权限和特权

这次让我们从一个对其他用户没有读或写权限的文件的示例开始:

$ sudo -u potato cat < testfile.txt
hello, I am testfile.txt and this is first line
line two
line three

last line
$ sudo -u potato cat testfile.txt
cat: testfile.txt: Permission denied
Run Code Online (Sandbox Code Playgroud)

这里发生了什么 ?为什么我们可以在第一个示例中以potato用户身份读取文件,但不能在第二个示例中读取?这可以追溯到前面提到的open(2)手册页中的相同引用。使用< file.txtshell 打开文件,因此openopenat()shell 执行/ 时进行权限检查。当时的 shell 以对文件具有读取权限的文件所有者的特权运行。由于打开文件描述是跨dup2调用继承的,shell 将打开文件描述符的sudo副本传递给 ,后者将文件描述符的副本传递给cat,并且cat不知道其他任何东西愉快地读取文件的内容。在最后一条命令中,catunder土豆用户执行open() 在文件上,当然该用户没有读取文件的权限。

更实际和更常见的是,这就是为什么用户对为什么这样的事情不起作用感到困惑(运行特权命令写入他们无法打开的文件):

$ sudo echo 100 > /sys/class/drm/*/intel_backlight/brightness
bash: /sys/class/drm/card0-eDP-1/intel_backlight/brightness: Permission denied
Run Code Online (Sandbox Code Playgroud)

但是这样的工作(使用特权命令写入需要特权的文件):

$ echo 100 |sudo tee /sys/class/drm/*/intel_backlight/brightness
[sudo] password for administrator: 
100
Run Code Online (Sandbox Code Playgroud)

与我之前展示的情况(privileged_prog < file.txt失败但privileged_prog file.txt有效)相反情况的一个理论示例是使用 SUID 程序。该SUID程序,例如passwd,允许执行与可执行文件所有者的权限的操作。这就是为什么passwdcommand 允许您更改密码,然后将更改写入/etc/shadow 的原因,即使该文件由 root 用户拥有。

为了示例和乐趣,我实际上cat在 C 中编写了类似快速演示的应用程序(此处的源代码)并设置了 SUID 位,但是如果您明白这一点,请随时跳到本答案的下一部分并忽略这部分. 旁注:操作系统会忽略带有 的解释可执行文件上的 SUID 位#!,因此同一事物的 Python 版本将失败。

让我们检查一下程序的权限和testfile.txt

$ ls -l ./privileged
-rwsr-xr-x 1 administrator administrator 8672 Nov 29 16:39 ./privileged
$ ls -l testfile.txt
-rw-r----- 1 administrator administrator 79 Nov 29 12:34 testfile.txt
Run Code Online (Sandbox Code Playgroud)

看起来不错,只有文件所有者和属于administrator组的人才能读取此文件。现在让我们以土豆用户身份登录并尝试读取文件:

$ su potato
Password: 
potato@my-PC:/home/administrator$ cat ./testfile.txt
cat: ./testfile.txt: Permission denied
potato@my-PC:/home/administrator$ cat  < ./testfile.txt
bash: ./testfile.txt: Permission denied
Run Code Online (Sandbox Code Playgroud)

看起来不错,shell 和cat拥有土豆用户权限的人都不能读取他们不允许读取的文件。还要注意谁报告了错误 - catvs bash。让我们测试我们的 SUID 程序:

potato@my-PC:/home/administrator$ ./privileged testfile.txt
hello, I am testfile.txt and this is first line
line two
line three

last line
potato@my-PC:/home/administrator$ ./privileged < testfile.txt
bash: testfile.txt: Permission denied
Run Code Online (Sandbox Code Playgroud)

按预期工作!此外,通过这个小演示提出的观点是,prog file.txtprog < file.txt在谁打开该文件有所不同,在打开的文件权限不同。

程序如何响应 STDIN

我们已经知道< testfile.txt重写 stdin 的方式是数据将来自指定的文件而不是键盘。从理论上讲,并基于 Unix 哲学“做一件事并把它做好”,从 stdin(又名文件描述符 0)读取的程序应该表现一致,因此prog1 | prog2应该类似于prog2 file.txt. 但是如果prog2想用lseek系统调用倒带,例如为了跳到某个字节或倒带到末尾以找到我们有多少数据,该怎么办?

某些程序不允许从管道读取数据,因为无法使用lseek(2)系统调用来回绕管道,或者无法使用mmap(2)将数据加载到内存中以进行更快的处理。Stephane Chazelas在这个问题中的出色回答已经涵盖了这一点: “cat 文件”和“文件”之间的区别是什么?./binary”和“./binary <文件”?我强烈建议阅读。

幸运的是,cat < file.txt并且cat file.txt行为一致并且cat不以任何方式反对管道,尽管我们知道它读取完全不同的文件描述符。这如何适用于prog file.txtvsprog < file.txt一般?如果程序真的不想对管道做任何事情,缺少位置参数file.txt就足以错误退出,但应用程序仍然可以使用lseek()stdin 来检查它是否是管道(尽管isatty(3)或检测fstat (2)中的S_ISFIFO 模式更有可能用于检测管道输入),在这种情况下,执行类似./binary <(grep pattern file.txt)./binary < <(grep pattern file.txt)可能不起作用的操作。

文件类型影响

文件类型可能会影响prog fileVSprog < file行为。这在某种程度上意味着作为程序的用户,即使您不知道这样做,您也在选择系统调用。例如,假设我们有一个 Unix 域套接字并且我们运行nc服务器来监听它,也许我们甚至准备了一些要提供的数据

$ nc -U -l /tmp/mysocket.sock   < testfile.txt 
Run Code Online (Sandbox Code Playgroud)

在这种情况下,/tmp/mysocket.sock将通过不同的系统调用打开:

socket(AF_UNIX, SOCK_STREAM, 0)         = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_UNIX, sun_path="/tmp/mysocket.sock"}, 20) = 0
Run Code Online (Sandbox Code Playgroud)

现在,让我们尝试从不同终端的套接字读取数据:

$ cat /tmp/mysocket.sock
cat: /tmp/mysocket.sock: No such device or address
$ cat <  /tmp/mysocket.sock
bash: /tmp/mysocket.sock: No such device or address
Run Code Online (Sandbox Code Playgroud)

shell 和 cat 都open(2)在需要完全不同的系统调用上执行系统调用——socket(2) 和 connect(2) 对。即使这不起作用:

$ nc -U  < /tmp/mysocket.sock
bash: /tmp/mysocket.sock: No such device or address
Run Code Online (Sandbox Code Playgroud)

但是如果我们意识到文件类型以及我们如何调用正确的系统调用,我们可以获得所需的行为:

$ nc -U /tmp/mysocket.sock
hello, I am testfile.txt and this is first line
line two
line three

last line
Run Code Online (Sandbox Code Playgroud)

笔记和其他建议阅读: