// Linux · 2024-11-24

深入理解 Shell 重定向

在 Linux/Unix 类操作系统中,Shell 重定向是程序员和系统管理员必备的核心技能之一。它允许我们灵活控制命令的输入来源、输出目的地以及错误处理方式,极大地扩展了命令行的使用场景——从简单的日志记录、自动化脚本编写,到复杂的进程间通信、网络连接,重定向都发挥着不可替代的作用。

本文将从文件描述符的底层概念出发,循序渐进地讲解 Shell 重定向的全部核心用法,包括标准流重定向、文件操作、此处文档/字符串、进程替换、管道进阶等内容,并附上完整的语法速查表,帮助你彻底掌握这一强大工具,提升命令行效率。


一、理解文件描述符:重定向的底层基础

在学习重定向之前,我们必须先搞清楚「文件描述符」(File Descriptor,简称 FD)的概念——它是 Shell 与操作系统交互的核心桥梁。

1.1 什么是文件描述符?

文件描述符是操作系统为打开的文件、管道、网络连接等「输入/输出资源」分配的非负整数标识符。Shell 中所有的输入输出操作,本质上都是通过操作文件描述符来完成的。

对于任何一个 Shell 进程,系统会默认分配 3 个标准文件描述符,它们是重定向的基础:

  • 0:标准输入(stdin):默认指向键盘,是命令的输入来源(比如 read 命令读取用户输入)。
  • 1:标准输出(stdout):默认指向终端屏幕,是命令的正常输出(比如 ls 命令列出的文件列表)。
  • 2:标准错误(stderr):默认指向终端屏幕,是命令的错误输出(比如 ls /nonexistent 输出的「没有那个文件或目录」)。

这三个标准流的默认指向可以通过重定向语法修改,比如将 stdout 指向文件、将 stdin 指向某个文本,从而实现灵活的输入输出控制。

1.2 文件描述符的核心特性

  • 整数标识:默认使用 0、1、2 表示标准流,用户可自定义 3-9 之间的文件描述符(10 及以上部分 Shell 支持,但兼容性较差)。
  • 资源指向:每个文件描述符都对应一个打开的资源(文件、管道、网络连接等),重定向本质就是修改这种「指向关系」。
  • 继承性:子进程会继承父进程的文件描述符(比如脚本中的命令会继承脚本进程的重定向设置)。
  • 可关闭/复制:用户可以手动关闭自定义文件描述符,或复制一个文件描述符指向另一个资源(类似文件的硬链接)。

理解了文件描述符,我们就能明白所有重定向语法的本质:通过特定符号修改文件描述符的指向,从而改变命令的输入、输出或错误流的目的地。

二、基础重定向:标准流的输入输出控制

基础重定向主要围绕 0(stdin)、1(stdout)、2(stderr)三个标准文件描述符,实现输出写入文件、输入读取文件、丢弃输出等常用操作。

2.1 标准输出重定向(stdout)

标准输出重定向用于将命令的正常输出写入文件,而不是显示在终端上。核心语法有两种:覆盖写入和追加写入。

(1)覆盖写入:cmd > file 或 cmd 1> file

  • 功能:将 cmd 的 stdout 写入 file,如果文件已存在则覆盖原有内容;如果文件不存在则创建。
  • 说明:1 是 stdout 的默认文件描述符,cmd > file 是 cmd 1> file 的简写,两者完全等价。
  • 示例:
    # 将 ls 命令的输出写入 filelist.txt(覆盖原有内容)
    ls /home/user > filelist.txt
    
    # 等价于上面的命令,显式指定文件描述符 1
    ls /home/user 1> filelist.txt

(2)追加写入:cmd >> file

  • 功能:将 cmd 的 stdout 追加到 file 的尾部,不会覆盖原有内容,只会在文件末尾添加新内容。
  • 示例:
    # 将当前日期追加到 log.txt 文件尾部
    date >> log.txt
    
    # 多次执行会持续追加,不会覆盖之前的日志
    echo "执行了备份操作" >> log.txt

2.2 标准错误重定向(stderr)

标准错误重定向用于单独处理命令的错误输出,比如将错误信息写入日志文件,避免干扰正常输出。

