Answers:
管道用于将输出传递到另一个程序或实用程序。
重定向用于将输出传递到文件或流。
示例:thing1 > thing2
vsthing1 | thing2
thing1 > thing2
thing1
thing1
输出的所有内容都将放置在名为的文件中thing2
。(注意-如果thing2
存在,它将被覆盖)如果要将输出从程序传递thing1
到名为的程序thing2
,则可以执行以下操作:
thing1 > temp_file && thing2 < temp_file
这将
thing1
temp_file
thing2
,假装键盘上的人键入的内容temp_file
作为输入。但是,这很笨拙,因此他们将管道制作为一种更简单的方法。thing1 | thing2
做与thing1 > temp_file && thing2 < temp_file
编辑以提供更多详细信息以在评论中提问:
如果>
试图同时“传递给程序”和“写入文件”,则可能会导致双向问题。
第一个示例:您正在尝试写入文件。已经存在一个您想要覆盖的文件名。但是,该文件是可执行的。据推测,它将尝试通过传递输入来执行该文件。您必须执行类似将输出写入新文件名,然后重命名文件的操作。
第二个示例:正如Florian Diesch所指出的,如果系统中其他地方有另一个同名命令(在执行路径中),该怎么办。如果您打算在当前文件夹中创建一个具有该名称的文件,那么您将陷入困境。
第三:如果您键入错误的命令,它不会警告您该命令不存在。现在,如果您键入ls | gerp log.txt
,它将告诉您bash: gerp: command not found
。如果>
两者兼有,它只会为您创建一个新文件(然后警告它不知道如何处理log.txt
)。
less
怎么办?thing | less
并且 thing > less
完全不同,因为他们做的事情不同。您提出的建议会造成歧义。
tee
命令执行其他操作。 tee
将输出写入屏幕(stdout
)和文件。重定向仅执行文件。
如果的含义foo > bar
取决于是否有一个命名的命令bar
,它将使使用重定向变得更加困难,并且更容易出错:每次我想重定向到文件时,我首先必须检查是否有一个名为目标文件的命令。
bar
属于$PATH
env变量的目录中写入时,这才是问题。如果您在/ bin之类的目录中,则可能是个问题。但是即使这样,bar
也必须设置可执行文件许可权,以便外壳程序不仅检查可执行文件,bar
还可以执行它。而且,如果要考虑覆盖现有文件,则noclober
shell选项应防止在重定向中覆盖现有文件。
两者之间在语法上有很大的不同:
您可以想到这样的重定向:cat [<infile] [>outfile]
。这表示顺序无关紧要:cat <infile >outfile
与相同cat >outfile <infile
。您甚至可以混合使用其他参数重定向起来:cat >outfile <infile -b
和cat <infile -b >outfile
都是完全正常的。也可以串起来多于一个的输入或输出(输入将被顺序地读取并输出所有将被写入到每个输出文件): cat >outfile1 >outfile2 <infile1 <infile2
。重定向的目标或源可以是文件名或流的名称(例如&1,至少在bash中)。
但是管道完全将一个命令与另一个命令分开,您不能将它们与参数混在一起:
[command1] | [command2]
管道将从命令1写入标准输出的所有内容发送到命令2的标准输入。
您还可以结合管道和重定向。例如:
cat <infile >outfile | cat <infile2 >outfile2
第一cat
行将从infile中读取行,然后将每一行同时写入outfile并将其发送至第二行cat
。
在第二个中cat
,标准输入首先从管道(infile的内容)读取,然后从infile2读取,并将每一行写入outfile2。运行此命令后,outfile将是infile的副本,并且outfile2将包含infile和infile2。
最后,您实际上使用“ here字符串”重定向(仅bash系列)和反引号执行与示例非常相似的操作:
grep blah <<<`ls`
将产生与以下相同的结果
ls | grep blah
但是我认为重定向版本将首先将ls的所有输出读入一个缓冲区(在内存中),然后一次将该缓冲区送入grep一行,而管道版本会在出现时从ls提取每一行,并将该行传递给grep。
echo yes 1>&2 2>/tmp/blah; wc -l /tmp/blah; echo yes 2>/tmp/blah 1>&2; wc -l /tmp/blah
,则顺序在重定向中很重要:此外,重定向到文件将仅使用最后一个重定向。echo yes >/tmp/blah >/tmp/blah2
只会写入/tmp/blah2
。
注意:答案反映了我对这些机制的最新了解,是在研究和阅读本站点和unix.stackexchange.com上的同行的答案时积累的,并将随着时间的推移而更新。不要犹豫,提出问题或提出改进意见。我还建议您尝试查看syscall在shell中如何通过strace
命令工作。另外,请不要被内部或系统调用的概念所吓倒-您不必了解或能够使用它们来了解shell的工作方式,但是它们绝对有助于理解。
|
管道不与磁盘上的条目相关联,因此没有磁盘文件系统的索引节点号(但在内核空间的pipefs虚拟文件系统中具有索引节点),但是重定向通常涉及文件,该文件确实具有磁盘条目并因此具有对应的索引节点。lseek()
,因此命令无法读取某些数据,然后回退,但是当您使用重定向>
或<
通常是带有lseek()
对象的文件时,命令可以随心所欲地导航。dup2()
在引擎盖下使用syscall来提供文件描述符的副本,在此发生实际的数据流。exec
内置命令“全局”应用(请参阅this和this),因此,如果执行此操作,则此后exec > output.txt
将写入所有命令output.txt
。|
管道仅适用于当前命令(这意味着简单命令或子shell之类的seq 5 | (head -n1; head -n2)
命令或复合命令。当重定向的文件来完成,像echo "TEST" > file
并echo "TEST" >> file
都使用open()
了该文件系统调用(见),并从它那里得到的文件描述符通过它来dup2()
。管道|
仅使用pipe()
和dup2()
syscall。
就执行的命令而言,管道和重定向只不过是文件描述符-类文件对象,它们可能会盲目地写入其中,或在内部对其进行操作(这可能会产生意外的行为;apt
例如,往往甚至不会写入stdout如果它知道有重定向)。
为了了解这两种机制的不同之处,有必要了解它们的基本属性,两者的历史以及它们在C编程语言中的根源。实际上,了解文件描述符是什么,以及如何dup2()
以及pipe()
系统调用是必不可少的lseek()
。Shell的意思是使这些机制对用户抽象的一种方式,但是比抽象更深入地挖掘有助于理解Shell行为的真实本质。
根据丹尼斯·里奇(Dennis Ritche)的文章《先知岩画》(Prophetic Petroglyphs),管道起源于1964年马尔科姆·道格拉斯·麦克罗伊(Malcolm Douglas McIlroy)编写的内部备忘录,当时他们正在研究Multics操作系统。引用:
简单地说:
- 我们应该有一些连接程序的方法,例如花园水管-当有必要以另一种方式处理数据时,请拧入另一段。这也是IO的方式。
显而易见的是,当时程序可以写入磁盘,但是如果输出很大,效率很低。在Unix Pipeline视频中引用Brian Kernighan的解释:
首先,您不必编写一个大型的大型程序-您已有的小型程序可能已经完成了部分工作...另一个是,如果处理的数据量可能不适合,则可能您将其存储在文件中...因为请记住,我们回到了磁盘上拥有这些东西的日子,如果幸运的话,我们拥有一到两个兆字节的数据...因此管道永远不必实例化整个输出。
因此,概念上的区别是显而易见的:管道是使程序相互通信的一种机制。重定向-是在基本级别上写入文件的方式。在这两种情况下,shell都使这两件事变得容易,但是在引擎盖下却有很多事情要做。
我们从文件描述符的概念开始。文件描述符基本上描述了一个打开的文件(无论是磁盘上的文件,内存中的文件还是匿名文件),均由整数表示。两个标准数据流 (stdin,stdout,stderr)分别是文件描述符0、1、2。他们来自哪里 ?好了,在shell命令中,文件描述符是从其父shell继承的。通常对于所有进程都是如此-子进程继承了父进程的文件描述符。对于守护程序,通常会关闭所有继承的文件描述符和/或重定向到其他位置。
返回重定向。到底是什么 它是一种机制,告诉外壳程序为命令准备文件描述符(因为重定向是在命令运行之前由外壳程序完成的),然后将其指向用户建议的位置。输出重定向的标准定义是
[n]>word
这[n]
有文件描述符数。当您执行echo "Something" > /dev/null
此操作时,数字1就会隐含在其中,而echo 2> /dev/null
。
在后台,这是通过dup2()
系统调用复制文件描述符来完成的。让我们来df > /dev/null
。外壳程序将在df
运行时创建一个子进程,但在此之前它将/dev/null
以文件描述符#3 打开并被dup2(3,1)
发布,这将复制文件描述符3,而副本将为1。您知道如何拥有两个文件file1.txt
,file2.txt
,当您cp file1.txt file2.txt
拥有两个相同的文件时,您可以独立地操作它们吗?这有点一样。通常,您可以看到在运行之前,它们bash
会dup(1,10)
制作一个副本文件描述符#1,该描述符为stdout
(该副本将为fd#10),以便稍后进行恢复。重要的是要注意,当您考虑内置命令时(它们是Shell本身的一部分,并且在/bin
其他地方没有文件)或非交互式shell中的简单命令,该Shell不会创建子进程。
然后我们有类似[n]>&[m]
和的事情[n]&<[m]
。这是在复制文件描述符,该dup2()
语法与shell语法中的机制相同,现在用户可以方便地使用它。
关于重定向的重要注意事项之一是它们的顺序不是固定的,但是对于shell解释用户想要的内容很重要。比较以下内容:
# Make copy of where fd 2 points , then redirect fd 2
$ ls -l /proc/self/fd/ 3>&2 2> /dev/null
total 0
lrwx------ 1 user user 64 Sep 13 00:08 0 -> /dev/pts/0
lrwx------ 1 user user 64 Sep 13 00:08 1 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:08 2 -> /dev/null
lrwx------ 1 runner user 64 Sep 13 00:08 3 -> /dev/pts/0
lr-x------ 1 user user 64 Sep 13 00:08 4 -> /proc/29/fd
# redirect fd #2 first, then clone it
$ ls -l /proc/self/fd/ 2> /dev/null 3>&2
total 0
lrwx------ 1 user user 64 Sep 13 00:08 0 -> /dev/pts/0
lrwx------ 1 user user 64 Sep 13 00:08 1 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:08 2 -> /dev/null
l-wx------ 1 user user 64 Sep 13 00:08 3 -> /dev/null
lr-x------ 1 user user 64 Sep 13 00:08 4 -> /proc/31/fd
这些在shell脚本中的实际使用可以是多种多样的:
还有很多其他
pipe()
和dup2()
那么如何创建管道?通过pipe()
syscall,它将把称为pipefd
两个int
(整数)类型的项的数组(也称为列表)作为输入。这两个整数是文件描述符。在pipefd[0]
将管道的读端,并pipefd[1]
会写入结束。因此df | grep 'foo'
,grep
将获得的副本,pipefd[0]
并 df
获得的副本pipefd[1]
。但是如何?当然,有了dup2()
syscall 的魔力。对于df
在我们的例子中,假设pipefd[1]
有#4,所以外壳会使孩子,做dup2(4,1)
(记得我的 cp
例子吗?),然后做execve()
实际运行df
。自然,df
将继承文件描述符#1,但不会意识到它不再指向终端,而是实际上指向fd#4(实际上是管道的写入端)。自然地,grep 'foo'
除了文件描述符数量不同之外,其他事情都会发生。
现在,一个有趣的问题:我们是否也可以制作重定向fd#2的管道,而不仅仅是fd#1?是的,事实上这就是|&
bash的功能。POSIX标准要求shell命令语言支持df 2>&1 | grep 'foo'
的语法为目的,但bash
确实|&
也是如此。
需要注意的重要一点是,管道始终处理文件描述符。存在FIFO
或命名为pipe,它在磁盘上有一个文件名,让我们将其用作文件,但其行为类似于管道。但是|
管道的类型是所谓的匿名管道-它们没有文件名,因为它们实际上只是连接在一起的两个对象。我们不处理文件这一事实也产生了重要的含义:管道不可用lseek()
。内存中或磁盘上的文件都是静态的-程序可以使用lseek()
syscall跳转到字节120,然后返回字节10,然后一直转发到末尾。管道不是静态的-它们是顺序的,因此您无法回退使用它们从管道中获得的数据lseek()
。这就是使某些程序知道它们是从文件还是从管道读取的,因此它们可以进行必要的调整以提高性能。换句话说,a prog
可以检测到我是cat file.txt | prog
还是prog < input.txt
。实际的例子是尾巴。
管道的另外两个非常有趣的属性是它们有一个缓冲区,在Linux上是4096字节,实际上它们有一个Linux源代码中定义的文件系统!它们不仅仅是传递数据的对象,它们本身就是数据结构!实际上,由于存在同时管理管道和FIFO的pipefs文件系统,因此管道在其各自的文件系统上都有一个索引 节点号:
# Stdout of ls is wired to pipe
$ ls -l /proc/self/fd/ | cat
lrwx------ 1 user user 64 Sep 13 00:02 0 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:02 1 -> pipe:[15655630]
lrwx------ 1 user user 64 Sep 13 00:02 2 -> /dev/pts/0
lr-x------ 1 user user 64 Sep 13 00:02 3 -> /proc/22/fd
# stdin of ls is wired to pipe
$ true | ls -l /proc/self/fd/0
lr-x------ 1 user user 64 Sep 13 03:58 /proc/self/fd/0 -> 'pipe:[54741]'
在Linux上,管道是单向的,就像重定向一样。在某些类似Unix的实现中-有双向管道。尽管Shell脚本具有魔力,但您也可以在Linux上建立双向管道。
pipe()
syscall和C在C中创建管道dup2()
。<<
,<<<
被实现为在匿名(未链接)临时文件bash
和ksh
,而< <()
使用匿名管道; /bin/dash
使用管道<<
。请参见bash中<<,<<和<<之间有什么区别?除了其他答案之外,语义上也存在细微差别-例如,管道比重定向更容易关闭:
seq 5 | (head -n1; head -n1) # just 1
seq 5 > tmp5; (head -n1; head -n1) < tmp5 # 1 and 2
seq 5 | (read LINE; echo $LINE; head -n1) # 1 and 2
在第一个示例中,当第一个调用head
结束时,它将关闭管道并seq
终止,因此第二个没有可用的输入head
。
在第二个示例中,head占用了第一行,但是当它关闭自己的stdin
管道时,该文件将保持打开状态以供下一次调用使用。
第三个示例显示,如果我们read
避免关闭管道,则它在子流程中仍然可用。
因此,“流”是我们通过其分流数据的东西(stdin等),并且在两种情况下都是相同的,但是管道连接来自两个流程的流,其中重定向将流程和文件之间的流连接起来,因此可以看到相同点和不同点的来源。
PS:如果您像我一样对这些示例感到好奇和/或惊讶,则可以深入研究trap
这些过程如何解决,例如:
(trap 'echo seq EXITed >&2' EXIT; seq 5) | (trap 'echo all done' EXIT; (trap 'echo first head exited' EXIT; head -n1)
echo '.'
(trap 'echo second head exited' EXIT; head -n1))
有时第一个过程在1
打印之前关闭,有时在打印之后关闭。
我还发现使用一种有趣的方法exec <&-
来关闭重定向中的流以近似管道的行为(尽管有错误):
seq 5 > tmp5
(trap 'echo all done' EXIT
(trap 'echo first head exited' EXIT; head -n1)
echo '.'
exec <&-
(trap 'echo second head exited' EXIT; head -n1)) < tmp5`
read
仅消耗第一行(1
和newline 是一个字节)。seq
共发送10个字节(5个数字和5个换行符)。因此,管道缓冲区中还剩下8个字节,这就是第二个head
起作用的原因-管道缓冲区中仍然有可用数据。顺便说一句,只有在读到0字节时头部才退出,有点像head /dev/null
seq 5 | (head -n1; head -n1)
第一个调用中清空了管道,因此它仍然处于打开状态,但是第二个调用中没有数据head
?那么,管道和重定向之间的行为差异是因为head将所有数据从管道中拉出,但是仅从文件句柄中拉出了两行?
strace
我在第一条评论中给出的命令所能看到的。通过重定向,tmp文件位于磁盘上,这使得它易于查找(因为它们使用lseek()
syscall -命令可以根据需要从文件的首字节跳到最后一个字节。但是管道是顺序的而不是可查找的。因此,head执行此操作的唯一方法工作是第一次读到的一切,或者,如果文件是大-地图有些是通过到RAM mmap()
的呼叫我曾经做过我自己。tail
在Python,跑进完全相同的问题。
(...)
,并且subshell会将其自己的stdin复制到其中的每个命令(...)
。因此,从技术上讲,它们是从同一对象读取的。首先head
认为它是从自己的标准输入读取的。第二head
认为它有自己的标准输入。但实际上,它们的fd#1(stdin)只是同一fd的副本,该副本在管道的末尾读取。另外,我已经发布了答案,因此也许可以帮助您弄清楚事情。
我今天在C语言中遇到了一个问题。本质上,即使将Pipe发送到,其重定向的语义也不同stdin
。确实,我认为给定这些差异,管道应该走到以外的地方stdin
,以便以不同的方式来处理stdin
它stdpipe
(让它成为任意的差异)。
考虑一下。当管道显示一个程序输出到另一个程序时fstat
,st_size
尽管ls -lha /proc/{PID}/fd
显示有文件,但似乎返回零。当重定向文件不是这种情况下(至少在Debian wheezy
,stretch
和jessie
香草和Ubuntu 14.04
,16.04
香草。
如果您cat /proc/{PID}/fd/0
有重定向,则可以重复阅读多次。如果使用管道执行此操作,则会注意到第二次连续运行任务时,不会获得相同的输出。
thing1 > temp_file && thing2 < temp_file
可以更轻松地使用管道。但是,为什么不重新使用>
操作员来执行此操作,例如thing1 > thing2
用于命令thing1
和thing2
?为什么需要额外的运算符|
?