shell 输入输出和重定向
shell 输入/输出重定向
-
大多数 Unix 系统从终端接收输入并将产生的输出发送至终端。命令通常从标准输入读取数据,默认指向终端。通常将输出写入到标准输出或标准错误,默认也是指向终端。
-
终端
/dev/pts/0是一种特殊的文件;标准输入是文件描述符0的别名;文件描述符是进程访问文件(包括终端、管道、普通文件)的索引。(stdin可理解为就是FD 0) -
/dev/stdin通常是一个符号链接,指向/proc/self/fd/0,而在不进行输入重定向时/proc/self/fd/0通常指向/dev/pts/0,所以说标准输入通常来自终端,因为命令大多从标准输入读取数据,而标准输入通常指向终端 -
改变
FD 0指向通常是由 shell 来完成的,在命令实际执行前,shell 会解析输入的命令行,不论是管道、HereDocument还是HereString,shell 会提前把 FD 0 的指向设置好,实际执行的命令只需要从标准输入读取数据,而不关心数据的来源。算是一种解耦。 -
命令接收的数据其实包括两部分:
- 参数 (Arguments):控制指令。告诉程序做什么、对谁做、怎么做。例如:
grep "error" log.txt,"error"是匹配规则(参数)。log.txt是数据源的位置(数据)。 - 标准输入 (Stdin):数据负载。程序要处理的具体内容。例如:
cat file.txt | grep "error"。grep不需要知道文件名,它只关心“给我数据流,我过滤给你”。
- 参数 (Arguments):控制指令。告诉程序做什么、对谁做、怎么做。例如:
常见的重定向符号
| 命令 | 说明 |
|---|---|
| command > file | 将 stdout 重定向到 file |
| command >> file | 将 stdout 追加到 file |
| command < file | 将 stdin 重定向到 file |
| n > file | 将文件描述符为 n 的文件重定向到 file |
| n >> file | 将文件描述符为 n 的文件追加到file |
| n>&m | 将 FD n 输出重定向到 FD m 指向的文件,让 n 变成 m 的只写副本(如果 m 是只写) |
| n<&m | 将 FD n 输入重定向 FD m 指向的文件,让 n 变成 m 的只读副本(如果 m 是只读) |
| exec n>&m | 打开文件描述符 n(复制 m) |
| exec n>&- | 关闭文件描述符 n |
| <<tag abc tag | 将开始标记 tag 和结束标记 tag 之间的内容 abc 作为输入 |
- 输入输出重定向的底层逻辑是打开一个文件描述符 fd,然后让 FD 0, 1, n 指向这个文件描述符 fd,
fd = open("file", O_WRONLY|O_CREAT|O_APPEND); dup2(fd, x)- 默认 FD:如果不写 n,
>和>>默认操作 FD 1,<默认操作 FD 0。- 对
&m的理解:&的作用是 类型标记,告诉 Shell 解析器:后面的数字是文件描述符编号,而不是文件名。(类似于指针解引用)- 命令通常是从 stdin 读取输入,并将输出写入 stdout 或 stderr,重定向改变的是 stdin,stdout 和 stderr 的指向,但命令依然从 std 读取或输出,效果上看是从文件读取和输出,实际是由 std 作为中间连接
- 输入重定向可以将
<写在命令开头:< file grep "pattern",重定向操作符(<, >, >>, 2>等)不属于命令参数,它们是由 Shell 解释的特殊标记。
# 同时有输入和输出重定向
< input.txt grep "error" > errors.txt 2> /dev/null
# 混合位置
> output.txt < input.txt command args
2> errors.log command < input.txt > output.txt
/dev/null文件:它是特殊文件,写入到它的内容都被丢弃,起到禁止输出的效果
管道中的重定向
- 使用
|时,Shell 会创建一个匿名管道文件(内存缓冲区),将左侧命令的 stdout 绑定到管道的写入端,右侧命令的 stdin 绑定到管道的读取端。