(1)覆盖写入:cmd 2> file

  • 功能:将 cmd 的 stderr 写入 file,覆盖原有内容(文件不存在则创建)。
  • 示例:
    # 尝试读取不存在的文件,将错误信息写入 error.log
    cat /nonexistent/file 2> error.log
    
    # 查看错误日志
    cat error.log  # 输出:cat: /nonexistent/file: 没有那个文件或目录

(2)追加写入:cmd 2>> file

  • 功能:将 cmd 的 stderr 追加到 file 尾部,不覆盖原有内容。
  • 示例:
    # 循环执行错误命令,将所有错误信息追加到 error.log
    for i in {1..5}; do
      cat /nonexistent/file$i 2>> error.log
    done

2.3 同时重定向 stdout 和 stderr

在实际使用中,我们经常需要将命令的正常输出和错误输出同时写入同一个文件(比如完整记录脚本运行日志),有两种常用语法。

(1)简洁语法:cmd &> file(bash 特有)

  • 功能:将 stdout 和 stderr 同时写入 file,覆盖原有内容;使用 &>> file 可实现追加。
  • 示例:
    # 将备份脚本的所有输出(正常+错误)写入 backup.log(覆盖)
    ./backup.sh &> backup.log
    
    # 追加模式:将输出添加到日志文件尾部
    ./backup.sh &>> backup.log

(2)兼容语法:cmd > file 2>&1(跨 Shell 支持)

  • 功能:与 &> file 效果相同,但兼容性更好(支持 bash、sh、zsh 等所有 Shell)。
  • 关键注意点:重定向顺序至关重要!
    • 正确写法:cmd > file 2>&1(先将 stdout 指向 file,再将 stderr 复制到 stdout 的指向,最终两者都写入 file)。
    • 错误写法:cmd 2>&1 > file(先将 stderr 复制到 stdout 的默认指向(终端),再将 stdout 指向 file,最终只有 stdout 写入 file,stderr 仍输出到终端)。
  • 示例:
    # 兼容写法,将 stdout 和 stderr 同时写入 log.txt
    ./script.sh > log.txt 2>&1
    
    # 追加模式:在兼容写法中,追加符号需加在 stdout 重定向上
    ./script.sh >> log.txt 2>&1

2.4 丢弃输出:重定向到 /dev/null

/dev/null 是 Linux 中的「空设备文件」,它会丢弃所有写入它的数据。常用于屏蔽命令的输出或错误信息。

(1)丢弃 stdout:cmd > /dev/null

  • 功能:执行命令,但不显示正常输出(错误输出仍会显示)。
  • 示例:
    # 安装软件时,屏蔽正常的进度输出,只显示错误信息
    sudo apt install nginx > /dev/null

(2)丢弃 stderr:cmd 2> /dev/null

  • 功能:执行命令,屏蔽错误信息(正常输出仍会显示)。
  • 示例:
    # 查找文件时,忽略「权限不够」的错误提示
    find / -name "config.json" 2> /dev/null

(3)完全静默运行:cmd &> /dev/null

  • 功能:同时丢弃 stdout 和 stderr,命令执行时不产生任何输出(完全静默)。
  • 示例:
    # 后台运行脚本,不输出任何信息
    ./background_task.sh &> /dev/null &

2.5 标准输入重定向(stdin)

标准输入重定向用于将文件内容作为命令的输入,而不是从键盘读取输入。核心语法是 cmd < file。

  • 功能:将 file 的内容作为 cmd 的 stdin(标准输入)。
  • 适用场景:命令本身支持从 stdin 读取数据(比如 cat、grep、awk、read 等)。
  • 示例:
    # 用 cat 命令读取文件(等价于 cat file.txt,但展示了输入重定向)
    cat < file.txt
    
    # 用 grep 从文件中查找关键词(等价于 grep "hello" file.txt)
    grep "hello" < file.txt
    
    # 用 read 命令从文件中读取一行内容赋值给变量
    read line < file.txt
    echo $line  # 输出 file.txt 的第一行内容
    
    # 批量导入数据:将 data.txt 的内容作为 mysql 命令的输入
    mysql -u root -p < data.txt

