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
识别它没有文件参数,并将从标准输入读取。
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)
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)
笔记:
小智 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)
由于权限问题,使用重定向并不能真正起作用。
用cat file.txt
的应用程序(在此情况下cat
)接收的一个位置参数,执行打开它(2)系统调用,以及权限检查发生在应用程序内。
与cat < file.txt
所述壳将执行dup2()
系统调用,使标准输入到文件描述符的对应于一个拷贝(通常下一个可用的一个,例如3) file.txt
,靠近该文件描述符(例如3)。应用程序不对文件执行 open(2) 并且不知道文件的存在;它严格按照标准输入文件描述符运行。权限检查取决于 shell。打开的文件描述将与 shell 打开文件时的描述相同。
表面上cat file.txt
和cat < file.txt
行为相同,但在幕后还有更多的事情发生在单一角色的差异上。这个<
字符改变了 shell 的理解方式file.txt
、打开文件的人以及文件在 shell 和命令之间传递的方式。当然,为了解释所有这些细节,我们还需要了解在 shell 中打开文件和运行命令是如何工作的,这就是我的回答旨在实现的目标 - 以最简单的方式教育读者,了解其中真正发生了什么这些看似简单的命令。在这个答案中,您会找到多个示例,包括那些使用strace命令来支持对幕后实际发生的事情的解释的示例。
由于 shell 和命令的内部工作原理是基于标准系统调用的,因此将其cat
视为众多命令中的一个是很重要的。如果您是阅读此答案的初学者,请保持开放的心态,并注意这prog file.txt
并不总是与prog < file.txt
. 当两种形式应用于不同的命令时,它的行为可能完全不同,这取决于权限或程序的编写方式。我还请您暂缓判断,从不同用户的角度来看待这一点——对于一个临时的 shell 用户来说,需求可能与系统管理员和开发人员的需求完全不同。
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 的输入。这也让我们进入下一个主题:标准输入和文件描述符。
谁打开文件cat
或外壳?他们是怎么打开的?他们甚至有权打开它吗?这些是可以问的问题,但首先我们需要了解打开文件的工作原理。
当进程对文件执行open()
或执行时openat()
,这些函数为进程提供与打开的文件相对应的整数,然后程序可以通过引用该整数来调用read()
、seek()
、write()
和无数其他系统调用。当然,系统(又名内核)将在内存中保存特定文件的打开方式、权限类型、模式类型 - 只读、只写、读/写 - 以及我们当前在文件中的位置- 在字节 0 或字节 1024 - 这被称为偏移量。这称为打开文件描述。
在最基本的层面上,cat testfile.txt
是cat
打开文件的位置,它将被下一个可用的文件描述符引用,即 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.txt
vs意味着什么cat < file.txt
?其实很多。在cat file.txt
该cat
打开的文件,这意味着它是在文件的打开方式控制。在第二种情况下,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.txt
shell 打开文件,因此在open
openat()
shell 执行/ 时进行权限检查。当时的 shell 以对文件具有读取权限的文件所有者的特权运行。由于打开文件描述是跨dup2
调用继承的,shell 将打开文件描述符的sudo
副本传递给 ,后者将文件描述符的副本传递给cat
,并且cat
不知道其他任何东西愉快地读取文件的内容。在最后一条命令中,cat
under土豆用户执行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
,允许执行与可执行文件所有者的权限的操作。这就是为什么passwd
command 允许您更改密码,然后将更改写入/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
拥有土豆用户权限的人都不能读取他们不允许读取的文件。还要注意谁报告了错误 - cat
vs 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.txt
和prog < file.txt
在谁打开该文件有所不同,在打开的文件权限不同。
我们已经知道< 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.txt
vsprog < file.txt
一般?如果程序真的不想对管道做任何事情,缺少位置参数file.txt
就足以错误退出,但应用程序仍然可以使用lseek()
stdin 来检查它是否是管道(尽管isatty(3)或检测fstat (2)中的S_ISFIFO 模式更有可能用于检测管道输入),在这种情况下,执行类似./binary <(grep pattern file.txt)
或./binary < <(grep pattern file.txt)
可能不起作用的操作。
文件类型可能会影响prog file
VSprog < 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)
open(2)手册中的引用指出,文件描述符的权限会被继承。理论上,有一种方法可以更改文件描述符的读/写权限,但这必须在源代码级别完成。
为什么 的行为command 1>file.txt 2>file.txt
不同于command 1>file.txt 2>&1
?