☰
Shell脚本入门:掌握echo、read、printf、test四大核心命令
2026/10/10 9:58:08 网站建设 项目流程

1. 为什么先啃这四条命令

我带过的很多新人学Shell,都有同一个毛病:一上来就抱着一本厚厚的命令行手册啃,今天背find明天背awk,一个礼拜下来还在原地打转,真正写脚本时连个判断分支都写不利索。学Shell这事,真没必要这么痛苦。找对切口,一天就能打通最关键的地基。

我给的切口就是四条命令:echo、read、printf、test。它们分别对应Shell脚本里最核心的三件事——往外输出、往里输入、做判断。你只要仔细琢磨任意一段脚本就会发现,逻辑再复杂,落到代码上也逃不出这几个动作:要么在打印信息,要么在等待输入,要么在执行条件分支。把这条主线吃透,等于拿到了Shell脚本的“任督二脉”。

这篇内容不是把man手册翻出来照着念,我按自己多年写脚本和排查踩坑的实际经历来写,重点放在那些文档里不写、但实战中一定会遇到的细节上。刚接触Shell的读者可以顺着往下捋,写过不少脚本但总被诡异报错卡住的人,建议直接跳到各节的“坑”部分看。

1.1 输出、输入、判断:脚本的三块基石

先想一个最简单的场景:你想让脚本问用户“要不要删除旧日志”,用户输入y之后才执行删除。这个场景里,提示信息得用输出命令打出来,用户的回答得用读取命令收进来,后续走删除还是跳过,得靠判断命令决定。一个不能再简单的交互,恰好把四条命令全用上了。

这也是我把它们放在一起讲的原因。单独背语法很快会忘,但当你意识到这三个动作是构建所有脚本的公用积木时,学习就有了着力点。echo和printf承担输出,read承担输入,test承担判断,四者组合能覆盖日常八成以上的脚本需求。剩下两成,不过是在这条主线外面接上循环、函数、文件操作而已。

1.2 为什么不是别的命令

可能有人会问:ls、grep、cd这些命令使用频率不是更高吗?确实更高,但它们属于“工具型命令”,大多用来完成某个具体任务,而echo、read、printf、test是“结构型命令”,它们决定脚本怎么组织、数据怎么流动、逻辑怎么分支。

工具型命令可以边用边查,结构型命令却最好一开始就过关。这就像学炒菜,油盐酱醋的牌子可以随便换,但先放盐还是先放糖、什么时候下锅,这个框架得先立起来。所以我建议新手不要急着背几十个命令的参数,先把这四条弄熟,后面的路会顺很多。

2. echo命令:Shell里最常用的“发声器官”

echo是绝大多数人写Shell接触到的第一个命令,它的作用很简单:把后面跟着的字符串打印到标准输出。简单归简单,真用起来其实有不少讲究,尤其是在引号、转义和跨shell兼容性上。

2.1 常用选项与转义:-n、-e、-E

最基本的用法是echo hello world,屏幕上就会输出一行hello world。它默认会在结尾补一个换行符,所以终端里的每一句输出都独占一行。如果不想换行,可以用-n,比如echo -n "正在下载",提示文字打出来之后光标停在行尾。

-e选项用来启用转义序列解释。比如echo -e "第一行\n第二行"会输出两行,\t是制表符,\r是回车符,\\是反斜杠本身。与之对应的是-E,它强制关闭转义解释,是多数Shell的默认行为。

这里有个容易翻车的点:echo -e在bash里好使,但脚本开头如果写的解释器是/bin/sh,而系统把sh指向了dash,-e就会被当成普通字符串原样打出来,屏幕上出现一行让人摸不着头脑的-e 第一行\n第二行。遇到这种诡异现象,第一反应就应该检查脚本用的是不是/bin/sh,或者干脆改用printf,后面会细说。

2.2 引号、变量展开与空格那些坑

echo后面接的内容,Shell会先做变量替换和通配符展开,再执行打印。这个机制看起来平常,却是无数新手踩坑的源头。

举个最常见的例子:

name=Alice echo $name # 输出 Alice echo "$name" # 输出 Alice

好像没区别,但把name改成"Alice Smith"再看:

name="Alice Smith" echo $name # 输出 Alice Smith,但中间的空格是“分裂”出来的 echo "$name" # 输出 Alice Smith,整个作为一份文本

