据我了解,任何从标准输入(即键盘)读取的命令都会从文件中获取输入。
$echo < text_content.txt
$
但是 echo 命令没有读取并在终端上显示 text_content.txt。这里有什么问题?
答案1
标准输入和命令
据我了解,任何从标准输入(即键盘)读取的命令都会从文件中获取输入。
是的。正如我在Linux/Unix 中文件的特征是什么?,文件是任何可以对其执行标准操作(例如read()
、open()
、write()
、 )的对象close()
。stdin
通过文件描述符 0 表示的文件实际上就是文件,并且 Linux 中的任何命令/进程在启动时都会获得 3 个标准文件描述符 - stdin、stdout、stderr。这些文件描述符背后的实际文件是什么?命令不关心也不应该关心,只要它可以对其执行操作即可。
但是 echo 命令没有读取并在终端上显示 text_content.txt。这里有什么问题?
现在,命令可以自由地对这些文件描述符1执行其想要的操作。如果是,echo
它仅处理stdout
而不执行任何操作stdin
。因此命令本身没有任何问题。
<
重定向将读取open()
文件text_content.txt
,并且它仍将分配从open()
调用返回的文件描述符(例如 3)到文件描述符 0,并且如果命令与 stdin 有关 - 它将像什么都没发生一样从文件描述符 0 读取。事实上,如果你运行strace -f -e dup2,write,openat bash -c 'echo < text_content.txt
openat(AT_FDCWD, "/etc/passwd", O_RDONLY) = 3
dup2(3, 0) = 0
write(1, "\n", 1
) = 1
dup2(10, 0) = 0
+++ exited with 0 +++
注意dup2()
系统调用。这就是文件描述符 3(文件)的分配/重定向方式。按照某种cp original copy
语法,dup2(3,0)
将文件描述符 3 复制到文件描述符 0,它们指向同一个文件。
还要注意,write()
将换行符输出到文件描述符1
。这是默认行为。如果我们这样做,strace -f -e dup2,write,openat bash -c 'echo FOO < /etc/passwd'
我们将看到以下内容
dup2(3, 0) = 0
write(1, "FOO\n", 4FOO
) = 4
dup2(10, 0) = 0
+++ exited with 0 +++
所以再说一次,这里没有什么错误 - 重定向正确执行,并完成了将内容写入文件描述符 1 的echo
工作。stdout
如何实际读取文件
现在,让我们讨论其他问题。我们如何在 shell 中读取文件?好吧,为此存在cat
一个接受参数的命令,因此您只需执行即可cat file.txt
。你能做到吗cat < file.txt
?当然。但这意味着 shell 必须执行该dup2()
调用,而 则cat file.txt
不需要 - 因此我的意思是说,不必要的系统调用更少。
在复杂的情况下,例如当你需要对文件的每一行执行操作时,你可以这样做
while IFS= read -r line || [ -n "$line" ]; do
# command to process line variable here
done < /etc/passwd
现在,对于整个循环,文件描述符0
将是打开时返回的任何文件描述符的副本/etc/passwd
。当然,如果你可以使用cat
或其他特定命令来读取文件 - 那么就这样做吧。Shell 是一种缓慢的方法,并且有很多缺陷。另请参阅,为什么使用 shell 循环来处理文本被认为是不好的做法?
1. 一些应用程序可能仍然关心它们可以用 stdin 做什么,或者检测 stdin 是文件还是管道。当stdin
文件描述符被指定为管道的读取端(也是文件描述符)时,输出是不可查找的(这意味着用 C 或其他语言编写的应用程序无法使用seek()
syscall 快速导航到文件中的特定字节偏移量)。标题为“cat file | ./binary” 和 “./binary < file” 有什么区别?
旁注:在 Linux 上,stdin 并不是从键盘获取输入的。如果你这样做
$ ls -l /proc/self/fd/0
lrwx------ 1 serg serg 64 Feb 23 16:45 /proc/self/fd/0 -> /dev/pts/0
您将在输出中看到它最初指向的是/dev/pts/0
终端设备stdin
。然后终端设备与键盘接口,或者它也可能是串行电缆。
此外,如果文件不是很大,您可以利用bash
内置mapfile
函数将行读入数组:
mapfile -t < /etc/passwd
for i in "${MAPFILE[@]}"; do echo "$i"; done