🔍搜索⌘K

Linux Shell 数据呈现完全指南:从标准流重定向到临时文件实战

概述

在写 Shell 脚本或者日常运维时,命令 / 脚本跑出来的东西,通常就三种去向:

  • 只往屏幕上打:图个快速看一眼结果(比如 ls、pwd);
  • 只存进文件:留个日志或者数据底稿(比如脚本运行日志、统计结果);
  • 屏幕和文件都留一份:一边实时看进度,一边永久存档(比如批量任务执行日志)。

想把这几种”数据分流”玩灵活,关键得理解 Linux 是怎么抽象输入 / 输出(I/O) 的——也就是标准文件描述符。这篇文章从基础概念讲起,一步步深入到重定向、自定义文件描述符、临时文件这些实战技巧,争取把 Shell 数据呈现的各种场景都讲透。

一、基础:Linux 标准文件描述符

Linux 把所有 I/O 操作都抽象成”文件操作”,用一个文件描述符(File Descriptor,FD) 来标记每一个打开的”文件对象”(键盘、显示器、真实的文件都算)。

Bash 默认留了前 3 个文件描述符,专门管默认的输入、输出和错误,数据重定向就是靠它们打底的。

1. 三大核心文件描述符

文件描述符(FD)缩写名称默认指向功能说明
0STDIN标准输入键盘接收外部输入(如 read 命令、cat 无参时)
1STDOUT标准输出终端显示器输出正常执行结果(如命令返回的有效数据)
2STDERR标准错误输出终端显示器输出错误信息(如命令不存在、文件找不到)

关键特性:

  • 默认情况下,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,正常结果写入 log2command 2> log1 1> log2完全分离,便于后续分别分析
错误和正常结果合并写入同一个 logcommand > log.txt 2>&1 或 command &> log.txtBash 专用,&> 是 > 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 读数据
关闭自定义 FDexec 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.log2 与 > 之间无空格,否则语法错误
分离正常与错误输出command 1> out.log 2> err.log顺序无关,可分别分析两类日志
合并输出并双存(屏幕 + 文件)command 2>&1 | tee -a all.log | 2>&1需在管道前,确保错误也进入 tee
脚本内临时重定向某行到 STDERRecho “错误” >&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 数据的去向了——不管是写脚本、日常运维还是跑批量任务,都能做到”精准分流、安全存储、方便排查”。