表面上输出一样,内部处理却完全不同。不带引号时,Shell会把Alice和Smith当成两个独立的词传给echo,如果这个变量后续还要传给别的命令,问题立刻放大。比如:

file="/tmp/my file.txt" cat $file # 报错,被拆成两个文件路径 cat "$file" # 正确

这个坑被称为“词分裂”,几乎每天都会咬人一口。我的建议简单粗暴:凡是使用变量,一律加双引号,尤其是文件路径和可能包含空格的字符串。

单引号和双引号也有区别。双引号里的变量会被展开,单引号里的内容原封不动:

echo "$name" # 输出 Alice Smith echo '$name' # 输出 $name

想打印带美元符号的原文,用单引号;想打印变量的值,用双引号。这个习惯养成之后,脚本的稳定性会肉眼可见地提升。

2.3 实际项目里的echo用法

日常脚本里,echo主要干三件事。一是打印进度提示,比如echo "文件下载完成";二是打印分隔线,让终端输出不至于挤成一团;三是往日志文件里追加记录。

追加日志时,我通常还会在前面拼一个时间戳:

echo "[$(date '+%Y-%m-%d %H:%M:%S')] 任务执行完毕" >> run.log

这里顺带提醒一句:重定向>>表示追加,如果写成>就会把日志文件覆盖掉,线上脚本一旦搞错,历史日志直接没影。这个低级错误我见过不止一次。

还有一个很多人不知道的用法是输出彩色文本。通过给输出包上ANSI转义码,可以让成功、警告、错误信息呈现不同颜色,方便排查问题:

echo -e "\033[31m错误:文件不存在\033[0m"

但这又回到刚才说的兼容性问题上。所以我在生产脚本里基本不用echo -e做彩色输出,而是统一用printf,可移植性更好,这也是下面第四部分要展开的内容。

3. read命令:脚本的耳朵,让程序学会倾听

read命令的作用是从标准输入读取一行内容,把它赋值给一个或多个变量。没有它,脚本就只能闷头执行,无法根据用户的选择做出不同响应,交互体验无从谈起。

3.1 从终端读取输入的常用参数

最基础的用法是read name,脚本会停在那里等用户输入,用户敲完回车后,输入的内容存入name变量。加一个-p参数可以让提示语和读取合到一起,省去先echo再read的繁琐:

read -p "请输入你的名字:" name echo "你好,$name"

-t用来设置超时时间,单位是秒。比如只等5秒,5秒内没输入就继续往后走:

read -t 5 -p "5秒内输入任意字符继续:" input

-s是静默模式,输入内容不回显在终端上,适合读取密码。但要注意,这个模式不会自动换行,用户输完密码按回车后,光标还停在密码后面,你需要手动补一个echo让输出换行:

read -s -p "请输入密码:" pass echo

-n用来限制读取的字符个数,比如只读一个字符用于确认操作:

read -n1 -p "确认删除?[y/N] " answer echo

-a可以把输入按空格拆分成数组,适合一次性读取多个值。-r则禁止反斜杠转义,让输入里出现\时原样保留,读取文件时尤其重要。

3.2 从文件逐行读取:while read的正确姿势

read不仅能读终端,也可以配合重定向逐行读取文件,这是Shell脚本里处理配置文件的经典姿势:

while IFS= read -r line; do echo "读取到:$line" done < config.txt

这里面的IFS=和-r都是有讲究的。IFS=把字段分隔符清空,这样行首行尾的空格不会被剥掉;-r让反斜杠不被特殊处理,遇到Windows换行符\r时也不会出事。二者齐上,才能保证“逐行原样读取”。如果漏了-r,一旦配置里有路径C:\new这类内容,就会被拆得乱七八糟。

需要格外小心的是管道和子Shell的坑:

count=0 cat config.txt | while read line; do count=$((count + 1)) done echo "总行数:$count"

这段代码输出的总行数永远是0。原因是管道右边的while跑在子Shell里,循环里的一切变量修改都发生在子进程,父进程这边的count根本没动过。解决办法是别再通过管道传给while,直接用输入重定向:

count=0 while read line; do count=$((count + 1)) done < config.txt echo "总行数:$count"

这是Shell新手最容易怀疑人生的问题之一,看到变量“明明改了却没生效”,第一个念头就应该是“是不是遇到了子Shell”。

