为什么会$ ls > ls.out
导致 'ls.out' 包含在当前目录中的文件名称列表中?为什么选择这样做?否则为什么不呢?
答案1
在评估命令时,>
首先解决重定向:因此运行时ls
输出文件已经被创建。
这也是为什么在同一命令中使用重定向读写同一文件>
会截断文件的原因;当命令运行时,文件已经被截断:
$ echo foo >bar
$ cat bar
foo
$ <bar cat >bar
$ cat bar
$
避免这种情况的技巧:
<<<"$(ls)" > ls.out
(适用于在重定向解析之前需要运行的任何命令)命令替换在外部命令评估之前运行,因此在创建
ls
之前运行:ls.out
$ ls bar foo $ <<<"$(ls)" > ls.out $ cat ls.out bar foo
ls | sponge ls.out
(适用于在重定向解析之前需要运行的任何命令)sponge
仅当管道的其余部分执行完毕后才写入文件,因此在创建ls
之前运行(随包提供):ls.out
sponge
moreutils
$ ls bar foo $ ls | sponge ls.out $ cat ls.out bar foo
ls * > ls.out
(适用于ls > ls.out
的具体情况)文件名扩展在解析重定向之前执行,因此
ls
将在其参数上运行,其中不包含ls.out
:$ ls bar foo $ ls * > ls.out $ cat ls.out bar foo $
关于为什么在程序/脚本/任何运行之前解析重定向,我没有看到具体的原因强制的但我看到了两个原因更好的这样做:
如果不事先重定向 STDIN,程序/脚本/其他任何内容都将保持等待状态,直到 STDIN 被重定向;
不事先重定向 STDOUT 必然会导致 shell 缓冲程序/脚本/任何内容的输出,直到 STDOUT 被重定向;
因此,第一种情况是浪费时间,第二种情况是浪费时间和内存。
这只是我想到的,我并不是说这些是真正的原因;但我想总而言之,如果一个人有选择的话,无论如何他们都会出于上述原因选择重定向。
答案2
从man bash
:
重定向
在执行命令之前,可以使用 shell 解释的特殊符号重定向其输入和输出。重定向允许复制、打开、关闭命令的文件句柄,使其引用不同的文件,并可以更改命令读取和写入的文件。
第一句话,建议在执行命令之前将输出转移到stdin
重定向以外的其他地方。因此,为了重定向到文件,文件必须首先由 shell 本身创建。
为了避免出现文件,我建议您先将输出重定向到命名管道,然后再重定向到文件。请注意使用&
将终端控制权返回给用户
DIR:/xieerqi
skolodya@ubuntu:$ mkfifo /tmp/namedPipe.fifo
DIR:/xieerqi
skolodya@ubuntu:$ ls > /tmp/namedPipe.fifo &
[1] 14167
DIR:/xieerqi
skolodya@ubuntu:$ cat /tmp/namedPipe.fifo > ls.out
但为什么?
想想看 - 输出会在哪里?一个程序有诸如printf
、sprintf
、之类的函数puts
,默认情况下它们都转到stdout
,但如果文件不存在,它们的输出能转到文件吗?这就像水。如果不先把玻璃杯放在水龙头下面,你能得到一杯水吗?
答案3
我不反对当前的答案。必须在命令运行之前打开输出文件,否则命令将无处写入其输出。
这是因为“一切都是文件”在我们的世界中。输出到屏幕的是 SDOUT(又名文件描述符 1)。对于要写入终端的应用程序,它打开fd1 并像文件一样写入其中。
当您在 shell 中重定向应用程序的输出时,您正在更改 fd1,因此它实际上指向文件。当您通过管道传输时,您会将一个应用程序的 STDOUT 更改为另一个应用程序的 STDIN (fd0)。
不过这样说很好,但你可以很容易地看看这是如何工作的strace
。这相当沉重,但这个例子很短。
strace sh -c "ls > ls.out" 2> strace.out
我们可以strace.out
看到以下亮点:
open("ls.out", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
这将打开ls.out
为fd3
。只写。如果存在则截断(覆盖),否则创建。
fcntl(1, F_DUPFD, 10) = 10
close(1) = 0
fcntl(10, F_SETFD, FD_CLOEXEC) = 0
dup2(3, 1) = 1
close(3) = 0
这有点像杂耍。我们将 STDOUT(fd1)分流到 fd10 并将其关闭。这是因为我们不会用此命令向真正的 STDOUT 输出任何内容。它通过将写入句柄复制到ls.out
原始句柄并关闭原始句柄来完成。
stat("/opt/wine-staging/bin/ls", 0x7ffc6bf028c0) = -1 ENOENT (No such file or directory)
stat("/home/oli/bin/ls", 0x7ffc6bf028c0) = -1 ENOENT (No such file or directory)
stat("/usr/local/sbin/ls", 0x7ffc6bf028c0) = -1 ENOENT (No such file or directory)
stat("/usr/local/bin/ls", 0x7ffc6bf028c0) = -1 ENOENT (No such file or directory)
stat("/usr/sbin/ls", 0x7ffc6bf028c0) = -1 ENOENT (No such file or directory)
stat("/usr/bin/ls", 0x7ffc6bf028c0) = -1 ENOENT (No such file or directory)
stat("/sbin/ls", 0x7ffc6bf028c0) = -1 ENOENT (No such file or directory)
stat("/bin/ls", {st_mode=S_IFREG|0755, st_size=110080, ...}) = 0
这就是搜索可执行文件的过程。也许这是一个教训,不要走太长的路径 ;)
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f0961324a10) = 31933
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 31933
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=31933, si_status=0, si_utime=0, si_stime=0} ---
rt_sigreturn() = 31933
dup2(10, 1) = 1
close(10) = 0
然后命令运行,父进程等待。在此操作期间,任何 STDOUT 实际上都将映射到 上的打开文件句柄ls.out
。当子进程发出 时SIGCHLD
,这会告诉父进程它已完成并且可以恢复。它以一些更多的操作和 结束ls.out
。
为什么所以玩杂耍吗?不,我也不太清楚。
当然,您可以改变这种行为。您可以使用类似这样的方法缓冲到内存中sponge
,这将在后续命令中不可见。我们仍然会影响文件描述符,但不是以文件系统可见的方式。
ls | sponge ls.out
答案4
还有一篇很好的文章shell 中重定向和管道操作符的实现。它显示了如何实现重定向,如下$ ls > ls.out
所示:
main(){
close(1); // Release fd no - 1
open("ls.out", "w"); // Open a file with fd no = 1
// Child process
if (fork() == 0) {
exec("ls");
}
}