Bash 零基础教程

Bash 标准 I/O 流与重定向

在 Linux 或类 Unix 系统中,每一个运行的程序都通过三个标准的输入/输出(I/O)流与其所处的环境进行交互:标准输入 (Standard Input, stdin)标准输出 (Standard Output, stdout)标准错误 (Standard Error, stderr)

这三个流构成了脚本接收数据、产生结果以及报告问题的基础。正是因为它们的存在,我们才能实现灵活的命令链式调用(管道)和输入输出重定向。

1. 深入理解标准流 (Standard Streams)

标准流是程序与其环境之间预先打开的数据通道。当一个程序启动时,它会自动继承这三个连接:

  1. 标准输入 (stdin - 文件描述符 0): 这是程序读取其输入数据的通道。默认情况下,stdin 连接到键盘,意味着程序会等待用户从控制台输入内容。然而,stdin 也可以被重定向,改为从一个文件或者另一个命令的输出中读取数据。
  2. 标准输出 (stdout - 文件描述符 1): 这是程序写入其正常输出结果的通道。默认情况下,stdout 连接到终端屏幕,因此命令产生的任何正常输出都会直接显示在你的控制台上。与 stdin 类似,stdout 也可以被重定向写入到一个文件,或者通过管道作为输入传递给另一个命令。
  3. 标准错误 (stderr - 文件描述符 2): 这是程序写入其错误消息和诊断输出的通道。默认情况下,stderr 也连接到终端屏幕,与 stdout 混合显示。将错误消息与常规输出分离开来对于调试和自动化任务至关重要,因为它允许我们对错误进行不同的处理(例如,单独记录到一个专属的错误日志文件中)。

文件描述符 (File Descriptors, FD)012 是操作系统用来引用这些特定 I/O 通道的数字标识符。Bash 和其他 Shell 环境在进行 I/O 重定向时会广泛使用这些描述符。

1.1 实战场景:日志记录与调试

考虑一个复制并压缩文件的备份脚本。

  • stdout 会包含确认哪些文件已成功备份以及压缩结果的常规消息。
  • stderr 会包含关于无法访问的文件、权限拒绝错误或磁盘空间已满等警告消息。
  • stdin 可能在脚本执行敏感操作前,用于等待用户输入 y 进行确认。

将这些流分离开来,系统管理员就可以快速审查错误日志 (stderr),而无需在成百上千行的成功操作消息 (stdout) 中痛苦地翻找;同时,还可以将成功的输出结果通过管道传递给另一个程序做进一步统计。

2. 重定向标准输出 (stdout)

重定向 stdout 允许你将命令的正常输出从终端屏幕“拐弯”发送到一个文件或另一个命令中。这是通过 >>> 操作符来实现的。

  • > (覆盖重定向操作符): 将 stdout 重定向到一个文件。如果文件不存在,则创建它。如果文件已存在,其原有内容将被完全覆盖(抹除)。
# 示例 1: 将 ls 的输出覆盖写入文件
ls -l /etc > system_files.txt
# 'ls -l /etc' 的输出被写入 system_files.txt。如果该文件之前有内容,则会被新内容替换。

# 示例 2: 存储命令输出供后续处理
date > current_datetime.log
# 当前的日期和时间被写入 current_datetime.log。文件以前的内容会丢失。
  • >> (追加重定向操作符): 将 stdout 重定向到一个文件。如果文件不存在,则创建它。如果文件已存在,新的输出结果将被追加 (appended) 到文件的末尾,原有的内容会安全保留。
# 示例 1: 追加日志条目
echo "第一条日志记录" >> application.log
echo "第二条日志记录" >> application.log
# 这两行都会被保存到 application.log 中。

# 示例 2: 随着时间推移构建列表
ls -d */ >> directories_list.txt # 将所有子目录追加到文件
find . -maxdepth 1 -type f >> files_list.txt # 将当前目录下的所有普通文件追加到文件

2.1 综合演示:生成系统巡检报告

利用重定向,我们可以将多个系统探测命令的输出汇聚到一个统一的报告文件中,而不会让终端屏幕满屏乱滚。

#!/bin/bash
REPORT_FILE="system_report_$(date +%Y%m%d).txt"

# 使用 > 开始一个新报告,如果存在同名文件则覆盖
echo "--- 系统信息巡检报告 ---" > "$REPORT_FILE" 
# 接下来全部使用 >> 追加内容
echo "生成时间: $(date)" >> "$REPORT_FILE"
echo "---------------------------------" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"

echo "### 磁盘使用情况 ###" >> "$REPORT_FILE"
df -h >> "$REPORT_FILE" 
echo "" >> "$REPORT_FILE"

echo "### 内存使用情况 ###" >> "$REPORT_FILE"
free -h >> "$REPORT_FILE" 
echo "" >> "$REPORT_FILE"

echo "### 占用内存 Top 5 的进程 ###" >> "$REPORT_FILE"
ps aux --sort=-%mem | head -n 6 >> "$REPORT_FILE" 
echo "" >> "$REPORT_FILE"

echo "巡检报告已保存至 $REPORT_FILE"

3. 重定向标准错误 (stderr)

重定向 stderr 是分离错误消息的必杀技,这使得记录错误、忽略错误或对其进行编程式处理变得异常简单。我们需要明确使用文件描述符 2 配合重定向操作符。

  • 2> (重定向 stderr): 将 stderr 覆盖写入文件。