三、高级输入:此处文档与此处字符串

除了从文件读取输入,Shell 还支持通过「此处文档」(Here-document)和「此处字符串」(Here-string)直接将字符串(单行或多行)作为命令的 stdin,无需创建临时文件。

3.1 此处文档(Here-document):cmd << EOL ... EOL

此处文档用于将多行文本作为命令的输入,适用于需要输入多行内容的场景(比如编写配置文件、批量执行命令等)。

(1)基本语法

cmd << EOL
第一行文本
第二行文本
变量或命令会被展开:$HOME $(date)
EOL
  • 核心规则:
    • 起始标记:<< EOL(EOL 是自定义标记,可替换为任意字符串,如 << END、<< MARKER)。
    • 结束标记:单独一行书写的 EOL(前后不能有空格,否则无法识别)。
    • 变量展开:默认情况下,文本中的变量(如 $HOME)、命令替换(如 $(date))会被 Shell 展开。
  • 示例:
    # 用 cat 命令创建多行文件(无需手动编辑,直接写入内容)
    cat > config.conf << EOL
    # 配置文件:$(date) 生成
    username=$USER
    home_dir=$HOME
    log_level=info
    EOL
    
    # 查看生成的文件(变量已展开)
    cat config.conf
    # 输出:
    # # 配置文件:2024年5月20日 14:30:00 生成
    # username=user
    # home_dir=/home/user
    # log_level=info
    
    # 批量执行 ssh 命令(无需交互输入多行命令)
    ssh user@remote-host << EOL
    echo "开始执行远程操作"
    ls /home/user
    date >> /tmp/remote_log.txt
    EOL

(2)原样解析:引号包裹结束标记

如果希望文本中的变量、命令替换不被展开(原样保留),可以用引号(单引号或双引号)包裹结束标记。

语法:

cmd << "EOL"  # 或 'EOL'
变量不会被展开:$HOME $(date)
EOL

示例:

cat > script.sh << "EOL"
#!/bin/bash
echo "当前用户:$USER"  # 变量 $USER 会在脚本运行时展开,而非此处文档生成时
echo "当前日期:$(date)"
EOL

# 查看生成的脚本(变量和命令替换原样保留)
cat script.sh
# 输出:
# #!/bin/bash
# echo "当前用户:$USER"
# echo "当前日期:$(date)"

(3)忽略行首制表符:cmd <<- EOL

如果希望此处文档的多行文本可以缩进(便于代码格式化),可以在起始标记前加 -,此时行首的制表符(Tab)会被自动忽略(空格不忽略)。

语法:

cmd <<- EOL
    行首的制表符会被忽略
        多行缩进也可以
EOL

示例:

# 代码中使用缩进,生成的文件无缩进(更整洁)
cat > nginx.conf <<- EOL
    server {
        listen 80;
        server_name example.com;
        root /var/www/html;
    }
EOL

# 查看生成的文件(行首制表符已被移除)
cat nginx.conf
# 输出:
# server {
#     listen 80;
#     server_name example.com;
#     root /var/www/html;
# }

3.2 此处字符串(Here-string):cmd <<< "string"

此处字符串是此处文档的简化版,用于将单行字符串作为命令的输入,语法更简洁。

  • 语法:cmd <<< "string"(字符串可加引号,也可不加,但含空格时必须加)。
  • 适用场景:只需输入单行内容,比此处文档更高效(无需写结束标记)。
  • 示例:
    # 用 grep 查找字符串中的关键词(等价于 echo "hello world" | grep "world")
    grep "world" <<< "hello world"  # 输出:hello world
    
    # 用 awk 处理单行数据
    awk -F ',' '{print $2}' <<< "apple,banana,orange"  # 输出:banana
    
    # 用 read 命令读取单行字符串赋值给变量
    read name age <<< "Alice 25"
    echo "姓名:$name,年龄:$age"  # 输出:姓名:Alice,年龄:25
    
    # 传递含空格的字符串(必须加引号)
    echo <<< "Hello, this is a here-string"  # 输出:Hello, this is a here-string

