四、输入输出:echo、printf、read、标准输入输出重定向、文件描述符
Shell 脚本的本质是命令的有机组合,而命令与外部环境交互的桥梁正是输入输出(I/O)体系。无论是将计算结果展示给用户,还是从键盘读取参数,抑或是将日志写入文件,都离不开对标准输入、标准输出和标准错误这三个数据流的精准控制。本章将深入剖析 Shell 的 I/O 基石:echo 与 printf 的输出差异、read 的交互读取机制、重定向的灵活运用,以及文件描述符这一底层抽象。理解这些概念,是编写健壮、可维护脚本的必备能力。
上图清晰地展示了 Shell 进程与三个标准文件描述符的关系。文件描述符 0(标准输入)默认连接键盘,文件描述符 1(标准输出)和文件描述符 2(标准错误)默认连接屏幕。重定向操作的本质,就是改变这些描述符所指向的目标,例如将输出从屏幕改为文件。下面我们将逐一拆解这些核心组件。
4.1 输出命令:echo 与 printf 的对比与选择
4.1.1 核心原理:从简单输出到格式化控制
echo 是 Shell 中最简单的输出命令,其核心功能是将参数以空格分隔并追加换行符后打印到标准输出。它适合快速打印变量值或固定字符串。然而,echo 在不同 Shell(如 Bash 与 Dash)中对转义字符(如 \n、\t)的处理存在差异,且不支持复杂的格式化控制。
printf 则源自 C 语言的 printf 函数,提供了强大的格式化能力。它使用格式字符串(format string)来控制输出的结构,支持 %s(字符串)、%d(整数)、%f(浮点数)等格式说明符,并允许指定宽度、对齐方式和精度。更重要的是,printf 的行为在所有符合 POSIX 标准的 Shell 中完全一致,这使其成为编写可移植脚本的首选。
4.1.2 操作流程:如何选择与使用
- 快速调试:使用
echo "$var"查看变量值。 - 格式化表格:使用
printf "%-20s %8d\n" "Name" 123实现列对齐。 - 无换行输出:
echo -n "Please wait..."或printf "Please wait..."。 - 转义字符:在
printf中,\n表示换行,\t表示制表符,需在格式字符串中显式写出。
#!/bin/bash
# 案例:使用 printf 生成对齐的报表
printf "%-10s %-8s %10s\n" "用户名" "状态" "使用率(%)"
printf "%-10s %-8s %10.2f\n" "root" "active" 87.456
printf "%-10s %-8s %10.2f\n" "www-data" "idle" 12.5
# 输出结果:
# 用户名 状态 使用率(%)
# root active 87.46
# www-data idle 12.50
4.1.3 常见误区与总结
- 误区一:在
echo中使用\n期望换行,但未加-e选项(Bash 默认不解释)。 - 误区二:混淆
printf的格式字符串与普通字符串,忘记使用%s等占位符。 - 总结:优先使用
printf处理需要格式化或跨平台兼容的场景;echo仅用于简单、非关键的输出。
4.2 交互输入:read 命令的深度应用
4.2.1 核心原理:从标准输入读取数据
read 命令是 Shell 脚本获取用户输入或读取文件内容的关键工具。它从标准输入(或通过 -u 指定的文件描述符)读取一行数据,并使用 IFS(内部字段分隔符,默认为空格、制表符、换行)将该行拆分为多个字段,依次赋值给指定的变量。如果变量数量少于字段数量,多余的字段会全部赋给最后一个变量。
4.2.2 操作流程:常用选项与实战场景
| 选项 | 作用 | 示例 |
|---|---|---|
-p | 指定提示符 | read -p "请输入用户名: " user |
-t | 设置超时时间(秒) | read -t 5 -p "5秒内确认: " ans |
-s | 静默模式(不回显输入) | read -s -p "密码: " pwd |
-n | 读取指定字符数后返回 | read -n 1 -p "按任意键继续" key |
-a | 将输入赋值给数组 | read -a arr -p "输入多个值: " |
#!/bin/bash
confirm() {
local prompt="$1" default="$2" ans
if [[ -n "$default" ]]; then
prompt="$prompt [$default]: "
fi
read -t 10 -p "$prompt" ans
ans="${ans:-$default}"
case "$ans" in
y|Y|yes|YES) return 0 ;;
*) return 1 ;;
esac
}
if confirm "是否继续安装?" "y"; then
echo "开始安装..."
else
echo "安装取消。"
fi
4.2.3 常见误区与总结
- 误区一:忘记处理
read的返回值。当用户输入 EOF(如 Ctrl+D)或超时时,read返回非零状态,脚本应据此做出反应。 - 误区二:在管道中使用
read时,变量赋值发生在子 Shell 中,导致父 Shell 无法获取变量值。例如echo "hello" | read var中$var为空。 - 总结:
read是交互式脚本的基石,务必掌握其选项和子 Shell 陷阱。
4.3 标准输入输出重定向与文件描述符
4.3.1 核心原理:一切皆文件的数据流抽象
Linux 哲学中“一切皆文件”,进程的输入输出也被抽象为文件描述符(File Descriptor, FD)。每个进程启动时,系统默认分配三个文件描述符:0(标准输入)、1(标准输出)、2(标准错误)。重定向的本质是使用 >、< 等操作符,将这些描述符指向的文件对象(默认是终端设备)替换为其他文件、设备或管道。
4.3.2 操作流程:重定向符号详解
| 符号 | 含义 | 示例 |
|---|---|---|
> | 将标准输出重定向到文件(覆盖) | ls > list.txt |
>> | 将标准输出追加到文件 | echo "new" >> log.txt |
2> | 将标准错误重定向到文件 | cmd 2> error.log |
2>&1 | 将标准错误合并到标准输出 | cmd > all.log 2>&1 |
< | 从文件读取标准输入 | wc -l < input.txt |
<< | Here Document(内联输入) | cat << EOF |
<<< | Here String(字符串输入) | grep "x" <<< "hello" |
>>file 2>&1 | 合并输出和错误并追加 | ./script.sh >> run.log 2>&1 |
4.3.3 案例拆解:日志记录与错误隔离
#!/bin/bash
# 场景:备份数据库,要求将正常日志和错误日志分开记录,并实时监控
BACKUP_DIR="/backup/mysql"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOG_FILE="$BACKUP_DIR/backup_$TIMESTAMP.log"
ERR_FILE="$BACKUP_DIR/backup_$TIMESTAMP.err"
# 执行备份命令,stdout 和 stderr 分别重定向
mysqldump --single-transaction -u root mydb > "$BACKUP_DIR/mydb_$TIMESTAMP.sql" 2> "$ERR_FILE"
# 检查命令执行状态
if [[ $? -eq 0 ]]; then
echo "[OK] 备份成功,日志: $LOG_FILE" | tee -a "$LOG_FILE"
else
echo "[FAIL] 备份失败,错误详情: $ERR_FILE" | tee -a "$LOG_FILE"
# 发送告警邮件(示例)
mail -s "MySQL Backup Failed" admin@example.com < "$ERR_FILE"
fi
# 使用 Here Document 生成配置文件
cat << EOF > /tmp/db_config.txt
[client]
user=root
password=${DB_PASS}
host=localhost
EOF
此案例展示了三个关键点:一是通过 2> 将错误信息与正常输出隔离,便于排查问题;二是使用 $? 判断命令执行状态,实现分支逻辑;三是利用 Here Document 生成多行配置文件,避免了繁琐的 echo 拼接。
4.3.4 常见误区与总结
cmd 2>&1 > file 与 cmd > file 2>&1 的结果截然不同。前者先将 stderr 指向当前 stdout(屏幕),再将 stdout 指向文件,最终 stderr 仍输出到屏幕;后者先将 stdout 指向文件,再将 stderr 指向 stdout(文件),最终两者都进入文件。务必记住:重定向从左到右解析。
- 误区一:混淆
>与>>,导致日志文件被意外覆盖。 - 误区二:使用
2>&1时未注意顺序,导致错误信息丢失。 - 总结:文件描述符是 Shell 数据流的底层逻辑,掌握重定向的解析顺序和组合技巧,是编写专业脚本的分水岭。
4.4 综合实战:构建一个带日志功能的健壮脚本框架
4.4.1 场景与目标
假设我们需要编写一个部署脚本,要求:1) 所有输出(包括错误)同时显示在屏幕并写入日志文件;2) 支持静默模式(仅写日志);3) 能够读取配置文件中的参数。这需要综合运用 printf、read、重定向和文件描述符。
4.4.2 实现方案与代码
#!/bin/bash
# 功能:部署脚本框架,演示 I/O 综合运用
LOG_FILE="/var/log/deploy_$(date +%Y%m%d).log"
SILENT=0
CONFIG_FILE="/etc/myapp.conf"
# 定义日志函数:利用文件描述符 3 指向日志文件
exec 3>> "$LOG_FILE" # 打开文件描述符 3 用于追加
log_info() {
local msg="[INFO] $(date '+%F %T') - $1"
echo "$msg" | tee /dev/fd/3 # 输出到屏幕和 fd3
if [[ $SILENT -eq 1 ]]; then
echo "$msg" >&3 # 静默模式仅写日志
fi
}
log_error() {
local msg="[ERROR] $(date '+%F %T') - $1"
echo "$msg" | tee /dev/fd/3 >&2 # 错误信息输出到 stderr 和 fd3
}
# 读取配置文件
if [[ -f "$CONFIG_FILE" ]]; then
while IFS='=' read -r key value; do
case "$key" in
server) SERVER="$value" ;;
port) PORT="$value" ;;
*) ;;
esac
done < "$CONFIG_FILE"
else
log_error "配置文件 $CONFIG_FILE 不存在"
exit 1
fi
# 主逻辑
log_info "开始部署到 $SERVER:$PORT"
# 模拟部署命令
if command -v curl >/dev/null 2>&1; then
curl -s "http://$SERVER:$PORT/health" >/dev/null 2>&1
if [[ $? -eq 0 ]]; then
log_info "健康检查通过"
else
log_error "健康检查失败"
exit 2
fi
else
log_error "curl 命令未安装"
exit 3
fi
# 清理:关闭文件描述符 3
exec 3>&-
4.4.3 复盘与经验
此脚本展示了几个高级 I/O 技巧:使用 exec 3>> "$LOG_FILE" 创建自定义文件描述符,避免了每次写日志都重新打开文件的开销;tee /dev/fd/3 实现了数据同时流向屏幕和日志;while read 循环配合重定向 < "$CONFIG_FILE" 安全地逐行解析配置文件。这种模式在真实的生产环境中非常常见,它保证了日志的完整性和脚本的可调试性。
>> 重定向更高效。同时,务必在脚本结束时关闭这些描述符(exec 3>&-),避免资源泄漏。
通过本章的学习,我们掌握了 Shell 输入输出的四大支柱:echo 与 printf 的输出控制、read 的交互读取、重定向的灵活运用以及文件描述符的底层机制。这些知识不仅是编写脚本的基础,更是理解 Shell 进程模型和构建复杂自动化系统的关键。在实际工作中,请务必注意重定向的顺序、子 Shell 的陷阱以及文件描述符的资源管理,这些细节往往决定了脚本的健壮性与可维护性。