# 示例 1: 捕获错误消息
ls /nonexistent_directory 2> error.log
# 'ls' 产生的错误消息(例如 "No such file or directory")
# 会被写入 error.log,而终端屏幕上不会显示任何报错。

# 示例 2: 丢弃(屏蔽)错误消息 (发送到 Linux 的“黑洞” /dev/null)
find /etc -name "*.conf" -print 2> /dev/null
# 执行 find 时遇到的所有权限不足错误 (Permission denied) 都会被直接丢弃。
# 终端上只会干干净净地显示成功找到的文件路径。
  • 2>> (追加重定向 stderr): 将 stderr 追加写入文件。
# 示例 1: 维护一个持久化的错误日志
cp /nonexistent_file /tmp 2>> script_errors.log
# 拷贝不存在文件产生的错误会被追加到 script_errors.log 末尾。

3.1 合并 stdout 和 stderr 的重定向

很多时候,你希望把正常输出和错误输出都重定向走。有几种常见的方法:

方法 1:分别重定向到不同的文件

some_command > output.txt 2> errors.txt
# 正常输出进 output.txt,报错信息进 errors.txt,泾渭分明。

方法 2:将两者合并重定向到同一个文件

  • 使用 &>>& (Bash 特有语法,现代且推荐):
some_command &> all_output.log
# stdout 和 stderr 都被塞进 all_output.log 中 (覆盖模式)。

some_command &>> all_output.log
# stdout 和 stderr 都追加进 all_output.log 中 (追加模式)。
  • 使用 2>&1 (传统的 POSIX 兼容写法,所有 Shell 通用): 这种写法的字面意思是“将文件描述符 2 (stderr) 重定向到文件描述符 1 (stdout) 当前所指向的位置”。注意:顺序极其重要!2>&1 必须放在 > output.txt 之后。
some_command > all_output.log 2>&1
# 1. 首先,stdout 被重定向指向 all_output.log。
# 2. 然后,stderr (2) 被重定向到 stdout (1) 当前指向的地方(即 all_output.log)。
# 结果:两者都进入了同一个文件。

some_command >> all_output.log 2>&1
# 将两者都追加到 all_output.log 中。

避坑指南:如果你写成 some_command 2>&1 > all_output.log,那么 stderr 会首先被重定向到 stdout 当前的位置(此时 stdout 还在指向终端屏幕),然后 stdout 才被重定向到文件。结果就是:错误信息依然会打印在屏幕上,只有正常信息进到了文件里!

3.2 综合演示:高健壮性的脚本日志记录

在自动化运维脚本中,记录所有输出以备日后审计是非常关键的。

#!/bin/bash
BACKUP_DIR="/var/backups"
LOG_FILE="/var/log/backup_script.log"
DATE_SUFFIX=$(date +%Y%m%d_%H%M%S)

# 确保备份目录存在,所有输出(包括可能因为权限不足产生的报错)都追加到日志
mkdir -p "$BACKUP_DIR" &>> "$LOG_FILE"

echo "--- 备份任务启动 ($DATE_SUFFIX) ---" &>> "$LOG_FILE"

# 尝试打包目录。无论是成功打包的详细列表,还是文件不可读的报错,统统进日志
tar -czvf "$BACKUP_DIR/my_config_$DATE_SUFFIX.tar.gz" /etc/my_app_configs &>> "$LOG_FILE"

# 尝试复制一个可能不存在的文件(用于测试错误捕获)
cp /nonexistent_file /tmp/backup_copy &>> "$LOG_FILE"

echo "--- 备份任务结束 ---" &>> "$LOG_FILE"

# 最后在屏幕上给用户一个干净的提示
echo "备份脚本执行完毕。详细情况请查看日志文件: $LOG_FILE"

4. 使用标准输入 (stdin) 与管道 (|)

标准输入 (stdin) 是命令接收数据的通道。除了通过键盘输入,stdin 也可以从文件中重定向,或者接收来自上一个命令的 stdout。

  • < (输入重定向操作符): 从文件中重定向 stdin。命令将从指定的文件中读取输入,而不是等待键盘敲击。
# 示例: 使用 wc 统计文件行数
wc -l < users.csv
# 'wc -l' 命令从 users.csv 中读取内容并打印出行数。
  • 管道 | (Pipe): 将前一个命令的 stdout 直接“灌入”到后一个命令的 stdin 中。这是在 Linux 中构建复杂数据处理流水线(Pipeline)最核心的魔法。
# 示例 1: 链式调用进行过滤和统计
ls -l | grep "Aug" | wc -l
# 1. 'ls -l' 列出目录详细内容,产生 stdout。
# 2. 这个 stdout 通过管道传给 'grep "Aug"',grep 筛选出包含 "Aug" 的行,产生新的 stdout。
# 3. 这个新的 stdout 传给 'wc -l' 进行行数统计。
# 最终结果:屏幕上只显示一个数字,即 8 月份修改过的文件数量。

# 示例 2: 经典的 Web 服务器日志分析一行流
cat access.log | grep " 404 " | awk '{print $7}' | sort | uniq -c | sort -nr | head -n 5
# 流程解析:
# 1. 读取日志 -> 2. 筛选出 404 错误行 -> 3. 提取请求的 URL 字段 -> 4. 排序 URL ->
# 5. 统计每个 URL 出现的次数去重 -> 6. 按次数倒序(从大到小)排序 -> 7. 取前 5 名。
# 结果:输出触发 404 错误次数最多的 Top 5 链接。