四、自定义文件描述符:扩展输入输出能力

除了 0、1、2 三个标准文件描述符,Shell 允许用户自定义 3-9 之间的文件描述符,用于同时操作多个文件或资源(比如同时读取多个文件、写入多个日志等)。

4.1 自定义文件描述符的基本操作

自定义文件描述符的操作主要通过 exec 命令实现(exec 用于修改当前 Shell 进程的文件描述符配置),核心操作包括:打开文件、关闭文件、复制文件描述符。

(1)打开文件(读写模式)

  • 只读模式:exec 3< file(用描述符 3 打开文件,只能读取)。
  • 只写模式:exec 3> file(用描述符 3 打开文件,只能写入,覆盖原有内容)。
  • 读写模式:exec 3<> file(用描述符 3 打开文件,可读写)。
  • 示例:
    # 用描述符 3 只读打开文件
    exec 3< file.txt
    
    # 从描述符 3 读取内容(用 cat <&3)
    cat <&3  # 输出 file.txt 的内容
    
    # 关闭描述符 3(用完必须关闭,避免资源泄漏)
    exec 3>&-
    
    # 用描述符 4 只写打开文件(覆盖)
    exec 4> output.txt
    
    # 向描述符 4 写入内容(用 echo "content" >&4)
    echo "自定义描述符写入" >&4
    echo "多行内容" >&4
    
    # 关闭描述符 4
    exec 4>&-
    
    # 查看写入的内容
    cat output.txt  # 输出:自定义描述符写入、多行内容

(2)关闭文件描述符:exec 3>&-

  • 功能:关闭自定义文件描述符 3(释放对应的文件资源)。
  • 注意:自定义文件描述符如果不手动关闭,会一直占用资源,直到当前 Shell 会话结束。因此,使用完后必须关闭。

(3)复制文件描述符:exec 4>&3

  • 功能:将文件描述符 3 的指向复制给 4,此时 4 和 3 指向同一个文件/资源(相当于「硬链接」)。
  • 衍生用法:exec 4>&3-(复制后关闭原描述符 3)。
  • 示例:
    # 打开文件并复制描述符
    exec 3< file.txt  # 3 只读打开 file.txt
    exec 4>&3         # 4 复制 3 的指向(也指向 file.txt)
    
    # 从 3 读取内容(会移动文件指针)
    head -n 1 <&3  # 输出第一行
    
    # 从 4 读取内容(文件指针已被 3 移动,所以从第二行开始)
    cat <&4  # 输出第二行及以后的内容
    
    # 关闭描述符
    exec 3>&-
    exec 4>&-
    
    # 复制后关闭原描述符(常用场景:传递文件描述符后释放原资源)
    exec 5> log.txt
    exec 6>&5-  # 6 复制 5 的指向,然后关闭 5
    echo "通过复制的描述符写入" >&6
    exec 6>&-

4.2 自定义文件描述符的实际应用场景

(1)同时读写多个文件

# 打开两个日志文件,分别写入不同类型的日志
exec 3> info.log   # 普通日志
exec 4> error.log  # 错误日志

# 向不同描述符写入内容
echo "[$(date)] 程序启动成功" >&3
echo "[$(date)] 数据库连接失败" >&4

# 关闭描述符
exec 3>&-
exec 4>&-

(2)永久重定向当前会话的输出

# 永久重定向当前会话的 stderr 到 error.log(所有后续命令的错误都写入该文件)
exec 2> error.log

# 后续命令的错误会自动写入 error.log,不再显示在终端
ls /nonexistent  # 错误信息写入 error.log
cat /invalid/file # 错误信息写入 error.log

# 取消永久重定向(将 stderr 恢复指向终端)
exec 2>&1

(3)通过文件描述符实现网络通信

Shell 支持通过 /dev/tcp/host/port 和 /dev/udp/host/port 打开网络连接,并通过自定义文件描述符进行读写(相当于简单的网络客户端)。

示例:测试端口连通性(替代 telnet 或 nc)

# 打开到 example.com 80 端口的 TCP 连接(描述符 3)
exec 3<> /dev/tcp/example.com/80