3.3 read的三大经典坑

第一个坑是输入残留。read -n1读完一个字符就立刻继续,但用户按的回车还留在输入缓冲区里,下一个read可能会把这个回车当成内容。解决办法是在使用-n之后紧跟一个空read,把残留的换行吃掉,或者干脆用read -r配合额外处理。

第二个坑是最后一行读不到。read按行读取,如果文件最后一行没有换行符,某些写法会在循环里漏掉它。稳妥的写法是借助||兜底:

while IFS= read -r line || [ -n "$line" ]; do echo "读取到:$line" done < config.txt

第三个坑是行内容包含大量空格时被无声拆分。如果没设IFS=,read line会把连续的空白压缩成单个空格。想要保留原始格式,必须严格写while IFS= read -r line。这三个坑都属于“不遇到不会信、遇到了才恍然大悟”的类型,提前知道能省下大把排查时间。

4. printf命令:被低估的格式化输出神器

如果说echo是便携喇叭,那printf就是一套标准扩音系统。它从C语言的printf继承了格式化输出的思想,能力远不止打印字符串这么简单。

4.1 为什么我在正式脚本里更推荐printf

第一是行为可预期。echo在不同Shell里对转义和选项的处理不一致,有的支持-e,有的不支持,有的还会把选项原样打印出来。printf的行为在POSIX标准里是统一的,写出来的脚本换到另一台机器上更不容易出幺蛾子。

第二是格式化能力强。输出表格、对齐字段、截断字符串、控制小数位数,这些事printf做起来得心应手。第三是变量安全性更好。用printf "%s" "$var"时,变量的内容会被当成普通字符串,不会被误解析成格式串。反观printf $var这种错误写法,变量里的%d会被当成格式占位符,输出立刻错乱。

4.2 格式说明符:%s、%d、%f、%-Ns逐个拆解

printf的基本语法是printf 格式串 参数...。格式串里用%开头指定占位符,后面的参数按顺序填进去。最常用的几个:

printf "姓名:%s,年龄:%d\n" "Alice" 30 printf "圆周率:%.2f\n" 3.14159 printf "十六进制:%x,八进制:%o\n" 255 8

%s表示字符串,%d表示整数,%f表示浮点数,%.2f保留两位小数,%x和%o输出十六进制和八进制。数字和加减号可以控制宽度和对齐方向:

printf "%-10s|%10s\n" "left" "right"

%-10s是左对齐并占满10个字符宽度,%10s是右对齐。这在输出列对齐的报表时非常实用。printf还有一个和echo很不一样的特点:如果给的参数比格式串里的占位符多,它会自动重复使用格式串。比如printf "%s\n" a b c会输出三行,这个行为用来对列表逐项输出很方便。反过来说,如果参数不够,缺失的部分会被当作空值处理。

4.3 printf 中文乱码到底是谁的锅

很多同学一遇到printf输出中文变成乱码,就以为是printf的编码有问题,其实它更像是一个背锅侠。乱码通常由两个原因造成。

第一个原因是脚本文件编码和终端环境的locale不一致。比如脚本文件保存成了GBK,终端却按UTF-8渲染,中文自然全是“口口口”或乱码符号。解决办法是统一编码环境:脚本文件存成UTF-8,终端字符集设置为UTF-8,并在脚本里明确环境:

export LANG=zh_CN.UTF-8

检查当前语言环境用locale,查看文件编码用file 脚本名,诊断思路先从这里开始。

第二个原因和%c有关。在Shell里,%c只取参数字符串的第一个字节,而一个中文字符在UTF-8下占3个字节。比如:

printf "%c\n" "中文"

它只会从“中”字里抠出一个字节,输出自然就是乱码。这个坑特别隐蔽,因为处理英文时%c看起来完全正常。想输出完整的中文字符串,用%s而不是%c。

还有一个虽不算乱码但也很恼人的问题:printf的宽度按字节算,不按字符算。中文每个字符占3个字节,一个包含中文的表格字段,即使用了%-20s,也会因为实际显示宽度不一致而歪歪扭扭。我的经验是:纯中文或中英混排的表格,要么手工估算宽度多留几个空格,要么直接用column命令或awk来处理对齐,别死磕printf的宽度参数。

4.4 printf与echo怎么选

