Bash read 命令
在 Bash 脚本中,read 命令用于从用户(标准输入)或文件中读取输入,并将读取到的内容赋值给一个或多个变量。正是有了这个命令,脚本才真正拥有了交互性(interactive)。
相较于仅仅依赖预设好的硬编码值或命令行参数(正如我们在前几章学到的 $1, $@ 等),read 命令允许脚本在执行过程中主动询问用户,获取其所需的关键信息。
1. 使用 read 获取基础用户输入
read 最直接的用法就是提示用户输入一条信息,并将其存储到一个变量中。如果执行 read 时不带任何参数,它会读取标准输入的一整行,并自动将其赋值给一个名为 REPLY 的特殊内置变量。然而,更常见也更推荐的做法是显式指定一个变量名。
#!/bin/bash
echo "请输入你的名字:"
read user_name
echo "你好, $user_name! 欢迎使用本脚本。"在这个例子中:
echo "请输入你的名字:"向用户显示一条提示信息,指示他们应该输入什么。- read
user_name会暂停脚本的执行。它静静地等待用户在键盘上敲击字符并按下回车键 (Enter)。用户在按回车前输入的所有内容都会被妥善保存在user_name变量中。 echo "你好, $user_name! ..."随后调用了用户刚才输入的值。
1.1 使用 read -p 直接输出提示
与其专门写一行 echo 命令来显示提示语,read 提供了一个非常贴心的 -p (prompt) 选项。它允许你直接把提示字符串和 read 命令写在同一行,这让代码看起来更加整洁、紧凑。
#!/bin/bash
read -p "你最喜欢的颜色是什么? " fav_color
echo "你最喜欢的颜色是 $fav_color。"注意细节: 请留意提示语 "你最喜欢的颜色是什么? " 末尾的那个空格。这个空格能确保终端上提示语和用户实际光标输入的位置之间有一段距离,极大地提升了视觉体验和可读性。
2. 读取多个变量
read 命令同样擅长将用户输入的一行文本“切分”并赋值给多个不同的变量。默认情况下,read 会根据空白字符(空格、制表符 Tab、换行符)来分割输入行,并将分割出来的词汇依次赋值给后面提供的变量。
如果在所有的变量都被赋值之后,输入行中还有剩余的单词,那么所有剩余的单词都会被一股脑地塞进最后一个变量里。
#!/bin/bash
# 示例 1: 读入两个词
read -p "请输入你的名和姓 (空格隔开): " first_name last_name
echo "你的名是: $first_name"
echo "你的姓是: $last_name"
# 示例 2: 读入三个数字
read -p "请输入三个数字: " num1 num2 num3
echo "第一个数字: $num1"
echo "第二个数字: $num2"
echo "第三个数字: $num3"
# 示例 3: 体验“最后一个变量”接收剩余所有内容的特性
read -p "请输入你的详细地址 (街道, 城市, 省份, 邮编): " street city state zip
echo "街道: $street"
echo "城市: $city"
echo "省份: $state"
echo "邮编(及剩余内容): $zip"在最后一个示例中,如果用户输入的是 "123 Main St Anytown CA 90210",那么 street 会变成 "123",city 变成 "Main",state 变成 "St",而 zip 会不可避免地吸收掉剩下的所有内容:"Anytown CA 90210"。
最佳实践: 像地址这种包含空格的复杂数据,通常更好的做法是分别提示用户输入各个部分(例如先问街道,再问城市),或者干脆把整行读取到一个变量中,后续再使用专门的工具(如 awk 或 sed,我们将在后续章节学习)进行精确解析。
3. 高级 read 选项
read 命令配备了几个强大的选项来精准控制其行为,极大地增强了脚本的交互体验和安全性。
3.1 隐藏输入的静默模式 (-s)
在处理密码或 API 密钥等敏感输入时,绝对不能让用户敲击的字符明文显示在屏幕上。-s (silent) 选项可以完美实现“盲打”效果,防止旁人偷窥(shoulder-surfing)。
#!/bin/bash
read -sp "请输入你的密码: " user_password
echo # 在静默输入后手动输出一个换行符,以防终端提示符错位
echo "密码已安全接收 (仅作演示输出): $user_password"当用户输入密码并按下回车后,屏幕上不会显示任何星号或字符。紧跟在 read -sp 后面的不带参数的 echo 命令非常关键,因为 -s 模式下连用户的回车动作都不会在屏幕上产生换行,如果不加这个 echo,脚本的下一行输出就会和密码提示语紧紧黏在同一行。
3.2 设置输入超时限制 (-t)
有时,脚本不能无限期地等下去。如果用户在规定时间内没有做出反应,脚本需要自动继续执行默认逻辑。-t (timeout) 选项允许你设置一个以秒为单位的倒计时。如果超时,read 命令会返回一个非零的退出状态码(表示失败)。
#!/bin/bash
echo "你有 5 秒钟的时间考虑..."
# 等待 5 秒
read -t 5 -p "你确定要继续吗?(yes/no): " choice
# 检查 read 的退出状态码
if [ $? -eq 0 ]; then
if [ "$choice" == "yes" ]; then
echo "正在继续执行..."
else
echo "操作已取消。"
fi
else
# 状态码非零,说明发生了超时
echo -e "\n超时未响应。默认执行 'no' 逻辑。"
fi这里我们再次利用了特殊变量 $?(在上一章学过)。状态码 0 表示在限定时间内成功接收到了输入;非零值则意味着超时时间到了。
3.3 限制输入的字符数 (-n)
-n (number of characters) 选项指定了 read 在自动结束读取并返回之前,最多接收多少个字符。这在处理单字符输入(如 Y/N 确认提示)时简直是绝配,因为用户敲击完指定数量的按键后,脚本会瞬间继续,无需用户再多按一次回车键。
#!/bin/bash
read -n 1 -p "按下 'y' 表示同意,'n' 表示拒绝: " answer
echo # 单字符输入后,手动换行保持整洁
if [ "$answer" == "y" ] || [ "$answer" == "Y" ]; then
echo "你选择了同意。"
elif [ "$answer" == "n" ] || [ "$answer" == "N" ]; then
echo "你选择了拒绝。"
else
echo "无效输入。"
fi3.4 从特定的文件描述符读取 (-u)
虽然 read 通常从键盘(标准输入,文件描述符为 0)读取,但 -u 选项允许它从你指定的任意文件描述符中读取数据。当你需要在脚本中同时处理用户输入和读取文件流时,这个高级技巧非常有用。
#!/bin/bash
# 1. 创建一个用于演示的临时文件
echo "这是文件里的第一行" > temp_input.txt
echo "这是文件里的第二行" >> temp_input.txt
# 2. 将文件绑定到自定义的“文件描述符 3”上以供读取
exec 3< temp_input.txt
# 3. 使用 -u 从文件描述符 3 中逐行读取
read -u 3 line_from_file1
read -u 3 line_from_file2
echo "读到的第一行: $line_from_file1"
echo "读到的第二行: $line_from_file2"
# 4. 关闭文件描述符 3,释放资源
exec 3<&-
rm temp_input.txt3.5 巧妙设置默认值
read 命令本身并没有一个叫“默认值”的参数选项。但是,业界有一个非常标准且优雅的惯用写法:利用 Bash 的参数扩展 (Parameter Expansion) 技术,在用户直接按回车(即输入为空)时,为其赋予一个默认值。
#!/bin/bash
# 提示语中用方括号 [ ] 标出默认值,这是一种 UX 惯例
read -p "请输入你最喜欢的操作系统 [Linux]: " os_choice
# 如果 os_choice 是空的(未设置或为空字符串),则将其赋值为 "Linux"
os_choice=${os_choice:-Linux}
echo "你最喜欢的操作系统是: $os_choice"语法 ${variable:-default_value} 的意思是:“如果 variable 为空,就使用 default_value 代替,否则就用 variable 真实的值”。这种方式让脚本在交互时显得极具“容错性”。
4. 综合实战案例
4.1 系统管理实战:交互式日志清理助手
在我们的系统管理脚本案例中,相比于硬编码写死一个要删除的目录,我们可以将其改造为交互式脚本,让管理员在执行敏感删除操作前进行确认。
#!/bin/bash
echo "--- 交互式日志清理实用工具 ---"
# 1. 获取目标目录并设置默认值
read -p "请输入要清理的日志目录 [/var/log]: " log_dir
log_dir=${log_dir:-/var/log}
# 2. 目录合法性校验
if [ ! -d "$log_dir" ]; then
echo "错误:目录 '$log_dir' 不存在。"
exit 1
fi
echo "正在 '$log_dir' 中查找 7 天前的旧日志文件..."
# 为了安全起见,这里仅做模拟演示,并未真实执行 rm 命令
# find "$log_dir" -type f -name "*.log" -mtime +7 会找到 7 天前的 .log 文件
# while read 循环用于逐行处理找到的文件
find "$log_dir" -type f -name "*.log" -mtime +7 -print0 | while IFS= read -r -d $'\0' file_to_delete; do
echo "找到旧文件: $file_to_delete"
# 3. 针对每个文件请求确认 (限制输入 1 个字符)
read -n 1 -p "是否删除此文件?(y/N): " confirm_delete
echo
if [ "$confirm_delete" == "y" ] || [ "$confirm_delete" == "Y" ]; then
echo " --> 正在删除 $file_to_delete..."
# rm "$file_to_delete" # 在生产环境中取消注释这行以执行真实删除
echo " --> (模拟删除成功)"
else
echo " --> 已跳过 $file_to_delete。"
fi
done
echo "清理向导执行完毕。"这个案例完美融合了输入默认值、目录校验以及使用 -n 1 实现高频交互确认,是系统运维中非常经典的防御性编程模式。
4.2 趣味实战:文字冒险游戏骨架
想象一下用 Bash 写一个简单的文字冒险游戏。read 绝对是控制玩家行动的核心引擎。
#!/bin/bash
echo "欢迎来到恐怖洋馆文字冒险!"
echo "你发现自己站在一座阴森洋馆的大门前。"
player_name=""
# 强制要求输入名字,否则陷入死循环
while [ -z "$player_name" ]; do
read -p "勇士,告诉我你的名字: " player_name
if [ -z "$player_name" ]; then
echo "名字不能为空,大侠请留步!"
fi
done
echo "很好,$player_name。你可以选择 '进入(enter)' 洋馆,或者 '逃跑(flee)'。"
choice=""
# 校验玩家的选择,必须是 enter 或 flee
while [ "$choice" != "enter" ] && [ "$choice" != "flee" ]; do
read -p "你要做什么? " choice
case "$choice" in
enter)
echo "你鼓起勇气推开了沉重的橡木门,走了进去。"
echo "一阵阴风吹过,让你不寒而栗。"
;;
flee)
echo "你觉得保命要紧,转身就跑。"
echo "游戏结束。至少你活下来了。"
exit 0
;;
*)
echo "无效的指令。请输入 'enter' 或 'flee'。"
;;
esac
done
echo "未完待续..."这个游戏脚本展示了如何利用 read 结合 while 循环进行严格的输入校验。如果用户胡乱输入,循环会把它死死卡住,直到它输入正确的指令为止。