# 向服务器发送 HTTP 请求(通过描述符 3 写入)
echo -e "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n" >&3

# 读取服务器响应(通过描述符 3 读取)
cat <&3

# 关闭连接
exec 3>&-

示例:UDP 端口测试

# 打开到 127.0.0.1 53 端口的 UDP 连接
exec 3<> /dev/udp/127.0.0.1/53

# 发送 UDP 数据包(DNS 查询示例,需配合 DNS 协议格式,此处仅演示)
echo -n -e "\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03\x77\x77\x77\x06\x67\x6f\x6f\x67\x6c\x65\x03\x63\x6f\x6d\x00\x00\x01\x00\x01" >&3

# 读取响应
cat <&3

# 关闭连接
exec 3>&-

五、高级重定向:进程替换与管道进阶

5.1 进程替换(Process Substitution)

进程替换是 Shell 的高级特性,它允许将一个命令的输出(或输入)作为另一个命令的「文件参数」,本质是创建一个匿名的命名管道(FIFO)来传递数据。

核心语法有四种:

  • 标准输出替换:cmd <(cmd1)(将 cmd1 的 stdout 作为 cmd 的文件参数)。
  • 标准输入替换:cmd >(cmd2)(将 cmd2 的 stdin 作为 cmd 的文件参数)。
  • 多进程替换:cmd <(cmd1) <(cmd2)(多个命令的 stdout 作为 cmd 的参数)。
  • 结合 stdin 重定向:cmd < <(cmd1)(将 cmd1 的 stdout 作为 cmd 的 stdin)。

(1)基础用法:对比两个命令的输出

进程替换最常用的场景是对比两个命令的输出(比如对比两个目录的文件列表、两个日志文件的差异)。

示例:对比两个目录的文件列表

# 查找 /path1 和 /path2 的文件列表,排序后对比差异
diff <(find /path1 | sort) <(find /path2 | sort)
  • 原理:
    • find /path1 | sort 的输出被写入一个匿名 FIFO。
    • find /path2 | sort 的输出被写入另一个匿名 FIFO。
    • diff 命令接收这两个 FIFO 的路径作为参数,对比它们的内容。
  • 等价于(但无需创建临时文件):
    find /path1 | sort > temp1.txt
    find /path2 | sort > temp2.txt
    diff temp1.txt temp2.txt
    rm temp1.txt temp2.txt

(2)多进程替换:传递多个输入源

示例:用 awk 同时处理两个命令的输出

# 统计两个日志文件中包含 "error" 的行数,并对比
awk '
  NR==FNR { count1++ }  # 处理第一个输入(<(grep "error" log1.txt))
  NR!=FNR { count2++ }  # 处理第二个输入(<(grep "error" log2.txt))
  END {
    print "log1.txt 错误行数:" count1
    print "log2.txt 错误行数:" count2
  }
' <(grep "error" log1.txt) <(grep "error" log2.txt)

(3)反向进程替换:cmd1 >(cmd2)

cmd1 >(cmd2) 表示将 cmd2 的 stdin 作为 cmd1 的输出文件参数,即 cmd1 的输出写入匿名 FIFO,cmd2 从 FIFO 读取。

示例:同时保存输出到文件和显示在终端(替代 tee 命令)

# 将 ls 的输出同时写入 filelist.txt 和通过 grep 过滤显示
ls /home/user >(grep "doc" > filtered.txt) > filelist.txt
  • 原理:
    • ls /home/user 的 stdout 被重定向到两个地方:filelist.txt 和匿名 FIFO(grep "doc" 从 FIFO 读取)。
    • grep "doc" 过滤出包含 “doc” 的行,写入 filtered.txt。

(4)结合 stdin 重定向:cmd < <(cmd1)

这种写法等价于 cmd1 | cmd(管道),但在某些场景下更灵活(比如需要同时传递多个输入)。

示例:

# 等价于 cat file.txt | grep "hello"
grep "hello" < <(cat file.txt)

# 更实用的场景:同时传递两个输入
diff <(sort file1.txt) < <(sort file2.txt)  # 等价于 diff <(sort file1.txt) <(sort file2.txt)

5.2 管道进阶:| 与 |&