我的选择标准很明确:临时调试、打一两个固定字符串,用echo;任何需要拼接变量、控制格式、或者可能移植到不同环境里的输出,一律用printf。宁可多敲几个字,也要换来行为稳定。

特别是要输出带转义的文本时,printf的%b说明符非常有用,它可以安全地解释字符串里的\n、\t等转义。对比一下:

printf "%b" "第一行\n第二行\n"

这比echo -e更规范,换到任何POSIX兼容Shell都不会掉链子。

5. test命令:脚本的分叉路口

没有test,脚本只能是一条直线走到黑。有了它,脚本才真正具备“看情况办事”的能力:判断文件是否存在、目录是否可写、字符串是否为空、数字是否达到阈值。

5.1 test的三种写法:test、方括号、双方括号

Shell里判断条件的写法有三种形态,其实是同一个家族:

test -f /etc/passwd [ -f /etc/passwd ] [[ -f /etc/passwd ]]

test是命令原生形态,[其实是test命令的别名,所以[后面必须跟空格,结尾还得再加一个]。忘记空格或结尾缺],是Shell新手高频报错之一。

[[ ]]是bash提供的增强版,它比前两者更好用,甚至在很多场景下更安全。它支持&&、||这类直观的逻辑组合,还支持模式匹配。但要注意,如果脚本解释器是/bin/sh,[[ ]]可能不可用,写POSIX兼容脚本时得回到[ ]或test。

5.2 文件、字符串、数值:三类判断场景速查

文件相关判断是最常用的,速查如下:

  • -e 路径:路径是否存在
  • -f 路径:是否为普通文件
  • -d 路径:是否为目录
  • -r / -w / -x:是否可读、可写、可执行
  • -s 路径:文件是否非空
  • -L 路径:是否为符号链接

字符串判断常用的有:

  • -z "$var":字符串为空
  • -n "$var":字符串非空
  • "$a" = "$b":两个字符串相等
  • "$a" != "$b":两个字符串不相等

数值判断是另一组,和字符串比较长得完全不一样,容易混淆:

  • -eq:等于
  • -ne:不等于
  • -lt:小于
  • -le:小于等于
  • -gt:大于
  • -ge:大于等于

注意,数值比较的写法是[ "$num" -gt 10 ],这里的-gt不能换成数学上的>,后面马上会解释为什么。

5.3 新手最容易翻车的三个test坑

第一个坑:变量不加引号。比如用户没有输入内容,var为空,此时[ $var = "yes" ]展开后变成[ = "yes" ],直接报错说缺了操作数。正确写法是[ "$var" = "yes" ]。这再次印证了上一章的原则:变量一律加双引号,特殊情况另说。

第二个坑:把>、<用在[ ]里。[ "$a" > "$b" ]根本不是在比较大小,>在这里是输出重定向符号,会把"$b"当成一个文件并把空内容写进去,同时产生一个同名的垃圾文件。要比较字符串大小,要么用[ "$a" \> "$b" ]给尖括号加转义,要么用[[ "$a" < "$b" ]],后者更清晰。

第三个坑:数字和字符串的比较方式搞混。判断变量num=10是否大于5,写成[ "$num" > 5 ]同样是重定向问题;写成[ "$num" -gt 5 ]才是正解。字符串的“相等”用=,数值的“等于”用-eq,两者不能互通,这是入职笔试和实际排障里反复出现的考点。

5.4 test的扩展用法:组合条件与模式匹配

复杂判断可以用&&和||组合条件,也支持布尔非。推荐写成两个[ ]之间加逻辑符号的形式,可读性更好:

if [ -f "$file" ] && [ -r "$file" ]; then echo "文件存在且可读" fi

在[[ ]]里可以直接写&&、||、甚至正则匹配:

if [[ "$name" == *.zip ]]; then echo "这是一个zip包" fi

这里==右边的*.zip是模式,不是普通字符串,所以不需要加引号。很多老手写脚本时偏爱[[ ]],就是因为它的语义更接近自然表达,不容易踩上面那些坑。

6. 四命令联合作战:写一个可交互的检测脚本

学命令好比收集零件,把它们组合在一起才算真的会组装机器。这里我把四条命令全部放进去,写一个简单的系统检测脚本,用来演示它们是如何协作的。

6.1 脚本目标与功能设计

