Linux Shell 数据呈现完全指南:从标准流重定向到临时文件实战
概述
在写 Shell 脚本或者日常运维时,命令 / 脚本跑出来的东西,通常就三种去向:
- 只往屏幕上打:图个快速看一眼结果(比如 ls、pwd);
- 只存进文件:留个日志或者数据底稿(比如脚本运行日志、统计结果);
- 屏幕和文件都留一份:一边实时看进度,一边永久存档(比如批量任务执行日志)。
想把这几种”数据分流”玩灵活,关键得理解 Linux 是怎么抽象输入 / 输出(I/O) 的——也就是标准文件描述符。这篇文章从基础概念讲起,一步步深入到重定向、自定义文件描述符、临时文件这些实战技巧,争取把 Shell 数据呈现的各种场景都讲透。
一、基础:Linux 标准文件描述符
Linux 把所有 I/O 操作都抽象成”文件操作”,用一个文件描述符(File Descriptor,FD) 来标记每一个打开的”文件对象”(键盘、显示器、真实的文件都算)。
Bash 默认留了前 3 个文件描述符,专门管默认的输入、输出和错误,数据重定向就是靠它们打底的。
1. 三大核心文件描述符
| 文件描述符(FD) | 缩写 | 名称 | 默认指向 | 功能说明 |
|---|---|---|---|---|
| 0 | STDIN | 标准输入 | 键盘 | 接收外部输入(如 read 命令、cat 无参时) |
| 1 | STDOUT | 标准输出 | 终端显示器 | 输出正常执行结果(如命令返回的有效数据) |
| 2 | STDERR | 标准错误输出 | 终端显示器 | 输出错误信息(如命令不存在、文件找不到) |
关键特性:
- 默认情况下,STDOUT 和 STDERR 都指向显示器,所以正常结果和错误信息会混在一起显示;
- 这三者是各走各的”流”,可以单独重定向(比如把错误存进日志,正常结果留在屏幕上)。
2. 标准输入(STDIN:FD 0)
STDIN 是 Shell 的”数据入口”,默认从键盘读东西。用输入重定向符 < 可以让它改成从文件读(相当于假装是键盘敲进去的)。
示例 1:默认从键盘读取(cat 命令)
cat 命令不带参数时,会一直从 STDIN 读,你敲一行它就回显一行(按 Ctrl+D 结束):
cat
# 输入:Hello Shell
# 输出:Hello Shell
# 输入:This is STDIN test
# 输出:This is STDIN test
示例 2:从文件读取(输入重定向)
用 < 让 cat 乖乖从 test.txt 读,而不是从键盘:
# 准备测试文件
echo "Line 1: From test.txt" > test.txt
echo "Line 2: From test.txt" >> test.txt
# 输入重定向:从文件读取
cat < test.txt
# 输出:
# Line 1: From test.txt
# Line 2: From test.txt
适用场景:给那些需要键盘输入的命令(比如 read、wc、sort)批量喂数据。
3. 标准输出(STDOUT:FD 1)
STDOUT 是 Shell 的”正常数据出口”,默认往显示器上写。用输出重定向符 > 或者追加符 >>,可以改成写进文件。
示例 1:覆盖写入文件(>)
将 ls -l 的正常结果写入 file_list.txt,覆盖原有内容(若文件不存在则创建):
ls -l > file_list.txt
# 查看结果
cat file_list.txt
# 输出:
# total 8
# -rw-r--r-- 1 user user 45 Jun 4 10:00 file_list.txt
# -rw-r--r-- 1 user user 60 Jun 4 09:30 test.txt
示例 2:追加写入文件(>>)
用 >> 将 who 命令的结果追加到 file_list.txt 末尾(不覆盖原有内容):
who >> file_list.txt
# 查看追加结果
cat file_list.txt
# 输出(新增最后一行):
# total 8
# -rw-r--r-- 1 user user 45 Jun 4 10:00 file_list.txt
# -rw-r--r-- 1 user user 60 Jun 4 09:30 test.txt
# user pts/0 2024-06-04 09:00 (192.168.1.100)
注意:> 会直接把原文件覆盖掉,用之前得想清楚要不要保留原来的内容;>> 更稳妥,适合往日志里追加。
4. 标准错误输出(STDERR:FD 2)
STDERR 专门用来输出错误信息,默认跟 STDOUT 一样指向显示器,但它不会跟着 STDOUT 的重定向一起跑——这是新手最容易栽的跟头。
示例:错误信息不随 STDOUT 重定向
尝试列出不存在的文件 badfile,并将 STDOUT 重定向到 output.txt:
# 执行命令:STDERR 未重定向
ls -l badfile > output.txt
# 屏幕输出(错误信息,未写入文件):
# ls: cannot access 'badfile': No such file or directory
# 查看 output.txt(空文件)
cat output.txt # 无内容
原因:STDERR(FD 2)和 STDOUT(FD 1)是各走各的,> 只把 STDOUT 重定向了,错误信息还是走它默认的”显示器”通道。
二、进阶:重定向错误与混合输出
实际用的时候,我们经常要”把错误和正常输出分开”(比如错误日志单独归档),或者”把它们合并到一起”(比如所有信息写进同一个日志),这都得专门对 STDERR 动手脚。
1. 仅重定向错误(STDERR:FD 2)
用 2>(2 代表 STDERR,> 代表重定向),就能单独把错误信息写进文件,不影响 STDOUT。
示例 1:错误写入文件,正常结果显示在屏幕
列出 test.txt(存在)和 badfile(不存在),仅将错误写入 error.log:
ls -l test.txt badfile 2> error.log
# 屏幕输出(STDOUT:仅正常结果):
# -rw-r--r-- 1 user user 60 Jun 4 09:30 test.txt
# 查看错误日志
cat error.log
# 输出(STDERR:仅错误信息):
# ls: cannot access 'badfile': No such file or directory
示例 2:覆盖还是追加错误日志
- 覆盖错误日志:command 2> error.log(把原来的错误清空);
- 追加错误日志:command 2>> error.log(原来的错误留着,新错误接在后面)。
2. 同时重定向错误与正常输出
根据需求,有两种常见的 “分离式” 重定向方案:
| 需求场景 | 命令格式 | 说明 |
|---|---|---|
| 错误写入 log1,正常结果写入 log2 | command 2> log1 1> log2 | 完全分离,便于后续分别分析 |
| 错误和正常结果合并写入同一个 log | command > log.txt 2>&1 或 command &> log.txt | Bash 专用,&> 是 > log 2>&1 的简化 |
示例 1:彻底分开
把 ls -l test.txt badfile test2.txt 的错误写进 err.log,正常结果写进 out.log:
ls -l test.txt badfile test2.txt 2> err.log 1> out.log
# 查看正常结果
cat out.log
# -rw-r--r-- 1 user user 60 Jun 4 09:30 test.txt
# 查看错误信息
cat err.log
# ls: cannot access 'badfile': No such file or directory
# ls: cannot access 'test2.txt': No such file or directory
示例 2:合并输出(&>)
用 &> 将所有输出(正常 + 错误)写入 all.log,适合需要完整记录执行过程的场景:
bash
ls -l test.txt badfile &> all.log
# 查看合并结果
cat all.log
# ls: cannot access 'badfile': No such file or directory
# -rw-r--r-- 1 user user 60 Jun 4 09:30 test.txt
注意:&> 是 Bash 特有语法,其他 Shell(如 sh)需用 > all.log 2>&1(顺序不能乱,必须先重定向 STDOUT,再将 STDERR 指向 STDOUT)。
三、实战:在脚本里灵活控制输出
写脚本的时候,经常要”一部分打到屏幕、一部分写进日志”,或者”把所有输出永久重定向走”,这就得靠临时重定向、永久重定向和自定义文件描述符来配合了。
1. 临时重定向:单独把某一行丢给 STDERR
脚本里用 >&2 可以把某一行输出定向到 STDERR(而不是默认的 STDOUT),方便后面单独把错误捞出来。
示例:脚本内部分离输出
# 脚本内容(save_log.sh)
#!/bin/bash
echo "【正常信息】脚本开始执行" # 默认 STDOUT
echo "【错误信息】配置文件不存在" >&2 # 定向到 STDERR
echo "【正常信息】脚本执行结束" # 默认 STDOUT
执行脚本并分离输出
# 运行脚本:STDERR 写入 error.log,STDOUT 显示在屏幕
./save_log.sh 2> error.log
# 屏幕输出(STDOUT):
# 【正常信息】脚本开始执行
# 【正常信息】脚本执行结束
# 查看错误日志(STDERR):
cat error.log
# 【错误信息】配置文件不存在
适用场景:脚本里自定义的错误提示(比如参数错了、文件没了),方便用户把错误日志单独收集起来。
2. 永久重定向:脚本内全局改道
如果一个脚本大部分输出都要写进文件,一行行写 > file 或 >&2 太啰嗦了。这时候可以用 exec 命令,把某个文件描述符全局重定向(一直到脚本结束或者再次改道)。
示例 1:全局重定向 STDOUT 到文件
# 脚本内容(global_redirect.sh)
#!/bin/bash
# 全局重定向:STDERR 写入 err.log,STDOUT 写入 out.log
exec 1> out.log # STDOUT → out.log
exec 2> err.log # STDERR → err.log
echo "脚本启动时间:$(date)" # 写入 out.log
ls -l badfile # 错误写入 err.log
echo "脚本执行完成" # 写入 out.log
执行结果
./global_redirect.sh # 无屏幕输出(所有内容已重定向)
# 查看正常输出
cat out.log
# 脚本启动时间:Tue Jun 4 11:30:00 CST 2024
# 脚本执行完成
# 查看错误输出
cat err.log
# ls: cannot access 'badfile': No such file or directory
示例 2:临时重定向之后再恢复默认
如果想”一部分输出重定向走,一部分再回到屏幕”,可以先把默认指向存到自定义文件描述符里,后面再恢复回来:
# 脚本内容(temp_restore.sh)
#!/bin/bash
# 1. 保存 STDOUT 默认指向(显示器)到 FD 3
exec 3>&1
# 2. 临时重定向 STDOUT 到 file.log
exec 1> file.log
echo "这行写入 file.log"
echo "这行也写入 file.log"
# 3. 恢复 STDOUT 到默认(显示器)
exec 1>&3
echo "这行显示在屏幕" # 恢复后,输出到显示器
# 关闭自定义 FD 3(避免资源泄漏)
exec 3>&-
执行结果
./temp_restore.sh
# 屏幕输出:这行显示在屏幕
# 查看文件内容
cat file.log
# 这行写入 file.log
# 这行也写入 file.log
3. 自定义文件描述符(FD 3-8)
Bash 允许用 3 到 8 号文件描述符(一共 6 个),用来灵活地做”多通道输出”(比如同时往日志文件、屏幕、统计文件里写)。
常用场景与示例
| 需求 | 实现命令 | 说明 |
|---|---|---|
| 自定义输出通道(FD 3 写入 log) | exec 3> app.log | 后续用 echo “内容” >&3 写入 app.log |
| 自定义输入通道(FD 4 读文件) | exec 4< config.ini | 后续用 read line <&4 从 config.ini 读数据 |
| 关闭自定义 FD | exec 3>&- | 释放资源,避免脚本结束后残留 |
实战:多通道输出脚本
# 脚本内容(multi_fd.sh)
#!/bin/bash
# 1. 初始化自定义 FD:3→日志,4→统计
exec 3> app.log # FD3:写入详细日志
exec 4> stat.log # FD4:写入统计数据
# 2. 多通道输出
echo "[$(date)] 脚本启动" >&3 # 日志
echo "用户: $(whoami)" >&3 # 日志
echo "执行次数: 1" >&4 # 统计
# 3. 业务逻辑(示例:检查文件)
if [ -f "test.txt" ]; then
echo "[$(date)] test.txt 存在" >&3
else
echo "[$(date)] test.txt 不存在" >&2 # 错误
fi
# 4. 关闭 FD
exec 3>&-
exec 4>&-
执行结果
./multi_fd.sh 2> error.log
# 查看日志
cat app.log
# [Tue Jun 4 11:45:00 CST 2024] 脚本启动
# [Tue Jun 4 11:45:00 CST 2024] 用户: user
# 查看统计
cat stat.log
# 执行次数: 1
# 查看错误(若 test.txt 不存在)
cat error.log
# [Tue Jun 4 11:45:00 CST 2024] test.txt 不存在
四、高级:临时文件与日志记录技巧
写脚本时,“临时文件(存中间数据)“和”日志双存(屏幕 + 文件)“是出现频率很高的需求,把 mktemp 和 tee 这两个命令玩明白,效率能提一大截。
1. 把命令输出吃掉(/dev/null 黑洞)
/dev/null 是 Linux 里一个特殊的文件,人称”黑洞”——任何写进去的数据都会被丢掉,读它则啥也读不到。常用于”命令得执行,但输出我根本不想要”的场景。
示例 1:抑制所有输出(正常 + 错误)
# 执行 ls -l badfile,不显示任何输出(包括错误)
ls -l badfile &> /dev/null
示例 2:快速清空文件
用 /dev/null 覆盖文件,保留文件本身但清空内容(比 rm + touch 更高效):
# 清空 log.txt(文件仍存在)
cat /dev/null > log.txt
# 验证(空文件)
ls -l log.txt
# -rw-r--r-- 1 user user 0 Jun 4 12:00 log.txt
2. 安全地建临时文件(mktemp)
临时文件一般用来存脚本跑过程中的中间数据,得满足”文件名唯一”和”权限安全”(别让别的用户能碰)。mktemp 就是干这个的:它建的临时文件 / 目录默认权限是 rw-------(只有属主能读写),而且会自动生成独一无二的文件名(靠把模板里的 XXXXXX 替换掉实现)。
用法核心:文件名模板
mktemp 需传入 “文件名模板”,模板末尾必须包含 6 个 X(用于生成随机字符),格式如下:
mktemp [选项] 模板名.XXXXXX
常见场景与示例
场景 1:在当前目录创建临时文件
# 创建临时文件,模板为 "temp_data.XXXXXX"
temp_file=$(mktemp temp_data.XXXXXX)
# 查看生成的文件名(唯一)
echo $temp_file # 输出:temp_data.3z8xQ7(每次运行结果不同)
# 写入数据并验证
echo "中间数据:$(date)" > $temp_file
cat $temp_file
# 输出:中间数据:Tue Jun 4 12:15:00 CST 2024
# 脚本结束后删除临时文件(避免残留)
rm -f $temp_file
场景 2:在系统临时目录(/tmp)创建文件
系统临时目录 /tmp 特性:重启后自动清空,适合存放无需持久化的临时数据。通过 -t 选项,mktemp 会直接在 /tmp 生成文件,并返回完整路径。
# -t 选项:在 /tmp 目录创建,返回全路径
temp_file=$(mktemp -t tmp_script.XXXXXX)
# 查看路径(/tmp 开头)
echo $temp_file # 输出:/tmp/tmp_script.9aY2b3
# 写入数据
echo "系统临时数据" > $temp_file
cat $temp_file # 输出:系统临时数据
场景 3:创建临时目录(-d 选项)
若需批量存放多个临时文件(如拆分的日志片段、临时配置),可用 -d 选项创建临时目录:
# -d 选项:创建临时目录
temp_dir=$(mktemp -d tmp_workdir.XXXXXX)
# 在临时目录中创建子文件
echo "子文件 1 数据" > $temp_dir/sub1.txt
echo "子文件 2 数据" > $temp_dir/sub2.txt
# 查看目录结构
ls -l $temp_dir
# 输出:
# -rw------- 1 user user 16 Jun 4 12:20 sub1.txt
# -rw------- 1 user user 16 Jun 4 12:20 sub2.txt
# 脚本结束后删除整个目录
rm -rf $temp_dir
安全优势:mktemp 生成的文件 / 目录只有属主自己能碰,避免了手动建个 temp.txt 被别的用户篡改的风险。
3. 日志双存:屏幕和文件一起输出(tee 命令)
实际运维里,经常想”一边在屏幕上实时看进度,一边把输出存成日志文件”——tee 命令正好解决这个,它的作用就像一个”T 型三通”:
- 接收 STDIN 数据;
- 一份输出到 STDOUT(屏幕);
- 另一份写入指定文件。
基础用法
tee [选项] 目标文件
- -a 选项:追加写入文件(默认覆盖);
- 可配合管道 |,将前一个命令的输出作为 tee 的输入。
示例 1:单次命令双存输出
将 date 命令的结果同时显示在屏幕和写入 time.log:
date | tee time.log
# 屏幕输出:Tue Jun 4 12:30:00 CST 2024
# 同时写入 time.log
# 验证文件内容
cat time.log # 输出:Tue Jun 4 12:30:00 CST 2024
示例 2:追加写入日志(-a 选项)
多次执行命令,将结果追加到同一日志(避免覆盖):
# 第一次:覆盖写入
who | tee user_log.log
# 屏幕输出:user pts/0 2024-06-04 09:00 (192.168.1.100)
# 第二次:追加写入
date | tee -a user_log.log
# 查看最终日志
cat user_log.log
# 输出:
# user pts/0 2024-06-04 09:00 (192.168.1.100)
# Tue Jun 4 12:35:00 CST 2024
示例 3:脚本中双存输出(结合重定向)
脚本执行时,将所有输出(正常 + 错误)同时显示在屏幕和写入 script.log:
# 脚本内容(tee_demo.sh)
#!/bin/bash
echo "【$(date)】脚本启动"
ls -l test.txt badfile # 包含正常和错误输出
echo "【$(date)】脚本结束"
执行脚本并双存日志
# 合并 STDOUT 和 STDERR,通过 tee 双存
./tee_demo.sh 2>&1 | tee -a script.log
执行结果
- 屏幕输出(实时查看):
【Tue Jun 4 12:40:00 CST 2024】脚本启动
-rw-r--r-- 1 user user 60 Jun 4 09:30 test.txt
ls: cannot access 'badfile': No such file or directory
【Tue Jun 4 12:40:00 CST 2024】脚本结束
- 日志文件 script.log(永久保存,内容与屏幕一致)。
五、排查工具:把打开的文件描述符列出来(lsof)
脚本跑的时候要是出现”文件描述符泄露""重定向没生效”这类问题,可以用 lsof 命令看当前进程的文件描述符状态,快速定位毛病出在哪。
核心用法:把当前进程的 FD 筛出来
lsof(List Open Files)能把系统里所有打开的文件列出来,配合下面几个选项就能筛出当前 Shell / 脚本的文件描述符:
- -p $$:$$ 是 Bash 内置变量,代表当前进程的 PID;
- -d 0-8:只显示 0 到 8 号文件描述符;
- -a:逻辑上的”与”,前面两个条件得同时满足。
示例 1:查看当前 Shell 的默认 FD
# 查看当前 Shell 的 0-2 号 FD
lsof -a -p $$ -d 0,1,2
输出解析
plaintext
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 12345 user 0u CHR 136,0 0t0 3 /dev/pts/0 # STDIN(键盘)
bash 12345 user 1u CHR 136,0 0t0 3 /dev/pts/0 # STDOUT(显示器)
bash 12345 user 2u CHR 136,0 0t0 3 /dev/pts/0 # STDERR(显示器)
- FD 列:0u 代表 0 号 FD,u 表示 “读写权限”;
- TYPE 列:CHR 代表字符设备(键盘 / 显示器),REG 代表常规文件。
示例 2:查看脚本中自定义 FD
在脚本中创建自定义 FD 后,用 lsof 验证其状态:
# 脚本内容(lsof_demo.sh)
#!/bin/bash
# 创建自定义 FD 3(写入文件)和 4(读取文件)
exec 3> demo.log
exec 4< test.txt
# 查看当前进程的 FD 0-4
lsof -a -p $$ -d 0-4
# 关闭 FD
exec 3>&-
exec 4>&-
执行结果(关键部分)
COMMAND 6789 user 3w REG 8,1 0 12345 /home/user/demo.log # FD3:写文件
COMMAND 6789 user 4r REG 8,1 60 12346 /home/user/test.txt # FD4:读文件
从输出能确认:FD3 是以”写权限(w)“指向 demo.log,FD4 是以”读权限(r)“指向 test.txt,跟我们配的一样。
六、总结:Shell 数据呈现最佳实践
| 需求场景 | 推荐方案 | 注意事项 |
|---|---|---|
| 仅保存正常输出 | command > out.log 或 command >> out.log | > 覆盖,>> 追加,避免误删数据 |
| 仅保存错误输出 | command 2> err.log 或 command 2>> err.log | 2 与 > 之间无空格,否则语法错误 |
| 分离正常与错误输出 | command 1> out.log 2> err.log | 顺序无关,可分别分析两类日志 |
| 合并输出并双存(屏幕 + 文件) | command 2>&1 | tee -a all.log | 2>&1 | 需在管道前,确保错误也进入 tee |
| 脚本内临时重定向某行到 STDERR | echo “错误” >&2 | 必须加 &,否则会被解析为 “文件名为 2” |
| 脚本内全局重定向后恢复默认 | exec 3>&1; exec 1> log.log; …; exec 1>&3 | 用自定义 FD 保存默认指向,避免无法恢复 |
| 创建安全临时文件 / 目录 | mktemp -t 模板.XXXXXX 或 mktemp -d | 模板.XXXXXX 模板需含 6 个 X,脚本结束后删除临时文件 |
| 抑制所有输出(无需记录) | command &> /dev/null | 适用于 “执行命令但不关心结果” 的场景(如清理) |
把上面这些技巧吃透,你就能根据实际需要灵活控制 Shell 数据的去向了——不管是写脚本、日常运维还是跑批量任务,都能做到”精准分流、安全存储、方便排查”。