管道(|)是最常用的重定向工具之一,它的本质是将前一个命令的 stdout 重定向到后一个命令的 stdin。Shell 还提供了错误管道(|&),用于同时重定向 stdout 和 stderr。

(1)普通管道:cmd1 | cmd2

  • 功能:cmd1 的 stdout → cmd2 的 stdin。
  • 注意:管道默认只传递 stdout,stderr 仍会输出到终端。如果需要传递 stderr,需结合 2>&1。
  • 示例:
    # 过滤 ls 的输出,只显示 .txt 文件
    ls /home/user | grep "\.txt"
    
    # 传递 stderr 到管道(过滤错误信息)
    find / -name "config.json" 2>&1 | grep -v "权限不够"  # 排除「权限不够」的错误

(2)错误管道:cmd1 |& cmd2(bash 4.0+)

  • 功能:将 cmd1 的 stdout 和 stderr 同时传递到 cmd2 的 stdin(等价于 cmd1 2>&1 | cmd2)。
  • 适用场景:需要同时处理一个命令的正常输出和错误输出。
  • 示例:
    # 同时过滤正常输出和错误输出
    find / -name "config.json" |& grep -E "config.json|权限"  # 显示找到的文件和权限错误
    
    # 等价于兼容写法
    find / -name "config.json" 2>&1 | grep -E "config.json|权限"

(3)管道退出状态:${PIPESTATUS[@]}

默认情况下,Shell 只返回管道中最后一个命令的退出状态($?)。如果需要获取所有管道命令的退出状态,可以使用 ${PIPESTATUS[@]} 数组(bash 特有)。

  • 功能:${PIPESTATUS[@]} 存储管道中每个命令的退出码(按命令顺序排列)。
  • 退出码规则:0 表示成功,非 0 表示失败。
  • 示例:
    # 执行一个包含失败命令的管道
    ls /nonexistent | grep "file" | sort
    
    # 查看所有命令的退出状态
    echo ${PIPESTATUS[@]}  # 输出:2 1 0
    # 解释:
    # ls /nonexistent 失败(退出码 2)
    # grep "file" 没有匹配到内容(退出码 1)
    # sort 成功执行(退出码 0)
    
    # 捕获每个命令的退出码
    ls /nonexistent | grep "file" | sort
    status=("${PIPESTATUS[@]}")
    echo "ls 退出码:${status[0]}"
    echo "grep 退出码:${status[1]}"
    echo "sort 退出码:${status[2]}"

5.3 多命令重定向:子shell与大括号

如果需要将多个命令的输出统一重定向到文件,可以使用「子shell」或「大括号」语法,避免重复写重定向符号。

(1)子shell:(cmd1; cmd2) > file

  • 功能:创建一个子shell,在子shell中执行 cmd1 和 cmd2,将所有命令的 stdout 统一重定向到 file。
  • 特点:子shell中的变量修改、环境变量设置不会影响父shell。
  • 示例:
    # 将多个命令的输出统一写入 log.txt
    (
      echo "=== 脚本开始执行:$(date) ==="
      ls /home/user
      echo "=== 脚本执行结束 ==="
    ) > log.txt
    
    # 查看日志
    cat log.txt

(2)大括号:{ cmd1; cmd2; } > file

  • 功能:与子shell类似,但不创建子shell(直接在当前shell中执行命令),效率更高。
  • 关键注意点:
    • { 后必须有空格。
    • } 前必须有分号(;)或换行。
    • 命令执行后的变量修改、环境变量设置会影响当前shell。
  • 示例:
    # 不创建子shell,统一重定向输出
    {
      echo "=== 系统信息:$(date) ==="
      uname -a
      free -h
    } >> system.log  # 追加模式
    
    # 变量修改会影响当前shell
    {
      name="Alice"
      age=25
    } > /dev/null
    echo $name $age  # 输出:Alice 25(变量已生效)

六、分流输出:同时处理 stdout 和 stderr

在某些场景下,我们需要将一个命令的 stdout 和 stderr 分别传递给不同的命令处理(比如 stdout 写入日志,stderr 发送邮件报警),这时可以结合重定向和进程替换实现分流。

