输入输出重定向
输入输出重定向是 Shell 最强大的特性之一。它允许你控制命令的输入来源和输出目标,将命令组合成复杂的数据处理流程。本章详细介绍文件描述符、重定向操作符、Here Document、管道等核心概念。
文件描述符
在 Unix/Linux 系统中,每个进程都有三个标准文件描述符(File Descriptor),它们是进程与外部世界通信的通道。
| 文件描述符 | 名称 | 默认设备 | 用途 |
|---|---|---|---|
| 0 | 标准输入(stdin) | 键盘 | 程序读取输入 |
| 1 | 标准输出(stdout) | 屏幕 | 程序输出正常结果 |
| 2 | 标准错误(stderr) | 屏幕 | 程序输出错误信息 |
文件描述符本质上是一个非负整数,内核用它来跟踪进程打开的文件。当你打开一个文件时,内核返回一个文件描述符,后续的读写操作都通过这个描述符进行。
查看文件描述符
# 查看当前 Shell 打开的文件描述符
ls -la /proc/$$/fd
# 输出示例:
# lrwx------ 1 user user 64 Jan 1 00:00 0 -> /dev/pts/0
# lrwx------ 1 user user 64 Jan 1 00:00 1 -> /dev/pts/0
# lrwx------ 1 user user 64 Jan 1 00:00 2 -> /dev/pts/0
为什么需要重定向
默认情况下,命令从键盘读取输入,将结果输出到屏幕。但在很多场景下,我们需要改变这种默认行为:
- 将命令输出保存到文件
- 让命令从文件读取输入
- 将错误信息与正常输出分开处理
- 将多个命令串联起来,前一个的输出作为后一个的输入
输出重定向
覆盖写入 >
> 将命令的标准输出重定向到指定文件。如果文件不存在,会创建新文件;如果文件存在,会清空原有内容。
# 将 ls 命令的输出保存到文件
ls -la > filelist.txt
# 将命令输出重定向到文件
echo "Hello, World" > greeting.txt
# 清空文件内容
> empty.txt
注意:覆盖写入会丢失原有内容。如果你不确定文件是否重要,可以先备份或使用追加模式。
追加写入 >>
>> 将命令的标准输出追加到文件末尾,不会清空原有内容。
# 追加内容到日志文件
echo "$(date): Script started" >> script.log
# 收集多次命令的输出
for i in 1 2 3; do
echo "Iteration $i" >> results.txt
done
标准错误重定向 2> 和 2>>
标准错误和标准输出是两个独立的流,需要分别重定向。
# 将错误信息重定向到文件
ls /nonexistent 2> errors.log
# 追加错误信息
find / -name "*.conf" 2>> errors.log
# 丢弃错误信息(发送到 /dev/null)
ls /nonexistent 2> /dev/null
/dev/null 是一个特殊的设备文件,写入它的数据会被丢弃,读取它会立即返回 EOF。它常用于丢弃不需要的输出。
同时重定向标准输出和标准错误
# 方式一:分别重定向到不同文件
command > output.txt 2> error.txt
# 方式二:重定向到同一个文件(传统方式)
command > all.txt 2>&1
# 方式三:重定向到同一个文件(Bash 简写)
command &> all.txt
# 方式四:追加模式
command &>> all.txt
重要:重定向的顺序非常关键。考虑这两个命令的区别:
# 正确:先将 stdout 重定向到文件,再将 stderr 重定向到 stdout
ls > output.txt 2>&1
# 结果:stdout 和 stderr 都写入 output.txt
# 错误:先将 stderr 重定向到 stdout(此时指向屏幕),再将 stdout 重定向到文件
ls 2>&1 > output.txt
# 结果:stdout 写入 output.txt,stderr 仍然输出到屏幕
Bash 从左到右处理重定向。在第一个命令中,> output.txt 先将 fd 1 指向文件,然后 2>&1 将 fd 2 指向 fd 1 当前指向的目标(文件)。在第二个命令中,2>&1 时 fd 1 还指向屏幕,所以 fd 2 也指向屏幕。
丢弃所有输出
# 丢弃所有输出
command > /dev/null 2>&1
command &> /dev/null
输入重定向
从文件输入 <
# 从文件读取输入
wc -l < file.txt
# 排序文件内容
sort < unsorted.txt
# 统计单词数
wc -w < document.txt
与 cat file.txt | wc -l 不同,wc -l < file.txt 直接将文件作为 wc 的标准输入,不经过管道,效率更高。
Here Document <<
Here Document 允许在脚本中嵌入多行文本,作为命令的标准输入。
# 基本语法
command << DELIMITER
text
DELIMITER
# 示例:创建配置文件
cat > config.ini << EOF
[database]
host = localhost
port = 3306
user = root
password = secret
EOF
# 示例:发送邮件
mail -s "Report" [email protected] << END
Dear User,
Here is the weekly report.
Please review it at your earliest convenience.
Best regards,
Admin
END
变量替换
默认情况下,Here Document 内的变量和命令会被替换:
name="World"
cat << EOF
Hello, $name!
Current directory: $(pwd)
Date: $(date)
EOF
# 输出:
# Hello, World!
# Current directory: /home/user
# Date: Fri Jan 1 00:00:00 UTC 2024
禁用变量替换
在分隔符前后加单引号,可以禁用变量替换:
cat << 'EOF'
$HOME will not be expanded
$(pwd) will not execute
EOF
# 输出:
# $HOME will not be expanded
# $(pwd) will not execute
去除前导制表符
使用 <<- 可以去除每行开头的制表符(注意:只能去除制表符,不能去除空格):
cat <<- EOF
This line starts with a tab
This line has two tabs
EOF
# 输出:
# This line starts with a tab
# This line has two tabs
Here String <<<
Here String 是 Here Document 的简化形式,用于传递单行字符串:
# 基本用法
grep "pattern" <<< "search in this string"
# 使用变量
name="World"
cat <<< "Hello, $name!"
# 传递多行(使用换行符)
awk '{print NR, $0}' <<< "line1
line2
line3"
Here String 会自动在字符串末尾添加换行符,所以命令能正确读取。
文件描述符操作
打开文件描述符
使用 exec 命令可以打开、关闭和操作文件描述符。
# 打开文件描述符用于写入
exec 3> output.txt
echo "Line 1" >&3
echo "Line 2" >&3
# 打开文件描述符用于读取
exec 4< input.txt
read -u 4 line
echo "Read: $line"
# 打开文件描述符用于读写
exec 5<> data.txt
echo "Write something" >&5
read -u 5 line
关闭文件描述符
# 关闭输出文件描述符
exec 3>&-
# 关闭输入文件描述符
exec 4<&-
# 关闭读写文件描述符
exec 5>&-
复制文件描述符
# 保存当前 stdout
exec 3>&1
# 重定向 stdout 到文件
exec > log.txt
echo "This goes to file"
# 恢复 stdout
exec >&3
exec 3>&-
echo "This goes to screen"
实际应用:同时写入文件和屏幕
#!/bin/bash
# 打开文件描述符 3 指向日志文件
exec 3> script.log
# 创建日志函数
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee /dev/fd/3
}
log "Script started"
log "Processing data..."
log "Script finished"
# 关闭文件描述符
exec 3>&-
使用命名文件描述符
Bash 4.1+ 支持命名文件描述符,使代码更易读:
# 打开命名文件描述符
exec {log_fd}> application.log
# 使用命名文件描述符
echo "Log message" >&$log_fd
# 关闭
exec {log_fd}>&-
特殊文件
Bash 对一些特殊的文件名有特殊处理:
| 文件 | 说明 |
|---|---|
/dev/null | 丢弃所有写入,读取返回 EOF |
/dev/zero | 读取时返回无限个零字节 |
/dev/random | 读取时返回随机字节 |
/dev/urandom | 读取时返回随机字节(非阻塞) |
/dev/stdin | 等同于文件描述符 0 |
/dev/stdout | 等同于文件描述符 1 |
/dev/stderr | 等同于文件描述符 2 |
/dev/fd/n | 等同于文件描述符 n |
/dev/tcp/host/port | TCP 连接 |
/dev/udp/host/port | UDP 连接 |
网络重定向
# 通过 TCP 发送 HTTP 请求
exec 3<>/dev/tcp/www.example.com/80
echo -e "GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n" >&3
cat <&3
exec 3>&-
# 检查端口是否开放
(echo > /dev/tcp/google.com/80) 2>/dev/null && echo "Port 80 is open"
管道
管道是连接命令的标准机制,它将一个命令的标准输出连接到另一个命令的标准输入。
基本用法
# 简单管道
ls | grep ".txt"
# 多级管道
cat access.log | grep "error" | sort | uniq -c | sort -rn | head -10
# 管道与重定向结合
ls | grep ".txt" > txt_files.txt
管道的工作原理
管道是内核提供的一种进程间通信机制。当你执行 cmd1 | cmd2 时:
- Shell 创建一个管道(两个文件描述符:一个用于读,一个用于写)
- Shell 创建两个子进程
- 第一个子进程将 stdout 重定向到管道的写端,执行 cmd1
- 第二个子进程将 stdin 重定向到管道的读端,执行 cmd2
- 数据从 cmd1 流向 cmd2
管道中的退出状态
默认情况下,管道的退出状态是最后一个命令的状态:
false | true | false
echo $? # 输出:1(最后一个命令的状态)
使用 PIPESTATUS 数组可以获取每个命令的退出状态:
false | true | false
echo "${PIPESTATUS[@]}" # 输出:1 0 1
使用 set -o pipefail 可以让管道在任意命令失败时返回失败:
set -o pipefail
false | true | false
echo $? # 输出:1
tee 命令
tee 命令将输入同时输出到文件和标准输出,就像管道的 T 型接头。
# 保存中间结果
ls | tee filelist.txt | grep ".txt"
# 同时保存到多个文件
ls | tee filelist1.txt filelist2.txt
# 追加模式
ls | tee -a log.txt | grep ".txt"
进程替换
进程替换将命令的输出(或输入)表现为文件,可以用于需要文件参数的场景。
输出替换 >(...)
# 将输出同时发送到多个命令
echo "Hello" | tee >(grep -o "H" > h.txt) >(grep -o "e" > e.txt)
# 比较两个目录
diff <(ls dir1) <(ls dir2)
输入替换 <(...)
# 合并并排序多个文件
sort -m <(sort file1.txt) <(sort file2.txt)
# 对比命令输出
diff <(grep "error" log1.txt) <(grep "error" log2.txt)
命名管道(FIFO)
命名管道是一种特殊类型的文件,允许不相关的进程进行通信。
创建和使用
# 创建命名管道
mkfifo mypipe
# 或者使用 mknod
mknod mypipe p
# 在一个终端写入
echo "Hello from writer" > mypipe
# 在另一个终端读取
cat < mypipe
# 输出:Hello from writer
# 删除命名管道
rm mypipe
实际应用
#!/bin/bash
pipe="/tmp/my_pipe"
# 创建命名管道
[ -p "$pipe" ] || mkfifo "$pipe"
# 消费者:后台读取
while read line < "$pipe"; do
echo "Received: $line"
done &
# 生产者:写入数据
echo "Message 1" > "$pipe"
echo "Message 2" > "$pipe"
# 清理
rm "$pipe"
常见陷阱
重定向顺序错误
# 错误:stderr 仍然输出到屏幕
command 2>&1 > output.txt
# 正确:先重定向 stdout,再复制到 stderr
command > output.txt 2>&1
覆盖输入文件
# 错误:文件在读取前就被清空
cat file.txt | sort > file.txt
# 正确:使用临时文件
sort file.txt > file.txt.tmp && mv file.txt.tmp file.txt
# 或者使用 sponge(moreutils 包)
sort file.txt | sponge file.txt
# 或者使用 -o 选项(部分命令支持)
sort -o file.txt file.txt
权限问题
# 错误:重定向由当前用户执行,可能没有权限
echo "content" | sudo > /root/file.txt
# 正确:使用 tee 或 dd
echo "content" | sudo tee /root/file.txt > /dev/null
子 Shell 中的变量
# 错误:管道在子 Shell 中执行,变量修改无效
echo "hello" | read var
echo $var # 输出为空
# 解决方案一:使用进程替换
read var <<< "hello"
echo $var # 输出:hello
# 解决方案二:使用 lastpipe
shopt -s lastpipe
echo "hello" | read var
echo $var # 输出:hello
# 解决方案三:使用命令替换
var=$(echo "hello")
实用示例
日志记录脚本
#!/bin/bash
LOG_FILE="script.log"
# 重定向所有输出到日志文件
exec > >(tee -a "$LOG_FILE")
exec 2>&1
echo "Script started at $(date)"
echo "Doing some work..."
echo "Script finished"
交互式命令自动化
#!/bin/bash
# 自动回答交互式命令的问题
ftp -n ftp.example.com << EOF
user username password
cd /remote/path
get file.txt
bye
EOF
# 自动输入密码(注意:密码会暴露在进程列表中)
mysql -u root -p << EOF
your_password
SHOW DATABASES;
EOF
并发数据处理
#!/bin/bash
# 使用命名管道实现生产者-消费者模式
pipe=/tmp/data_pipe
[ -p "$pipe" ] || mkfifo "$pipe"
# 消费者:处理数据
process_data() {
while read data; do
echo "Processing: $data"
sleep 0.1
done < "$pipe"
}
# 启动多个消费者
for i in 1 2 3; do
process_data &
done
# 生产者:发送数据
for item in {1..20}; do
echo "Item $item" > "$pipe"
done
wait
rm "$pipe"
捕获命令输出和错误
#!/bin/bash
# 捕获 stdout 和 stderr 到不同变量
{
output=$(command 2>&1 1>&3)
error=$?
} 3>&1
echo "Output: $output"
echo "Exit code: $error"
# 另一种方法:使用临时文件
tmp_file=$(mktemp)
output=$(command 2>"$tmp_file")
error=$(<"$tmp_file")
rm "$tmp_file"
echo "Output: $output"
echo "Error: $error"
实时日志处理
#!/bin/bash
# 实时处理日志文件
tail -f /var/log/app.log | while read line; do
# 提取错误信息
if echo "$line" | grep -q "ERROR"; then
# 发送告警
echo "$line" | mail -s "Error Alert" [email protected]
fi
# 写入统计
echo "$line" >> /var/log/app_archive.log
done
小结
本章详细介绍了 Shell 输入输出重定向:
- 文件描述符:理解 stdin、stdout、stderr 三个标准流
- 输出重定向:
>覆盖写入、>>追加写入、2>错误重定向 - 输入重定向:
<从文件输入、<<Here Document、<<<Here String - 文件描述符操作:使用
exec打开、关闭、复制文件描述符 - 管道:连接命令的输出和输入,
tee命令分发输出 - 进程替换:
<(...)和>(...)将命令输出作为文件 - 命名管道:FIFO 实现进程间通信
- 常见陷阱:重定向顺序、覆盖输入文件、权限问题
掌握输入输出重定向是编写复杂 Shell 脚本的关键技能。下一章将学习 Shell 的高级特性。