Shell 脚本跟用户打交道,靠的就是输入输出这几件事:read从键盘读变量,echo、printf往外打印。看着都是最基础的命令,真用起来一堆细节——read怎么隐藏密码、echo为什么要加-e、printf怎么对齐列、日志里怎么打出一行红字。这一篇把它们逐个拆开。
一、read:从标准输入读变量
语法格式
read[选项]变量名...不带变量名时,读进来的内容默认存进REPLY,这是 bash 的约定,不是 bug。
常用参数
| 参数 | 说明 |
|---|---|
-p 提示串 | 把提示打到标准错误,不换行,紧跟在输入位置等用户敲 |
-s | 静默模式,输入不回显,输密码用 |
-t 秒数 | 超时返回非零,脚本里用来做"默认继续" |
-n 字符数 | 读满 N 个字符就返回,不用按回车 |
-a 数组名 | 把输入按 IFS 拆成数组 |
-d 分隔符 | 用指定字符代替换行作为结束符 |
示例与预期输出
最基础的交互:
read-p"请输入你的名字: "nameecho"你好,$name"预期输出(用户敲Ben后回车):
请输入你的名字: Ben 你好, Ben静默读密码:
read-s-p"请输入密码: "pwechoecho"长度是${#pw}"敲密码时屏幕一个字符都不显示,最后一行打长度。
超时给默认值:
read-t5-p"5 秒内按 Y 继续,其它任意键退出: "ansans=${ans:-N}["$ans"="Y"]&&echo"继续"||echo"退出"5 秒不敲,ans是空串,${ans:-N}落回默认值 N。
一行拆进数组:
read-aparts<<<"apple banana cherry"echo"第一个:${parts[0]}, 共${#parts[@]}个"预期输出:
第一个: apple, 共 3 个⚠️ 常见坑
-p后面的提示串含空格或中文,必须加引号,否则 shell 会把空格后的部分当成另一个参数。- 从管道或文件喂
read时,循环里的read会把后面所有输入一次性吃掉(while read那一篇专门讲)。 read -s在没有 tty 的环境(cron、管道)里不生效,因为它要改终端属性。
一行一次读多个变量也很常见,按 IFS 拆:
read-p"输入姓名和年龄: "name ageecho"$name今年$age岁"预期输出(输入Ben 25):
输入姓名和年龄: Ben 25 Ben 今年 25 岁输入段数比变量多,最后一个变量吃掉剩余全部。比如只读name age两个变量,敲Ben 25 Beijing,age会拿到25 Beijing。
二、echo:最常用的打印
常用参数
| 参数 | 说明 |
|---|---|
-n | 行尾不加换行 |
-e | 开启转义解释,识别\n\t\\ |
-E | 关闭转义(bash 默认行为) |
示例与预期输出
echo"hello"echo-n"不换行"echo" 接着上面"预期输出:
hello 不换行 接着上面用-e打制表和换行:
echo-e"姓名\tBen\n年龄\t25"姓名 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保留两位小数。
示例与预期输出
printf"%-10s %6s\n"姓名 年龄printf"%-10s %6d\n"Ben25printf"%-10s %6d\n"Alice30预期输出:
姓名 年龄 Ben 25 Alice 30%-10s左对齐占 10 个字符宽,%6d右对齐占 6 位,两列自动对齐。这就是printf比echo强的地方——做表格、对齐数字,echo做不到。
保留两位小数:
printf"Pi = %.2f\n"3.14159Pi = 3.14循环里用printf打对齐的表:
whileread-ruser uid;doprintf"%-15s %6d\n""$user""$uid"done<<'EOF' root 0 zhangsan 1000 lisi 1001 EOF预期输出:
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 | 背景绿 |
封装成变量用
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更可读,也不依赖硬编码码值:
RED=$(tput setaf1)GREEN=$(tput setaf2)NC=$(tput sgr0)echo"${RED}错误${NC}"⚠️ 注意
重定向到文件时,颜色码会变成一堆^[[31m乱字符写进日志。判断一下输出是不是连着终端,是才上色:
if[-t1];thenRED=$(tput setaf1)GREEN=$(tput setaf2)NC=$(tput sgr0)elseRED="";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),它就老老实实显示成乱码。颜色是终端的能力,不是文本的一部分。