- shell 执行命令时会
fork()出一个子进程,同时复制父进程的 FD 表给子进程(两个独立的副本),重定向操作(open, dup2)只在子进程中进行,父 Shell 的 FD 表从未被修改,子进程结束后,其 FD 表随进程销毁 - 重要例外情况,虽然重定向通常只影响单个命令,但有几种特殊情况会影响当前 Shell 或后续命令:
- 例外 1:
exec不带命令名(修改当前 Shell 的 FD),exec不带命令名时,重定向在当前 Shell 进程中执行,而不是子进程。
# 这会修改当前 Shell 进程的 FD 表!
exec 3> /tmp/log.txt
# 后续所有使用 FD 3 的命令都会写入 /tmp/log.txt
echo "line1" >&3
echo "line2" >&3
# 甚至新的命令也受影响(如果它们继承 FD 3)
bash -c 'echo "line3" >&3' # 也会写入 /tmp/log.txt
# 恢复:关闭 FD 3
exec 3>&-
- 例外 2:子 Shell 中的重定向不影响父 Shell
# 子 Shell 中的重定向
(
exec 3> /tmp/sub.txt
echo "in sub" >&3
)
# 父 Shell 的 FD 3 不存在(子 Shell 的 FD 表已销毁)
echo "test" >&3
# 报错:Bad file descriptor
- 例外 3:管道中的每个命令都是独立进程
# 管道中的重定向只影响对应命令
cat file.txt | grep "pattern" > out.txt
# cat 的 stdout → 管道
# grep 的 stdin ← 管道,stdout → out.txt
# 两个命令的 FD 表独立,互不影响
- 例外 4:
source或.执行的脚本
# source 在当前 Shell 中执行,不 fork
source script.sh
# 如果 script.sh 中有 exec 重定向,会影响当前 Shell
实用技巧:保存和恢复 FD
- 由于重定向是临时的,有时我们需要保存原始 FD 以便恢复:
# FD 3 也指向 FD 1 指向的文件,相当于记录原 FD 1 的指向(复制 FD 1)
exec 3>&1
# 现在可以安全地重定向 FD 1
echo "to file" > /tmp/output.txt
# 让 FD 1 再指向 FD 3 所指向的文件,相当于恢复 FD 1 原来的指向
exec 1>&3
# 关闭 FD 3
exec 3>&-
exec 命令
- 有时在脚本开头写入
exec >> "$log_file" 2>&1这会将所有输出(包括错误)自动记录到日志文件,无需每条命令单独重定向 exec >> "$log_file" 2>&1的机制:将脚本中所有命令的标准输出(stdout)和标准错误(stderr)追加(>>)到日志文件 $log_file 中,不显示在终端。exec > >(tee -a "$log_file") 2>&1的机制:通过 tee 命令将标准输出和错误同时显示在终端并追加到日志文件。>(tee -a "$log_file"):使用进程替换(Process Substitution),将 tee 命令作为临时文件描述符接收输入。tee -a:追加模式,保留原有日志。
进程替换
> >(...)的语法:第一个>表示重定向标准输出(exec >)。>(...)是进程替换语法(Bash 特性,需确保脚本以#!/bin/bash开头而非#!/bin/sh),将 tee 命令的输出管道作为文件描述符传递给 exec。这种写法是 Bash 中实现同时输出到终端和文件的标准方式。>(...)的基本语法与功能- 语法:
>(command_list)其中command_list是一组命令(用分号;分隔),>和(之间不能有空格。 - 作用:创建一个临时文件描述符(如
/dev/fd/63),将左侧命令的输出重定向到该文件,并作为右侧命令的输入。
- 语法:
>(...)的工作原理:- 临时文件描述符:Shell 会创建一个临时文件(如
/dev/fd/63),用于中转数据。 - 数据流方向:左侧命令的输出被重定向到临时文件。右侧命令从该文件读取输入。
- 异步执行:括号内的命令与主命令并行执行,通过临时文件传递数据。
- 临时文件描述符:Shell 会创建一个临时文件(如
echo "hello" > >(cat) # 实际步骤:
# 1. 创建 /dev/fd/63
# 2. echo "hello" → /dev/fd/63
# 3. cat 从 /dev/fd/63 读取并输出
>(...)和<(...)对比
| 特性 | >(...) |
<(...) |
|---|---|---|
| 数据方向 | 接收数据(输入重定向) | 产生数据(输出重定向) |
| 用途 | 将数据传递给命令的输入 | 将命令的输出作为文件读取 |
| 示例 | cmd > >(grep "pattern") |
diff <(cmd1) <(cmd2) |
进程替换和管道对比
-
进程替换(
<()或>())和管道(|)都是 Shell 中用于命令间数据传递的机制 -
数据流方向
- 管道:数据是单向流动的,从左到右依次传递。前一个命令的标准输出(stdout)作为后一个命令的标准输入(stdin)。
command1 | command2command1 的输出传给 command2 作为输入 - 进程替换:数据通过临时文件描述符(如
/dev/fd/63)传递,可以模拟文件输入或输出。cmd1 < <(cmd2):cmd1读取cmd2的输出作为输入(类似输入重定向)。cmd1 > >(cmd2):cmd2读取cmd1的输出作为输入(类似输出重定向)。diff <(ls dir1) <(ls dir2)比较两个目录的文件列表
- 管道:数据是单向流动的,从左到右依次传递。前一个命令的标准输出(stdout)作为后一个命令的标准输入(stdin)。
-
子进程与变量作用域
- 管道:管道中的每个命令在子 Shell 中执行,变量赋值无法传递到父 Shell。例如:
echo "hello" | read var; echo $var输出为空(var在子 Shell 中失效) - 进程替换:命令在当前 Shell 中执行,变量赋值可保留。例如:
read var < <(echo "hello"); echo $var输出hello
- 管道:管道中的每个命令在子 Shell 中执行,变量赋值无法传递到父 Shell。例如:
-
适用场景
- 管道:适合线性数据流处理(如过滤、排序、统计)。示例:
cat log.txt | grep "error" | sort | uniq -c - 进程替换:适合需要多输入/输出源或命令参数化的场景。示例:
- 管道:适合线性数据流处理(如过滤、排序、统计)。示例:
# 比较两个命令的输出,需要两个多个数据源,没法用管道传递
diff <(command1) <(command2)
# 将输出同时传递给多个命令
# tee 从 log.txt 读取数据,并同时输出到所有进程替换的临时文件描述符
# 同时输出到终端
tee >(grep "error" > errors.txt) >(grep "warning" > warnings.txt) < log.txt
# 相当于
cat log.txt|tee >(grep "error" > errors.txt) >(grep "warning" > warnings.txt)
个人理解:可以将进程替换视为一个临时文件
- 例如
ls > >(grep "txt"),>(grep "txt")会生成/dev/fd/63,ls的结果输出到此文件,grep从此文件中查找txt。cat <(echo "hello")中,<(echo "hello")会生成/dev/fd/45,echo "hello"的结果写入此文件,cat读取此文件
管道、HereDocument, HereString, - stdin
- 这四个概念(管道、Here Document、Here String、
-)的核心都是围绕标准输入(stdin) 的数据来源和处理方式。 - 它们的关系可以概括为:管道、Here Document、Here String 是 Shell 的重定向操作符,用于“制造”或“转发”数据流;而
-是一个约定俗成的文件名占位符,用于告诉命令“去读取标准输入”。 - 四种方式都是为了解决“如何向需要文件作为输入的命令提供数据”的问题,而不必创建物理临时文件。
管道 (|):内核级的数据通道
- 原理:Shell 调用
pipe()系统调用,内核创建一个管道对象(内存缓冲区),返回两个 FD:fd[0](读端) 和fd[1](写端)。 - Shell
fork出两个子进程(命令 A 和命令 B)。 - 在命令 A 中:
dup2(fd[1], 1)。将 A 的stdout(FD 1) 重定向到管道的写端fd[1]。命令 A 的输出将写入该管道对象。 - 在命令 B 中:
dup2(fd[0], 0)。将 B 的 stdin (FD 0) 重定向到管道的读端。命令 B 将从该管道读取数据 - 关闭多余 FD,执行
exec。 - 本质:命令 A 的 FD 1 和 命令 B 的 FD 0 指向了同一个内核管道对象。
- 管道对象就相当于一个临时文件,命令 A 向其中写入,而命令 B 从中读取,不过这是流式的,不是命令 A 写完命令 B 才能读取,而是实时的写入、读取。
HereDocument 和 HereString
# HereDocument
cat > test.txt << EOF
This is a test file.
This is a test file.
EOF
# HereString
bc <<< '1+2' # 等价于 echo '1+2' | bc
- 二者原理类似,shell 会解析命令中的文本,同时创建一个临时文件或者管道,将命令的输入重定向到这个文件。
- Here Document/String 的内容是由当前 Shell 解析的,而不是由目标命令解析的。这意味着变量展开发生在
exec之前。 << 'EOF'里面的变量不展开,所有内容视为字面量<< "EOF"里面的变量会展开,但抑制通配符展开<<< "$var"展开变量,自动加换行<<-EOFShell 只删除制表符 (Tab),不删除空格 (Space)。- 如果 Here Doc 中包含用户输入的变量,且未加引号,可能导致命令注入。应使用
<< 'EOF'防止展开 - 结束标记 (EOF):是告诉 Shell 内容到此结束的信号,有以下要求:
- 必须独占一行。
- 对于
<<:它必须严格顶格(第 1 列),前面不能有任何空格或 Tab。内容行中的 Tab 和空格会保留 - 对于
<<-:它前面可以有 Tab(会被忽略),但不能有空格。内容行中的 Tab 也会被删除,但空格会保留
<<-设计初衷是为了让 Shell 脚本本身更美观(允许脚本缩进),而不是为了让传递的内容缩进。
- 脚本层面:希望 EOF 能和上面的代码对齐(用 Tab 缩进),所以 Shell 把 EOF 前的 Tab 删掉。
- 内容层面:Shell 假设用的 Tab 也是为了跟脚本对齐,所以顺便把内容行的 Tab 也删除了。
- 空格层面:Shell 假设空格是实际数据(比如配置文件中的缩进),所以不会删除。
四种写法对比
| 写法 | 行为 | 场景 |
|---|---|---|
<<EOF |
保留所有前导空白(Tab 和空格)。内容中的缩进会原样保留。 | 需要严格保持格式(如 Python 代码、Makefile、YAML)。 |
<<-EOF |
仅去除前导 Tab 字符,保留空格。允许你在脚本中对 Here Doc 内容进行缩进以提高可读性。 | 大多数 Shell 脚本场景,希望代码整洁且内容不受缩进影响。 |
<< 'EOF' |
禁止变量展开和命令替换,但保留所有前导空白。 | 内容包含 $、` 或 \ 且不希望被 Shell 解析时。 |
<<-'EOF' |
禁止变量展开,且仅去除前导 Tab 字符。 | 最推荐的通用写法:既整洁又安全。 |
个人总结:
-决定是否删除前导tab'单引号决定内容是否展开
- 的特殊性
-不是 Shell 的特性,也不是内核的特性,它是应用程序(Command)的代码逻辑,是约定而非机制- Shell 视角:
-只是一个普通的字符串参数argv[1] = "-"。Shell 不做任何特殊处理。 - 命令视角:程序解析
argv,发现"-",于是主动去读取 FD 0。 - 举例:
cat file | tar -cvf out.tar -tar的argv包含"-cvf", "out.tar", "-"tar解析到最后一个"-",知道这里需要填入一个文件流。tar读取 FD 0(此时 FD 0 已被 Shell 通过管道连接到了cat的 FD 1)。
觉得有帮助的话,还请多多关注。

浙公网安备 33010602011771号