这个脚本要实现三件事:显示当前主机基本信息;给用户列出可执行的操作菜单;根据用户选择做不同处理。核心逻辑无非是:用printf展示菜单,用read读用户选择,用test判断用户输入是否合法,用echo或printf输出结果。

脚本开头我用了#!/usr/bin/env bash,明确指定bash解释器,这样可以放心使用[[ ]]、read -p等增强特性,不会因为sh环境不同而行为怪异。

6.2 完整脚本与逐段解读

#!/usr/bin/env bash printf "\033[36m========== 系统信息 ==========\033[0m\n" printf "%-12s: %s\n" "主机名" "$(hostname)" printf "%-12s: %s\n" "系统版本" "$(uname -srm)" printf "%-12s: %s\n" "当前用户" "$(whoami)" printf "\n请选择要执行的检查:\n" printf "1) 磁盘使用率\n" printf "2) 内存使用率\n" printf "3) 退出\n" read -n1 -p "输入选项 [1-3]:" choice echo case "$choice" in 1) df -h ;; 2) free -h ;; 3) echo "再见" exit 0 ;; *) printf "\033[31m无效选择:%s\033[0m\n" "$choice" exit 2 ;; esac if [ "$?" -eq 0 ]; then printf "\033[32m执行完成\033[0m\n" fi

一点点拆开看。printf里带\033[36m和\033[0m,这是ANSI颜色码,前者让文字变成青色,后者关闭颜色效果,这是我在生产环境里更偏爱的彩色输出方式,因为它不依赖echo -e。%-12s则让所有字段左对齐,输出整齐。

read -n1只读取一个字符,用户不用敲回车就能触发选择,交互体验更顺。这里必须紧跟一个echo,否则后续所有输出都会挤在同一个行尾。case接收这个字符做分支,如果用户乱按了其他键,就进入*)分支,这里用printf把非法输入原样回显出来。

最后一行的判断if [ "$?" -eq 0 ],检查的是上一个命令的退出状态。$?保存着前一条命令的返回码,-eq 0表示成功。这是test在脚本里最常见的应用之一:根据退出码判断后续流程。整体看下来,输出、输入、判断、分支四件事全齐了,而且每一处都刻意用了相对稳妥的写法。

7. 常见报错与排查速查表

脚本写多了,你会发现大部分报错根本不是随机事件,而是固定的几个原因反复出现。下面这张表是我整理的高频问题速查,基本都能对号入座。

现象常见原因解决办法
echo -e不生效,输出里带-e脚本用/bin/sh运行,当前Shell不支持该选项改用printf,或者脚本显式写#!/usr/bin/env bash
printf输出中文乱码文件编码、终端字符集、locale不一致脚本存UTF-8,检查locale,用export LANG=zh_CN.UTF-8
%c输出中文出现残缺%c只取第一个字节,中文占多字节改用%s
read读文件漏掉最后一行文件最后一行没有换行符使用while IFS= read -r line || [ -n "$line" ]
管道里while read修改的变量外部为0while在子Shell中执行,变量改动带不出来把cat file | while改成while ... done < file
报错unary operator expected变量为空且没加双引号所有变量统一加双引号
报错[: too many arguments变量内容被拆分成多个词变量加双引号,检查输入中是否带空格
[ "$num" > 5 ]生成一个名为5的文件在[ ]里>被当成重定向数值比较使用-gt,字符串比较用\>或[[ ]]
if里明明相等却走不到对应分支数值用了=,字符串用了-eq记住:字符串比相等用=,数值比相等用-eq
read -n1之后第二个read自动跳过残留的回车符留在缓冲区在-n之后补一个空read清理缓冲

排查问题有个通用思路:先看脚本用的是哪个解释器,再看变量有没有加引号,最后考虑编码和子Shell。这三板斧砍下去,能解决掉至少七成Shell脚本里莫名其妙的报错。

最后说一点个人习惯,也算不上什么大道理:我写脚本时,凡是面向用户的正式输出,一律交给printf;凡是临时调试用的输出,随手用echo;凡是涉及条件判断,先把判断条件在脑子里翻译成test表达式,再动手写if。这套流程谈不上精妙,但用来保证脚本稳定和后续好维护,效果一直不错。你按这个思路把你自己的脚本翻一遍,很多之前绕来绕去的问题,应该都能变得清楚起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询