ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Shell编程基石:echo、read、printf、test四大命令的实战避坑指南

Shell编程基石:echo、read、printf、test四大命令的实战避坑指南 1. 为什么要花一整篇去讲这四个命令做Shell编程这几年我见过太多脚本跑着跑着就崩的案例十有八九都出在echo、read、printf、test这四个命令上。你说它们简单确实语法一眼就能看完。但恰恰是这种“看着简单”的错觉让很多人在写脚本时栽了跟头——要么变量带空格被拆得七零八落要么read读文件读到一半丢了数据要么printf输出中文变成一堆乱码要么test在数字比较上直接给了个让人摸不着头脑的报错。这四个命令其实是Shell脚本的底层骨架echo负责把信息抛出来read负责把外部输入接进来printf负责把内容按格式钉死test负责把条件判断立起来。你写任何一个有实际用途的脚本几乎都绕不开它们。尤其是自动化部署、日志采集、系统巡检这一类场景这四个命令是否用得顺手直接决定脚本的健壮性和可维护性。这篇不是命令手册的搬运我把这些年实际踩过的坑、排查过的诡异问题、以及最终沉淀下来的用法都整理出来按四个命令逐个拆解最后再聊一些组合实战和故障排查的实录。无论你是刚入门Shell的新手还是写了两三年脚本但偶尔还会被引号折腾的老手这篇都值得花十几分钟仔细看一遍。2. echo最基本的输出也是最容易被坑的输出2.1 先搞清楚你用的是内建echo还是外部echo绝大多数情况下你在终端敲echo用的是Bash的内建命令而不是/bin/echo这个外部程序。这两个东西行为有差异最典型的就是转义参数-e的处理。Bash内建echo默认不解释反斜杠转义序列想输出带换行的内容得显式加-eecho 第一行\n第二行 # 输出第一行\n第二行\n是字面量 echo -e 第一行\n第二行 # 分两行输出而/bin/echo在部分系统上默认就解释转义。这种不一致正是脚本从一台机器搬到另一台机器就行为异常的常见源头。建议在脚本里明确写清楚要么统一用echo -e要么干脆用printf替代把格式控制权拿到自己手里。顺带提一个我经常用的小写法。调试脚本的时候我喜欢在关键节点打这样的标记echo $(date %F_%T) 开始执行 check_disk 等号加时间戳日志里一搜就能定位到每个阶段的边界。别嫌土排查线上问题的时候这种朴素的标记比花里胡哨的日志框架好使多了。2.2 echo开启转义后的颜色和样式控制早期脚本输出日志时为了区分错误和普通信息我习惯用ANSI转义序列给文字上色。在echo -e后面直接拼颜色码比如echo -e \033[31m错误磁盘空间不足\033[0m echo -e \033[32m成功备份已完成\033[0m\033[31m是红色起始\033[0m是恢复默认。这套东西在终端里显示没问题但如果脚本输出被重定向到文件颜色码会一起写进文件日志系统里就多出一堆可见的乱码符号。后来我学到的做法是判断一下输出目标是不是终端是终端才加颜色重定向就干干净净输出纯文本。if [ -t 1 ]; then echo -e \033[31m错误信息\033[0m else echo 错误信息 fi-t 1判断文件描述符1是否是终端这个技巧在日志脚本里特别实用。很多人问为什么生产环境的脚本日志里没有颜色就是这个原因。2.3 引号处理echo最容易翻车的点我见过最多的问题是变量值里带空格或带通配符时echo输出出来的结果和预期完全不一样。看这个例子file_lista.txt b.txt echo $file_list # 输出 a.txt b.txt好像没问题 echo $file_list # 同样是 a.txt b.txt但语义不同表面看两个都输出一样但如果变量里有换行符或者多个连续空格不加引号会直接被Shell拆词重排输出就变形了。更危险的情况是变量值里有星号不加引号时星号会被展开成当前目录下的所有文件名echo瞬间变成目录列表工具轻则输出杂乱重则引发后续命令操作错误文件。我的规矩很简单echo $var永远带双引号。除非你明确知道我想让它分词否则不带引号的echo我写代码时直接当bug处理。2.4 关于echo输出换行的一个冷知识有个老梗说“Windows换行是\r\nLinux是\n”但很多人没意识到echo的输出默认就是在结尾补一个\n。如果你的脚本在Windows上用编辑器编辑保存成CRLF换行格式再传到Linux上跑每行结尾都会带着一个\recho输出时这个\r会让终端光标回到行首看起来就像输出被覆盖了一样。这个坑在写脚本时年年都有人踩建议所有脚本文件统一用LF换行最好在编辑器里设置默认。3. read把输入稳稳接进来没那么简单3.1 read的常用参数一张表讲清楚read的作用是读取一行输入按空格或IFS拆分依次赋给后面的变量。参数不少但真正高频使用的是下面这些参数作用使用场景-p先输出提示语再读取交互式输入-n读取指定字符数就返回按键选择菜单-t设置超时时间单位秒防止脚本卡死-s静默模式不显示输入内容密码输入-r禁用反斜杠转义读取含反斜杠的路径-a将输入按空格拆分赋值给数组批量解析最常用的是-p一句话就把提示和读取干完了read -p 请输入目标端口号 port要限制用户在5秒内完成输入加个-t。比如巡检脚本里问管理员是否继续等5秒没回应就自动跳过不至于人离开终端脚本就挂在那if ! read -t 5 -p 5秒内未操作则跳过按y继续 choice; then echo 操作超时自动跳过 fi这里必须注意read超时返回非0状态所以要用if包一层来判断否则变量没赋到值脚本还会继续往下走。3.2 逐行读取文件时小心IFS和转义read最常见的非交互用法是从文件读内容。想按行处理文件大多数人第一反应是cat file.txt | while read line; do echo $line done这样能用但不严谨。问题有两个。一是管道会让while在子Shell里执行循环里给变量赋的值在循环结束后丢失二是read默认会拆分行内空格行里若有多个空格就只能拿到第一段。严谨写法之一是去掉cat直接重定向让循环留在当前Shell进程里while IFS read -r line; do echo $line done file.txt我对每一处的解释IFS把行内分隔符临时置空整行内容原样进变量-r避免行尾反斜杠被吞 file.txt把文件用重定向喂给循环不用cat也没子Shell问题。这三件套我几乎在每一个读文件的脚本里都会写。3.3 读密码和读选项的特殊处理让用户输密码千万记得-s但-s不回显输入提示语要写清楚。而且读完后要手动补一个换行否则终端上光标会停在密码输入那行后面read -s -p 请输入sudo密码 pwd echo这里的echo就是用来补换行的配合read使用非常经典。读单字符选项用-n 1只取一个按键不用等回车read -n 1 -p 是否继续[y/n] confirm echo case $confirm in y|Y) echo 继续执行 ;; *) echo 退出脚本 ;; esaccase里的|是或逻辑yY大小写都接住。这个小交互模板我几乎每个脚本都在用。3.4 多个变量一次性读取的隐藏逻辑read的一大优势是一次读多段赋值给多个变量。默认按IFS拆分空格或Tab都能拆read ip port user 192.168.1.10 8080 admin这里是Here String把字符串作为标准输入喂给read。这个用法很巧妙用一行代码就完成了“把一段文本拆成多个字段并分别赋值”的操作日志解析、配置解析场景中非常常见。要注意的是如果实际输入段的个数少于变量个数多余的变量就为空如果输入段个数多于变量个数最后那个变量会吃掉剩余所有内容。这个特性有时候可以故意利用比如read first rest hello world shell scripting echo $first # hello echo $rest # world shell scripting3.5 读取关联数据时的超时与校验生产环境中read读取的输入必须先校验再使用否则后面跟着命令可能被特殊字符“干掉”。一个不错的习惯是读取端口号后做数字校验read -p 端口号: port if [[ ! $port ~ ^[0-9]$ ]]; then echo 端口号必须为数字 exit 1 fi正则匹配判断是否纯数字这招在参数校验时通用性极强。凡是read读进来的参数我都默认先做合法性检查再进业务逻辑。4. printf格式化输出的正统方案4.1 为什么我推荐优先用printf而不是echoprintf严格来说不是输出工具而是一个按格式模板生成文本的工具。它的第一个参数是格式串后面的参数按格式串里的占位符依次填入。这个设计让printf的输出完全可控不会有echo那种“反斜杠解释不解释”的困惑。在脚本里做任何需要严格格式的输出我都优先选择printf。对比一下同样输出一行带变量的内容nameweb printf 服务名: %s 状态: %d\n $name $status echo 服务名: $name 状态: $statusprintf里%s和%d定义了参数的类型传错了会被纠正或报错echo纯粹是字符串替换状态值如果非数字格式上根本看不出来。对日志系统而言printf这种严格格式意味着后续用awk/sed解析时不会被意外打乱。4.2 常用格式说明符与转义printf最常用的占位符是%s字符串、%d十进制整数、%f浮点数以及%-Ns这种带宽度的左对齐写法。我做一个表格整理一下核心用法格式含义示例输出%s字符串printf %s abc%d十进制整数printf %d 42%05d补零宽度5位printf %05d 42 输出00042%.2f保留2位小数printf %.2f 3.14159 输出3.14%-10s左对齐宽度10printf %-10s name\n换行printf a\nb\t制表符printf a\tb宽度补零和保留小数这两个在生成报表时特别有用。比如统计脚本要输出磁盘使用百分比想对齐表格用%-10s加%5d就能整齐地排成列。4.3 格式化输出到变量而不是屏幕printf一个容易被人忽视的能力是把格式化结果保存到变量用命令替换即可formatted$(printf %-10s | %9.2f $user $score) echo $formatted这个用法在执行环境里配置参数拼接时极其好用比如拼带参数的SQL语句、拼IP加端口字符串既能保证格式统一又避免了一堆字符串拼接的可读性问题。4.4 中文乱码问题核心在locale不在printf热搜词里“printf中文乱码”出现频率很高。很多人以为是printf不支持中文实际不是。中文乱码大多是因为终端或环境变量locale没设对。printf严格按照字节流输出中文按UTF-8编码本来就没问题但如果你输出后终端用GBK解码或者脚本开头没有export LANG就可能出现乱码。建议脚本开头统一写上export LANGzh_CN.UTF-8 export LC_ALLzh_CN.UTF-8再用printf输出中文配合运行环境的终端UTF-8编码基本不会乱码。另外要注意printf的宽度计算是按字节数来的中文字符占3个字节UTF-8所以%s的宽度填充在中英文混排时会显得不够整齐这一般无解行业常见做法是改用awk或python来做对齐。4.5 printf拼接JSON时的转义处理现在很多脚本需要生成JSON上报到监控系统printf在这里表现不错但要注意双引号要转义printf {host:%s,port:%d}\n $host $port用单引号包住格式串里面的双引号就不用转义了。而%s对应的变量值如果本身包含双引号或者反斜杠得先在变量层做替换处理否则拼出来的JSON不合法。这个细节我在写监控上报脚本时反复踩过。5. test条件判断的基础设施5.1 test的三种写法别混着用test命令在Shell里有三种等价形态test expr、[ expr ]、[[ expr ]]。前两者是外部命令或内建命令的调用[[ expr ]]是Bash的关键字。我的建议是在Bash脚本里尽量用[[ ]]因为它的运算符更丰富、支持正则匹配、且不会像[ ]那样在某些极端参数上翻车。看一个典型对比# 传统test写法 test -f /etc/hosts echo 文件存在 [ -f /etc/hosts ] echo 文件存在 # 推荐写法 [[ -f /etc/hosts ]] echo 文件存在三者功能类似但对空变量、特殊字符变量的处理上有差别。[ ]里变量没加引号变量为空时会被拆成语法错误[[ ]]内部对变量做了额外处理即使不加引号问题也不大。虽然我依然建议加引号但至少[[ ]]给了多一道保险。5.2 文件测试和字符串测试速查test主要分三类文件测试、字符串测试、整数测试。先看文件测试这也是我写得最多的表达式含义-f file是否为普通文件-d file是否为目录-e file是否存在-r file是否可读-w file是否可写-x file是否可执行-s file是否非空文件字符串测试中最常用的就是判断变量是否为空[[ -z $var ]] echo var为空 [[ -n $var ]] echo var不为空-z表示字符串长度为0-n表示长度不为0。注意-z和-n后面最好带引号虽然[[ ]]对引号不那么敏感但保持这个习惯能避免不少边缘问题。5.3 数字比较和字符串比较的陷阱这是test最大的坑。数字比较用-eq、-ne、-gt、-lt、-ge、-le字符串比较用、、!、、。如果把字符串比较符号用在数字上比如[ 10 2 ] echo 成立这里被当成输出重定向10 2等于把2写入文件10执行结果完全错误。正确写法是[ 10 -gt 2 ] echo 成立而如果用写数字比较比较的是字符串内容而不是数值。最经典的例子[ 10 9 ] echo 字符串比较不成立 [ 10 -eq 9 ] echo 整数比较不成立 [ 9 -eq 09 ] # 这个会报错因为09被当成八进制时非法最后一行是我踩过最深的一个坑。数字带前导零时在部分Shell里会按八进制解析。处理端口、月日这类可能带零的数值时一定要先去零或统一转成十进制再比较value09 value$(echo $value | sed s/^0*//) [ -n $value ] [ $value -gt 5 ] echo 大于55.4 组合条件、||在test里的优先级test条件可以自由组合[[ -f /etc/passwd -r /etc/passwd ]] echo 可读的passwd文件 [[ -n $a || -n $b ]] echo a或b至少一个非空和||在[[ ]]里的优先级比[ ]要更自然。在[ ]里写多个条件会被整体当成一次test调用容易出错而[[ ]]允许直接用、||组合代码好读得多。如果遇到“变量为空导致test语法错误”这种疑难杂症第一时间检查是不是用了旧式[ ]且漏了引号。5.5 逻辑短路与结合if的使用节奏test经常配合if使用但我更常用的是短路语法[[ -d $backup_dir ]] || mkdir -p $backup_dir这行代码等于“目录不存在就创建”比if-else写起来简洁太多。同样的逻辑还可以做参数默认值[[ -n $env ]] || envprod注意这两个例子都在用||的“不满足就执行”特性。命令行的可读性虽然不如if明显但脚本看多了就会喜欢上这种紧凑写法。不过要节制超过三层组合时还是老老实实写if否则维护成本太高。6. 四大命令组合实战以及我排查过的几个典型故障6.1 一个典型的交互式部署脚本框架把四个命令串起来最常见的场景是交互式部署脚本。我来搭一个简化版本的框架展示它们在真实脚本里怎么配合#!/bin/bash export LANGzh_CN.UTF-8 # 读目标环境参数 read -p 目标环境 (dev/test/prod): env read -p 服务端口: port # test校验 [[ $env ~ ^(dev|test|prod)$ ]] || { echo 环境不合法; exit 1; } [[ $port ~ ^[0-9]$ ]] || { echo 端口必须为数字; exit 1; } # printf输出格式化的部署信息 printf 部署环境: %-6s\n服务端口: %d\n $env $port # 确认部署 read -n 1 -p 确认部署? [y/n] confirm echo [[ $confirm ~ ^[Yy]$ ]] || { echo 取消部署; exit 0; } # 模拟执行 echo $(date %T) 开始部署到 $env 这个不到二十行的脚本四个命令各司其职read负责获取输入[[ ]]test负责校验printf负责格式输出echo负责时间戳标记。这种结构我已经用了很長時間遇到生产事故时定位效率比一堆if嵌套高得多。6.2 排查故障读取文件时行内容消失有一回写日志解析脚本用while read line读文件发现中间有些行读出来是空的但源文件明明有内容。排查下来原因有两个一是文件是Windows传上来的行尾是\r\n\r被当成内容读到变量里打印时光标跳回行首看似没有内容二是在循环里又用了echo -e解析变量把底层的特殊符号吃掉了。解决办法也很经典先dos2unix统一换行符再用IFS read -r line三件套读取。上面3.2节提到的那套写法就是为了这种场景准备的。6.3 排查故障printf中文变问号另一个真实案例是脚本输出的中文在终端全变成问号。排查过程是先echo $LANG看环境变量结果显示空再查系统locale是否装了中文包发现压根没生成。最终在脚本里加了export LANGzh_CN.UTF-8并把终端编码调成UTF-8问题解决。如果你的环境里连中文本地化语言包都没有怎么export都不管用那就只能退而求其次用英文提示或者用uni2ascii转换。但绝大多数云主机和桌面发行版装完系统locale就是齐的export一句就能搞定。6.4 排查故障test数字比较报错“integer expression expected”大概是test相关报错里出现频率最高的。原因无一例外变量不是纯数字。比如[ $count -gt 10 ]但count是从日志里grep出来的带了个冒号或者前后有空白就直接报错。排查时先在命令行把变量值打印出来看看用printf %s\n $count就能看到变量值前后有没有隐含字符。我习惯在比较前统一做一次“清洗”去掉非数字字符、去空白、去前导零然后再比较。脚本里宁可多两行清洗代码也别让一个脏数据把整条流程干掉。6.5 关于read超时和管道子Shell的年轻队友常踩的坑团队里的小朋友写了一个批量处理脚本处理完成后需要汇总计数结果循环里count的值最后输出总是0。原因就是他把while read循环放在管道后面子Shell里改的变量带不回主进程。改造方式就是3.2节说的用重定向替代管道# 错误示范 cat list.txt | while read line; do ((count)) done echo $count # 仍然是0 # 正确示范 count0 while IFS read -r line; do ((count)) done list.txt echo $count # 正确次数这个问题我几乎每隔一阵就会在论坛看人问一次写成脚本时请直接养成重定向的习惯。7. 留在最后的一句体己话这四个命令单个看都没什么好说的但组合在一起就是整个Shell世界的语法骨架。我这些年写脚本最大的感悟是Shell不怕你用得多就怕用得糙。echo多想想引号read多想想IFSprintf多想想格式串test多想想到底是比字符串还是比数字四个点都想清楚了脚本的健壮性直接上一个台阶。最开始学Shell的时候我也曾觉得这些命令太简单没必要深究直到被线上脚本的诡异表现教育了几次才老老实实把每个细节抠明白。希望这篇文章能让你少走我走过的弯路。下次写脚本的时候不妨刻意把printf和test用得更规范一些时间会给你回报。
返回列表