Shell 脚本跟用户打交道,靠的就是输入输出这几件事:read 从键盘读变量,echo、printf 往外打印。看着都是最基础的命令,真用起来一堆细节------read 怎么隐藏密码、echo 为什么要加 -e、printf 怎么对齐列、日志里怎么打出一行红字。这一篇把它们逐个拆开。
一、read:从标准输入读变量
语法格式
bash
read [选项] 变量名...
不带变量名时,读进来的内容默认存进 REPLY,这是 bash 的约定,不是 bug。
常用参数
| 参数 | 说明 |
|---|---|
-p 提示串 |
把提示打到标准错误,不换行,紧跟在输入位置等用户敲 |
-s |
静默模式,输入不回显,输密码用 |
-t 秒数 |
超时返回非零,脚本里用来做"默认继续" |
-n 字符数 |
读满 N 个字符就返回,不用按回车 |
-a 数组名 |
把输入按 IFS 拆成数组 |
-d 分隔符 |
用指定字符代替换行作为结束符 |
示例与预期输出
最基础的交互:
bash
read -p "请输入你的名字: " name
echo "你好, $name"
预期输出(用户敲 Ben 后回车):
text
请输入你的名字: Ben
你好, Ben
静默读密码:
bash
read -s -p "请输入密码: " pw
echo
echo "长度是 ${#pw}"
敲密码时屏幕一个字符都不显示,最后一行打长度。
超时给默认值:
bash
read -t 5 -p "5 秒内按 Y 继续,其它任意键退出: " ans
ans=${ans:-N}
[ "$ans" = "Y" ] && echo "继续" || echo "退出"
5 秒不敲,ans 是空串,${ans:-N} 落回默认值 N。
一行拆进数组:
bash
read -a parts <<< "apple banana cherry"
echo "第一个: ${parts[0]}, 共 ${#parts[@]} 个"
预期输出:
text
第一个: apple, 共 3 个
⚠️ 常见坑
-p后面的提示串含空格或中文,必须加引号,否则 shell 会把空格后的部分当成另一个参数。- 从管道或文件喂
read时,循环里的read会把后面所有输入一次性吃掉(while read那一篇专门讲)。 read -s在没有 tty 的环境(cron、管道)里不生效,因为它要改终端属性。
一行一次读多个变量也很常见,按 IFS 拆:
bash
read -p "输入姓名和年龄: " name age
echo "$name 今年 $age 岁"
预期输出(输入 Ben 25):
text
输入姓名和年龄: Ben 25
Ben 今年 25 岁
输入段数比变量多,最后一个变量吃掉剩余全部。比如只读 name age 两个变量,敲 Ben 25 Beijing,age 会拿到 25 Beijing。
二、echo:最常用的打印
常用参数
| 参数 | 说明 |
|---|---|
-n |
行尾不加换行 |
-e |
开启转义解释,识别 \n \t \\ |
-E |
关闭转义(bash 默认行为) |
示例与预期输出
bash
echo "hello"
echo -n "不换行"
echo " 接着上面"
预期输出:
text
hello
不换行 接着上面
用 -e 打制表和换行:
bash
echo -e "姓名\tBen\n年龄\t25"
text
姓名 Ben
年龄 25
常见转义符:
| 写法 | 含义 |
|---|---|
\n |
换行 |
\t |
制表符 |
\\ |
反斜杠本身 |
\a |
响铃(有些终端会"嘀"一声) |
\033 / \e |
ESC 字符,颜色码的开头 |
注意:echo 不带 -e 时,\n 原样打印成两个字符。这是新手最常遇到的"为什么我的 \n 不换行"。
⚠️ 注意
echo 在 bash 和 dash 下对 -e 的默认处理不一样。脚本要跨发行版跑(比如 Debian 默认 sh 是 dash),格式化输出交给 printf,echo 只用来打一行简单文本。另外 echo $var 不加引号,变量里带空格会被 IFS 拆开,还会把 *.log 这种通配符展开,这个坑在下面 printf 一节一起说。
三、printf:格式化输出,跨 shell 一致
printf 模仿 C 语言的同名函数,格式串后面跟参数。它不自动加换行,这是和 echo 最大的区别。
常用格式符
| 格式符 | 含义 |
|---|---|
%s |
字符串 |
%d |
十进制整数 |
%f |
浮点数 |
%x |
十六进制 |
%% |
百分号本身 |
宽度和对齐写在 % 和字母之间:%10s 右对齐占 10 位,%-10s 左对齐,%.2f 保留两位小数。
示例与预期输出
bash
printf "%-10s %6s\n" 姓名 年龄
printf "%-10s %6d\n" Ben 25
printf "%-10s %6d\n" Alice 30
预期输出:
text
姓名 年龄
Ben 25
Alice 30
%-10s 左对齐占 10 个字符宽,%6d 右对齐占 6 位,两列自动对齐。这就是 printf 比 echo 强的地方------做表格、对齐数字,echo 做不到。
保留两位小数:
bash
printf "Pi = %.2f\n" 3.14159
text
Pi = 3.14
循环里用 printf 打对齐的表:
bash
while read -r user uid; do
printf "%-15s %6d\n" "$user" "$uid"
done <<'EOF'
root 0
zhangsan 1000
lisi 1001
EOF
预期输出:
text
root 0
zhangsan 1000
lisi 1001
%-15s 左对齐占 15 位,短名字自动补空格,长名字顶出来也不影响下一列。这种表用 echo 拼不出来。
⚠️ 常见错误
- 忘记加
\n,所有输出挤在一行。 - 格式符数量多于参数,剩下的
%s填空串、%d填 0;参数多于格式符,格式串会从头再用一遍。写循环打印时这个"从头再用"经常导致意外重复。
四、颜色输出:ANSI 转义码
终端里打红字,本质是往输出里塞一段特殊字符序列,终端模拟器识别后改颜色。这段序列以 \033[ 开头、以 m 结束。
常用颜色码
| 编码 | 含义 |
|---|---|
0 |
恢复默认 |
1 |
加粗 |
31 |
前景红 |
32 |
前景绿 |
33 |
前景黄 |
34 |
前景蓝 |
37 |
前景白 |
41 |
背景红 |
42 |
背景绿 |
封装成变量用
bash
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
NC='\033[0m'
echo -e "${RED}错误: 备份失败${NC}"
echo -e "${GREEN}成功: 备份完成${NC}"
echo -e "${YELLOW}警告: 磁盘剩余不足 10%${NC}"
屏幕上三行分别是红、绿、黄。每次打完必须用 ${NC} 复位,否则后面所有输出都带着这个颜色,连下一个命令提示符都会变色。
用 tput 更可读,也不依赖硬编码码值:
bash
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
NC=$(tput sgr0)
echo "${RED}错误${NC}"
⚠️ 注意
重定向到文件时,颜色码会变成一堆 ^[[31m 乱字符写进日志。判断一下输出是不是连着终端,是才上色:
bash
if [ -t 1 ]; then
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
NC=$(tput sgr0)
else
RED=""; GREEN=""; NC=""
fi
-t 1 测试标准输出是否连着终端,是写彩色日志的标配。
五、知识扩展:read 为什么非得按回车
read 拿到一个字符就返回吗?默认不是。这跟终端工作在 canonical(规范)模式有关:内核的终端驱动会自己把一行字符攒起来、处理退格和 Ctrl+U,攒够一个换行才把整行交给 read。所以你敲一半按退格改字,read 根本看不到中间过程,它只收到最终那一行。
-n N 和 -s 其实是偷偷把终端切到非规范模式(或关掉回显),读完再切回来。这也是为什么在管道里 read -s 读不到密码------没有 tty 可切。写交互式脚本遇到"read 怎么不响应按键",先检查是不是被 cron 或管道包着,那种环境下根本没有终端。
ANSI 转义码同理:它不是 shell 干的,是终端模拟器在解释。把带颜色的输出重定向到文件、再 cat 出来,文件里存的就是那串 \033[31m;放到不支持转义的终端(某些串口控制台、老的网络设备 CLI),它就老老实实显示成乱码。颜色是终端的能力,不是文本的一部分。