十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Shell脚本初级测试题精选:变量、循环、条件与避坑解析

Shell脚本初级测试题精选:变量、循环、条件与避坑解析 Shell脚本这个东西属于典型的“看着简单、一写就废”的技能。语法书翻过一遍变量会了循环会了可真到了自己动手写一个能用的脚本时各种奇奇怪怪的问题全冒出来了引号没加报错、空格多了报错、for循环遍历的文件多了一个隐藏目录……我接触过很多刚入门的朋友最常问的一句话就是“有没有shell脚本练习题能练练手”。所以这一篇就干脆整理一套Shell脚本初级测试题从变量、条件、循环到综合实战全部带解析和避坑说明。不管你是刚接触Linux的新手还是想补一补基本功的运维或开发都可以拿来自测一遍看看到底哪些知识点是真会了哪些只是“眼会了”。1. 测试题的设计思路与知识范围1.1 为什么用“做题”的方式学Shell看教程和写脚本之间差距远比想象中要大。很多人在终端里敲echo hello world没问题但让他写一个判断目录是否存在、不存在就创建的脚本就开始卡壳了。刷题的本质是把零散的知识点逼进实操场景里你必须在限定条件下完成一个目标过程中会踩坑踩了坑才会真正记住。用测试题的方式去学Shell还有一个好处是能快速暴露盲区。比如变量替换、命令替换、退出码、位置参数这些东西光看解释会觉得“这有什么难的”可一放到题目里经常会出现 “为什么我这么写没效果”“为什么明明文件存在判断却是假”这类问题。这些盲区靠阅读是补不起来的必须靠输出倒逼输入。这套题的花样不算多但每个题都经过筛选尽量贴近日常运维和开发场景不考偏门语法。做完一遍脚本的编写思路会清晰很多。1.2 题目难度与考察点编排逻辑整套题大致分四个模块难度递进模块考察范围对应章节基础语法变量、引用、位置参数、算术运算第2章条件与循环if判断、for/while、case分支第3章综合实践文件处理、管道组合、日志清理、函数第4章调试排错语法检查、运行追踪、常见报错第5章每一章的题目都采用“题目 常见错误演示 正确写法 原理说明”的结构。你先自己做再看答案重点看我标注的“坑”和“为什么”这两个才是这套题最值钱的部分。2. 基础题型详解变量、参数与算术运算2.1 变量赋值与引用的两个经典坑题目写一个Shell脚本定义一个变量内容为hello world中间有空格然后用echo输出这个变量。很多人一开始会这么写#!/bin/bash ahello world echo $a执行之后报错world: command not found原因很明确Shell的赋值语法中等号两边不能有空格但变量值本身也会做“分词”处理。它看到world是一个独立单词就把它当成一条命令来执行了。正确写法是加引号#!/bin/bash ahello world echo $a这里还有一个进阶版坑输出的时候echo $a和echo $a在很多情况下看似一样但一旦变量里有空格、通配符、分号等特殊字符不加引号就会出问题。比如a*.txt echo $a如果当前目录下有a.txt、b.txt输出会变成a.txt b.txt而不是*.txt。所以我的习惯是所有变量引用一律加双引号除非你明确希望Shell做分词和通配符展开。再补充一个小知识单引号和双引号的区别。单引号内部的所有字符都是字面量$、反引号、双引号都不会被解释双引号则允许变量展开和命令替换。实际写脚本时绝大多数场景用双引号就够了。2.2 位置参数与特殊变量题目写一个脚本传入三个参数分别输出脚本名、参数个数、所有参数、第一个参数和最后一个参数。考察的是这几个特殊变量$0脚本名$#参数个数$所有参数每个参数独立$*所有参数合在一起$1、$2、$3按位置取参数$?上一条命令的退出码一个标准参考写法#!/bin/bash echo 脚本名: $0 echo 参数个数: $# echo 全部参数: $ echo 第一个参数: $1 echo 最后一个参数: ${!#}注意最后一个参数的写法。如果直接用$!那表示的是“后台执行的上一个进程PID”不是最后一个参数。正确方式是${!#}这个知识点很多人不知道。$和$*在没有双引号包裹时表现一样但加了双引号就有区别for arg in $; do echo $arg done用$遍历每个参数都是独立的用$*遍历所有参数会先合并成一个字符串再被当作一个整体。你在脚本里看到“为什么循环只执行了一次”大概率就是这里写错了。2.3 算术运算的三种标准写法题目定义一个变量i1让它自增分别用三种方式实现并输出结果。Shell里的算术运算不像C语言那么自由写ii1会被当成字符串拼接或者直接报错。常用的三种写法#!/bin/bash i1 # 写法一双小括号 i$((i 1)) echo $i # 写法二let let i echo $i # 写法三expr注意空格 i$(expr $i 1) echo $i我最推荐的是第一种$((...))语法清晰、支持变量名直接使用、还支持、--、这些操作符。不推荐用expr因为空格要求严格容易踩坑。算术运算还有一个隐蔽问题空变量。如果某个变量没赋值$((a 1))不会报错会按0处理。这在很多场景下是好事但也可能导致逻辑错误——你以为变量有值实际一直是0。严谨的写法是在使用前先做一次默认值设置a${a:-0}这样如果a未定义或为空就自动补成0。3. 条件与循环核心题型if、for、while实战3.1 if判断与测试表达式题目写一个脚本判断/tmp/test.txt是否存在并且可读存在则输出可以读取否则输出文件不存在或不可读。先看一个容易犯的错#!/bin/bash if [ -f /tmp/test.txt ] [ -r /tmp/test.txt ]; then echo 可以读取 else echo 文件不存在或不可读 fi这个写法本身没问题但有一个细节[后面、]前面必须有空格。写成[-f /tmp/test.txt]、[ -f /tmp/test.txt]都会报command not found因为[其实是一个命令test命令的别名命令和参数之间必须有空格。为了兼容性和可读性条件测试我通常推荐用[[ ]]而不是[ ]。区别在于[[ ]]是关键字解析更宽松支持、||不需要转义[[ ]]支持~正则匹配[ ]里写必须拆成多个[ ]或者用-a上面的题改成[[ ]]风格#!/bin/bash if [[ -f /tmp/test.txt -r /tmp/test.txt ]]; then echo 可以读取 else echo 文件不存在或不可读 fi这里顺便补充一个高频考点文件测试运算符。-e判断存在-f判断普通文件-d判断目录-r判断可读-w判断可写-x判断可执行-s判断文件非空。试着自己写一遍这些判断比背表格有用得多。3.2 for循环的三种遍历写法题目分别实现三种for循环——遍历当前目录下所有.txt文件、遍历1到10、遍历显式给出的单词列表。先看第一种也是最容易出错的#!/bin/bash for file in *.txt; do echo 找到文件: $file done坑在哪里如果当前目录下没有.txt文件*.txt不会自动变成“空”而是会原样保留为字符串*.txt然后循环体执行一次输出“找到文件: *.txt”。这是Shell通配符的典型陷阱。严谨的写法是在循环前判断一下#!/bin/bash shopt -s nullglob for file in *.txt; do echo 找到文件: $file doneshopt -s nullglob的作用是让没有匹配到的通配符展开为空循环体一次都不执行。这个选项在日常脚本里很实用建议记下来。第二种遍历1到10#!/bin/bash for i in {1..10}; do echo $i done花括号扩展{1..10}很简洁但注意它只支持字面量不支持变量。如果上限是变量就得用seq或C风格写法#!/bin/bash max10 for ((i1; imax; i)); do echo $i doneC风格for循环的好处是条件灵活支持变量也支持步长控制。第三种遍历单词列表#!/bin/bash for name in tom jack lucy; do echo 名字: $name done如果把列表换成“从文件读取”更常见的做法是配while read放到下一节的综合题里。3.3 while read逐行读取文件题目用while read逐行读取/etc/passwd统计总行数并打印每一行的第一列用户名。参考实现#!/bin/bash count0 while IFS read -r line; do user$(echo $line | cut -d: -f1) echo 用户: $user count$((count 1)) done /etc/passwd echo 总行数: $count这里有两个关键点新手特别容易踩第一是IFS。IFS是Shell的字段分隔符默认包含空格、Tab、换行。read读取一行时会用IFS把行左右两端的空白字符去掉。如果某一行以空格结尾不加IFS就会出现内容丢失。所以读取文件内容时习惯性写成IFS read -r line是安全的。第二是-r选项。它禁止反斜杠转义。如果不加-r文件中出现\n、\t这样的内容时read会把它们当作转义符处理导致内容失真。还有一个容易出现的问题while read和管道的子Shell。如果你写cat /etc/passwd | while read line; do count$((count 1)) done echo $count输出的count大概率是0。原因在于管道右边的while运行在子Shell里子Shell中的变量修改不会影响到父Shell。解决办法就是我上面演示的输入重定向写法done /etc/passwd让循环运行在当前Shell中。3.4 case多分支结构题目写一个脚本模拟服务的启动停止操作。接收start、stop、restart三个参数分别输出对应的操作日志传入其它参数时提示用法。用case实现#!/bin/bash case $1 in start) echo 正在启动服务... ;; stop) echo 正在停止服务... ;; restart) echo 正在重启服务... ;; *) echo 用法: $0 {start|stop|restart} exit 1 ;; esaccase的匹配规则有两个技巧一是*)分支是兜底项任何不匹配的情况都会走到这里。做命令行参数解析时这个分支通常用来打印帮助信息不要省略。二是模式支持通配符和|多模式匹配case $1 in start|begin|go) echo 启动 ;; esaccase比if链的代码可读性好很多参数一旦超过两个优先考虑case。4. 综合实践题从单句命令到完整脚本4.1 文件批量改名与移动题目当前目录下有一批以.jpg结尾的图片文件把它们统一改名为.jpg.bak并统计一共改了多少个文件。参考实现#!/bin/bash count0 for f in *.jpg; do [[ -e $f ]] || continue mv $f ${f}.bak count$((count 1)) done echo 共修改 $count 个文件这里我特意加了一行[[ -e $f ]] || continue用来处理“没有.jpg文件”的情况。上一节说过没有匹配时*.jpg会原样保留循环体执行一次加上存在性判断后如果这个“文件名”实际上不存在就直接跳过。进阶一点的问题把文件名中的空格替换成下划线。这个题目很常见因为批量处理从Windows拷贝过来的文件时经常遇到空白字符。实现方案#!/bin/bash for f in *.mp4; do [[ -e $f ]] || continue newname${f// /_} mv $f $newname done${f// /_}是Shell的字符串替换语法把变量f中所有空格替换成下划线。注意写法和正则类似但这里不是正则中间的空格就是字面空格。4.2 管道组合与文本统计题目统计/var/log/nginx/access.log中状态码为404出现的次数并输出次数最多的前3个IP。先看统计次数的实现#!/bin/bash grep 404 /var/log/nginx/access.log | wc -l这个比较简单。关键是前3个IP的统计需要组合awk、sort、uniq#!/bin/bash awk / 404 /{print $1} /var/log/nginx/access.log \ | sort \ | uniq -c \ | sort -rn \ | head -3管道从左到右的作用分别是提取所有出现404的行并取第一个字段IP、排序、统计出现次数、按次数降序排列、取前3行。这个思路几乎可以套用到所有日志统计场景建议背下来。需要注意一点uniq只能去除相邻的重复行所以必须先sort再uniq -c。如果不排序出现次数会被拆成多段统计结果完全错误。这是我见过最多人踩的坑。4.3 日志清理脚本综合题题目写一个脚本清理/var/log/myapp/下30天前的.log文件保留目录结构输出删除的文件名和总共释放的空间。参考实现#!/bin/bash log_dir/var/log/myapp find $log_dir -type f -name *.log -mtime 30 -print0 | while IFS read -r -d file; do size$(du -sk $file | cut -f1) rm $file echo 已删除: $file (${size}KB) done这里用到了-print0和read -d 目的是处理文件名中的空格和换行。如果文件名里没有空格简化写法也够用find $log_dir -type f -name *.log -mtime 30 -delete但要注意find -delete是直接把匹配到的文件删掉没有任何确认过程。生产环境里我习惯的做法是先用不带-delete的find把文件列表列出来人工确认再加上-delete执行。确认无误后还可以加上-print输出删除日志。如果担心删错可以用find先移动到备份目录确认运行正常后再清理备份目录find $log_dir -type f -name *.log -mtime 30 \ -exec mv {} /backup/log_archive/ \;这种“先备份再删除”的思路比直接删除安全得多适合放到定时任务里。4.4 函数编写与退出状态码题目写一个函数传入一个端口号判断该端口是否被占用。被占用返回0未被占用返回1。参考实现#!/bin/bash check_port() { local port$1 if ss -tln | awk {print $4} | grep -q :${port}$; then return 0 else return 1 fi } if check_port 8080; then echo 端口8080被占用 else echo 端口8080空闲 fi这里涉及到两个重点第一个是local变量。函数内部用local声明变量的作用域限在函数内避免污染全局变量。写比较长的脚本时这是保持变量清晰的重要手段。第二个是返回值。Shell函数没有真正的返回值靠return返回0~255的整数调用方用$?或if 函数名来判断。return 0代表成功非0代表失败这个约定和命令执行状态码是一致的。端口判断那里不同系统的命令不一样。CentOS 7以前用netstat新版本用ss写脚本时最好先判断命令是否存在。这里给一个兼容版本check_port() { local port$1 if command -v ss /dev/null 21; then ss -tln | grep -q :${port} else netstat -tln | grep -q :${port} fi }command -v是检查命令是否存在的最稳妥方式输出重定向到/dev/null是为了避免把命令路径打印出来干扰脚本输出。5. 常见问题与调试技巧实录5.1 语法检查与运行追踪bash -n 和 bash -x我在实际调试脚本时第一件事永远是用bash -n script.sh做语法检查。这个命令只检查语法错误不实际执行脚本能把“少了个fi”“if后面多了个空格”这类低级错误一次性找出来。举例来说下面这段脚本有一个常见的语法错误#!/bin/bash if [ -f /tmp/a.txt ]; then echo exists运行bash -n会报syntax error: unexpected end of file看到这个报错十有八九是if没有对应fi。语法检查通过后如果逻辑还有问题就用bash -x script.sh执行。它的作用是把每条命令展开执行的过程打印到终端前缀表示命令展开后的结果。比如 a10 echo 10 10这样你能清晰地看到变量展开成了什么、命令实际执行的是什么。定位“变量没值”“条件判断不对”这类问题特别有效。小脚本可以直接bash -x跑复杂脚本可以在脚本内部临时加set -x打开追踪调试完再set x关闭。5.2 最容易出现的三类报错解读第一类是command not found。出现原因一般是三种脚本没有加#!/bin/bash或编辑器里带了不可见字符变量赋值时等号两边有空格命令名拼写错误。排查时可以执行bash -x script.sh加上追踪输出后到底哪一行出了问题一目了然。第二类是syntax error near unexpected token。多半是if和then没写在同一行还少了个分号或者case分支缺少;;。如果你是从其他语言转过来写Shell最容易犯的就是把Python的行结构套到Shell上——Shell对换行和空格的处理逻辑完全不一样。第三类是integer expression expected。这个报错出现在test或[ ]做整数比较时变量不是纯数字。比如aabc if [ $a -gt 5 ]; then echo yes fi就会报这个错。排查思路先确认变量确实被正确赋值再确认变量没有包含换行或空格。可以从echo [$a]开始看把变量用中括号包起来立刻能发现是不是有多余空白。5.3 新手自查清单我见过太多新人在刷题时问“为什么我写的和答案一样却报错”大部分时候问题出在一些容易被忽视的细节上。把这套检查清单放在手边能省很多事检查项正确做法等号两侧赋值时a1等号两侧不能有空格条件语句空格[ $a 1 ]方括号内侧必须有空格变量引用统一用双引号包裹如$a整数比较用-eq、-gt不是、if/fi配对每个if必须有fi每个case必须有esac执行权限脚本需chmod x或使用bash script.sh换行符在Windows编辑过的文件需转成Unix换行否则会报错关于换行符的问题多提一句很多同学在Windows上用记事本或某些编辑器写脚本传到Linux后执行就报bad interpreter这是因为文件带了\r结尾。用sed -i s/\r$// script.sh就能处理掉或者用dos2unix script.sh一步到位。最后再分享一个小技巧。做练习题的时候不要只满足于“答案能跑通”我一般会建议给每个题目的答案加上注释写清楚“这段的作用是什么”“这个参数为什么这么设”。过两周再回头看能看懂自己写的东西才算真正掌握了。Shell脚本的知识点其实不多但细节密布多写几遍就会发现那些曾经让你头疼的语法慢慢都会变成肌肉记忆。
返回列表