核心语法:cmd > >(cmd1) 2> >(cmd2)

  • 功能:将 cmd 的 stdout 传递给 cmd1,stderr 传递给 cmd2(两个命令并行处理)。
  • 示例:
# 备份数据库:stdout 写入日志,stderr 发送邮件报警
mysqldump -u root -p database > >(tee backup.log) 2> >(mail -s "备份失败" admin@example.com)

# 解释:
# mysqldump 的 stdout → tee backup.log(同时写入文件和终端)
# mysqldump 的 stderr → mail 命令(发送报警邮件)

# 另一个示例:stdout 写入文件,stderr 过滤后显示
./script.sh > output.log 2> >(grep -E "ERROR|WARNING" >&2)
# 解释:
# stdout 写入 output.log
# stderr 过滤出 ERROR/WARNING 行,并重定向到终端(>&2 表示 stderr)

七、Shell 重定向语法速查表

重定向命令 描述
cmd > file 将 cmd 的标准输出(stdout)重定向到文件(覆盖文件内容)
cmd 1> file 与 cmd > file 相同,1 是 stdout 的默认文件描述符
cmd 2> file 将 cmd 的标准错误(stderr)重定向到文件(覆盖文件内容),2 是 stderr 的默认文件描述符
cmd >> file 将 cmd 的 stdout 追加到文件尾部(不覆盖原有内容)
cmd 2>> file 将 cmd 的 stderr 追加到文件尾部(不覆盖原有内容)
cmd &> file 将 cmd 的 stdout 和 stderr 同时重定向到文件(覆盖文件内容)
cmd > file 2>&1 另一种将 stdout 和 stderr 重定向到文件的方式(覆盖文件内容)
⚠️ 注意:与 cmd 2>&1 > file 不同,重定向顺序至关重要!
cmd > /dev/null 丢弃 cmd 的 stdout(不输出任何内容)
cmd 2> /dev/null 丢弃 cmd 的 stderr(不输出错误信息)
cmd &> /dev/null 同时丢弃 cmd 的 stdout 和 stderr(完全静默运行)
cmd < file 将文件内容作为 cmd 的标准输入(stdin)
cmd << EOL<br>foo<br>bar<br>baz<br>EOL 此处文档(here-document):将多行文本作为 cmd 的 stdin
如果 EOL 用引号括起(如 << "EOL"),文本将被原样解析(不展开变量/命令)
cmd <<- EOL<br> foo<br> bar<br>EOL 带制表符忽略的此处文档:与普通此处文档类似,但会忽略行首的制表符(空格不忽略),便于格式化代码
cmd <<< "string" 此处字符串(here-string):将单行字符串作为 cmd 的 stdin
exec 2> file 永久重定向所有命令的 stderr 到文件(当前终端会话生效)
exec 3< file 使用自定义文件描述符 3 打开文件(只读模式)
exec 3> file 使用自定义文件描述符 3 打开文件(只写模式,覆盖原有内容)
exec 3<> file 使用自定义文件描述符 3 打开文件(读写模式)
exec 3>&- 关闭自定义文件描述符 3
exec 4>&3 将文件描述符 3 复制到 4(4 成为 3 的副本,指向同一文件/流)
exec 4>&3- 将文件描述符 3 复制到 4 后,关闭文件描述符 3
echo "foo" >&3 向自定义文件描述符 3 写入内容
cat <&3 从自定义文件描述符 3 读取内容
(cmd1; cmd2) > file 使用子shell将多个命令的 stdout 统一重定向到文件
{ cmd1; cmd2; } > file 不使用子shell(效率更高),将多个命令的 stdout 统一重定向到文件
⚠️ 注意:{ 后和 } 前必须有空格,且命令末尾需加 ;
exec 3<> /dev/tcp/host/port 打开到 host:port 的 TCP 连接,使用文件描述符 3 进行读写
exec 3<> /dev/udp/host/port 打开到 host:port 的 UDP 连接,使用文件描述符 3 进行读写
cmd <(cmd1) 进程替换:将 cmd1 的 stdout 重定向到匿名 FIFO(命名管道),并将 FIFO 路径作为参数传递给 cmd
适用于 cmd 不直接读取 stdin 的场景
cmd < <(cmd1) 进程替换+stdin重定向:将 cmd1 的 stdout 重定向到匿名 FIFO,再将 FIFO 作为 cmd 的 stdin
示例:`diff <(find /path1
cmd <(cmd1) <(cmd2) 多进程替换:将 cmd1 和 cmd2 的 stdout 分别重定向到两个匿名 FIFO,均作为参数传递给 cmd
cmd1 >(cmd2) 进程替换(反向):运行 cmd2 并将其 stdin 连接到匿名 FIFO,将 FIFO 路径作为参数传递给 cmd1
即 cmd1 的输出写入 FIFO,cmd2 从 FIFO 读取
cmd1 | cmd2 管道:将 cmd1 的 stdout 重定向到 cmd2 的 stdin
💡 进阶:与 cmd1 > >(cmd2)、cmd2 < <(cmd1) 效果相同
cmd1 |& cmd2 错误管道(bash 4.0+):将 cmd1 的 stdout 和 stderr 同时重定向到 cmd2 的 stdin
旧版本 bash 需使用 cmd1 2>&1 | cmd2
cmd | tee file 分流输出:将 cmd 的 stdout 同时输出到屏幕和文件(覆盖文件内容)
使用 tee -a file 可实现追加模式
exec {filew}> file 命名文件描述符(bash 4.1+):使用命名变量 filew 打开文件(只写模式),避免硬编码数字描述符
cmd 3>&1 1>&2 2>&3 交换 stdout 和 stderr:通过临时文件描述符 3 实现两者互换
cmd > >(cmd1) 2> >(cmd2) 分流 stdout/stderr:将 cmd 的 stdout 传递给 cmd1,stderr 传递给 cmd2(并行处理)
cmd1 | cmd2 | cmd3 | cmd4<br>echo ${PIPESTATUS[@]} 管道退出状态:${PIPESTATUS[@]} 数组存储所有管道命令的退出码(按顺序排列),默认仅返回最后一个命令的退出码

八、总结与最佳实践

Shell 重定向是命令行效率的「倍增器」,掌握它可以让你轻松应对日志记录、自动化脚本、进程间通信等复杂场景。以下是核心总结和最佳实践:

核心总结

  1. 文件描述符是基础:记住 0(stdin)、1(stdout)、2(stderr)三个标准描述符,自定义描述符使用 3-9。
  2. 重定向顺序很关键:cmd > file 2>&1 是正确的,cmd 2>&1 > file 是错误的(顺序颠倒会导致 stderr 无法重定向到文件)。
  3. 按需选择工具:
    • 单行输入用「此处字符串」(<<<),多行输入用「此处文档」(<< EOL)。
    • 对比命令输出用「进程替换」(<(cmd)),避免创建临时文件。
    • 同时处理多个文件用「自定义文件描述符」。
  4. 资源释放:自定义文件描述符使用后必须关闭(exec 3>&-),避免资源泄漏。

最佳实践

  1. 日志记录:脚本中使用 exec 2> error.log 永久重定向 stderr,或 ./script.sh &>> log.txt 记录所有输出。
  2. 静默执行:后台运行的命令使用 &> /dev/null 屏蔽输出,避免干扰终端。
  3. 批量操作:使用此处文档批量生成配置文件、执行远程命令,无需手动编辑。
  4. 错误处理:通过 |& 或 2>&1 | 传递 stderr,结合 grep 过滤错误信息,或发送报警邮件。
  5. 兼容性考虑:跨 Shell 脚本优先使用 cmd > file 2>&1 而非 &> file,使用 cmd 2>&1 | cmd2 而非 |&。

通过本文的学习,相信你已经掌握了 Shell 重定向的全部核心用法。在实际使用中,建议多尝试将这些语法结合起来,比如「自定义文件描述符 + 进程替换 + 管道」,可以实现更复杂的需求。记住,最好的学习方式是实践——多写脚本、多测试,才能真正将这些知识内化为自己的技能。

原文 https://blog.csdn.net/2301_79518550/article/details/144012854