Shell脚本入门到实战:核心语法与高频避坑指南

发布时间:2026/9/15 10:05:32
Shell脚本入门到实战:核心语法与高频避坑指南 最近后台涌进来不少关于Shell脚本的提问热搜词里也满是“shell脚本入门”“shell脚本for循环”“shell中常见坑”“shell面试题”这些。看得出很多人到了真正要用Shell的时候才开始着急补基础。这我太熟悉了——我当年啃Shell就是从复制粘贴网上的命令开始看不懂就硬跑跑通了就行结果一遇到特殊情况就翻车。后来把基础补扎实了再看那些“炫技”的代码其实都是几个基础概念的组合。这篇就把Shell脚本基础系统捋一遍它解决什么问题、命令到脚本之间发生了什么、for循环和shift这类核心语法怎么用、实战中怎么把SFTP拉文件、批量重命名、远程执行命令串起来以及最容易被忽略的高频坑和面试题方向。适合完全没接触过Shell的新手也适合零零散散用过但没系统整理过的朋友。1. 为什么说Shell是Linux世界的第一块敲门砖1.1 你搜索的那些Shell关键词暴露了同一个需求把热搜词拉出来看很有意思“shell脚本入门”说明有一大批人刚走到门口“shell脚本for循环”“shell的shift命令”说明入门之后卡在了语法上“shell中常见坑”说明已经开始实战、并且被坑了“shell面试题”说明学到一定程度准备拿它换offer了。这基本就是一个Shell学习者的完整成长路径。这些关键词背后其实是同一个需求用命令行的方式把重复的、批量的事情自动化。比如把100个文件统一改名比如每天从服务器拉取数据比如检查一批IP哪个在线。这些事情用鼠标点会疯掉用程序写又太重Shell脚本刚好卡在中间——轻量、直接、随手就能写。1.2 Shell到底是什么一个小类比Shell翻译过来是“壳”它是用户和Linux内核之间的翻译官。你敲的每个命令Shell负责解释给内核听内核执行完Shell再把结果反馈给你。类比一下内核是后厨Shell是服务员。你点菜不需要冲进后厨而是告诉服务员服务员转告后厨再把菜端上来。同理你不需要直接跟内核打交道只需要把需求写成命令Shell负责翻译和回传。不同的服务员有不同的风格这就有了bash、zsh、sh、dash这些不同的Shell。常见的Shell类型Shell特点常见场景sh最古老功能最少系统脚本、POSIX兼容bash功能丰富最常用绝大多数Linux发行版默认zsh交互体验好插件多macOS默认、开发者日常dash轻量快速Debian/Ubuntu的sh指向1.3 学会Shell能解决什么问题我的感受是Shell不是一门“编程语言”更像是一双在Linux世界里干活的手。有了它你才能处理日志、管理文件、部署服务、排查故障。具体来说批量操作批量重命名、批量压缩、批量上传下载。日志分析从几十GB的日志里快速提取错误、统计接口访问量。定时任务配合cron每天自动备份、自动清理、自动上报。服务部署拉代码、打镜像、重启服务、健康检查一条龙。适用人群也几乎没有门槛运维、后端开发、数据分析师、测试、学生甚至做嵌入式开发的兄弟最后兜底排查问题也离不开Shell。它不会让你“会编程”但能让你“会干活”。2. 从敲命令到写脚本Shell到底帮你做了什么2.1 命令是怎么被找到的PATH与type新手最容易忽略的一个问题你敲一个ls系统是怎么知道去哪里找它的答案是PATH环境变量。你可以执行echo $PATH看看输出是一堆用冒号隔开的目录。Shell会在这些目录里从左到右查找你输入的命令找到了就执行。所以有些命令你能用有些命令要用绝对路径/usr/local/bin/xxx本质就是它不在PATH包含的目录里。还有一个很容易被坑的点Shell命令分三种——内建命令如cd、echo、外部命令如ls、grep、别名如ll。用type命令可以查type cd # cd is a shell builtin type ls # ls is /bin/ls type ll # ll is an alias for ls -l理解了这一点你就明白为什么有人改了PATH之后一堆命令找不到了也明白为什么同一个命令在脚本里和交互式终端里行为可能不一样——因为脚本可能不加载某些配置别名也不生效。2.2 变量与特殊变量脚本的“内存”Shell的变量定义写起来很随意但有几个硬规矩必须记住namezhangsan # 等号两边绝对不能有空格 echo $name echo ${name} # 推荐写法明确边界 readonly age25 # 只读变量 unset name # 删除变量变量的引号规则是新手重灾区单引号$name里面是什么就是什么$不解析。双引号$name$会解析为变量值但*、?等通配符不会展开。反引号cmd和$(cmd)执行命令把输出当作值。除了自己定义的变量Shell还内置了一批特殊变量脚本里经常用到变量含义$0脚本本身的名字$1、$2...第1个、第2个位置参数$#位置参数的总个数$所有参数每个参数是独立个体$*所有参数整体一个字符串$?上一条命令的退出码0代表成功$$当前Shell的进程PID这些特殊变量在后面讲shift的时候还会用到务必先混个脸熟。2.3 引号、通配符与重定向凑齐脚本的语法零件通配符是Shell让你偷懒的工具。*匹配任意多个字符?匹配单个字符[abc]匹配方括号中的任一字符ls *.log # 查看所有.log结尾的文件 rm file?.txt # 删除file1.txt、fileA.txt等 ls [0-9]*.sh # 以数字开头的.sh文件重定向不复杂但天天用echo hello test.txt # 覆盖写入 echo world test.txt # 追加写入 cat test.txt # 从文件读取输入 ls noexist 2/dev/null # 错误输出丢进黑洞数一下是标准输出2是标准错误21是把错误也塞进标准输出。这是排查脚本报错的关键尤其是定时任务里一旦报错不知道错在哪十有八九是没把标准错误重定向到日志里。2.4 管道Shell的乐高积木管道的思路特别简单前一个命令的输出当作后一个命令的输入。用|连接ps aux | grep nginx | awk {print $2}这条命令干了三件事列出所有进程、筛出nginx相关行、提取第二列的PID。三步是三个独立命令用管道串成一条流水线。这就是Shell编程的核心思维——不要写一个巨复杂的命令而是把简单命令拼起来。命令替换$(...)和管道配合还能实现更“编程化”的操作today$(date %Y%m%d) echo 今天的备份文件名是: backup_${today}.tar.gz把date %Y%m%d执行后的结果存进today变量后面随便用。这一招在日志分析、备份脚本里太常用了。3. for循环、shift与位置参数脚本里的三件套3.1 for循环的三种常见姿势Shell的for循环看似简单但写法多很多人只记住了其中一种。我把最常见的三种整理出来# 方式一遍历固定列表 for ip in 192.168.1.1 192.168.1.2 192.168.1.3; do ping -c 2 $ip /dev/null 21 echo $ip 通了 || echo $ip 不通 done # 方式二遍历命令输出 for file in $(ls *.log); do echo 找到日志文件: $file done # 方式三C风格循环适合数值区间 for ((i1; i10; i)); do echo 第 $i 次 done第一种适合数量少、写死的场景第二种适合“从命令结果里拿一批东西来循环”第三种才是真正的“从1到N”遍历。很多人在写批量建用户、批量改名时非要纠结用哪个其实关键看你的数据源在哪——是已经列好了还是要现算。3.2 批量重命名的实战从jpg到jpeg光说语法没意思来个能直接用的。假设当前目录下有一堆.jpg文件想全部改成.jpeg后缀for file in *.jpg; do mv $file ${file%.jpg}.jpeg done这里有两个知识点${file%.jpg}是Shell的字符串截取语法%表示从尾部删除匹配部分意思就是去掉.jpg后缀。变量一定要加双引号$file防止文件名带空格导致mv命令被拆成多个参数。如果文件名带空格比如my photo.jpg不加引号就会变成mv my photo.jpg my photo.jpeg系统会以为有四个参数直接报错。这个坑太经典了。3.3 shift命令让脚本参数处理变得优雅shift的作用就一句话把位置参数整体左移一位原来的$2变成$1$1被丢掉。它本身不难但它和while、case配合是处理脚本参数的经典套路。举个例子写一个脚本支持-f指定文件、-v输出详细信息、-h显示帮助while [ $# -gt 0 ]; do case $1 in -f) file$2 shift 2 ;; -v) verbose1 shift ;; -h) echo 用法: $0 -f 文件名 [-v] exit 0 ;; *) echo 未知参数: $1 exit 1 ;; esac done echo 处理文件: $file [ $verbose -eq 1 ] echo 开启详细模式这里的核心逻辑while [ $# -gt 0 ]判断还有没有剩余参数进入循环后用case $1匹配当前第一个参数是什么。-f这种带值的参数解析后shift 2把参数和值一起挪走-v这种开关参数shift 1就够了。这个写法几乎是所有命令行工具的模板。看懂了它你就能读懂大部分开源脚本的参数解析部分也能自己写出“有模有样”的脚本。3.4 不带值的多参数如果混着带值的参数怎么办再进阶一点点。如果脚本既要支持开关参数又要支持末尾跟一堆文件名按上面的套路扩展就行遇到不以-开头的参数就把它当成文件名收集起来。files() while [ $# -gt 0 ]; do case $1 in -v) verbose1 shift ;; -*) echo 未知选项: $1 exit 1 ;; *) files($1) shift ;; esac done echo 共 ${#files[]} 个文件 for f in ${files[]}; do echo 处理: $f done注意这里用了数组files()和${files[]}这是Shell处理“多个带空格路径”的标准做法。不用数组光靠$*和$很难安全地把一串参数传递下去。4. 实战串讲SFTP拉取、批量重命名、执行远程命令4.1 先看需求每天手动干的三件事很多人的Shell第一课是“写一个备份脚本”但真实场景比备份更杂。我拿一个最常见的运维场景举例每天上班干三件事——从SFTP服务器把昨天的数据文件拉下来把拉下来的文件按日期重命名归档在远程Linux服务器上执行一条命令确认服务状态。这三件事单独看都是基础操作串成一个脚本才能体现Shell真正的价值。4.2 从SFTP拉文件批处理模式与交互式SFTP默认是交互式的会要密码不方便塞进脚本。两个方向解决一是用sftp -b指定批处理文件把操作命令写进去sftp -b batch.txt user192.168.1.100batch.txtlcd /data/local cd /data/remote get report_20250101.csv byelcd是切换本地目录cd是切换远程目录get是下载bye是退出。把命令写进文件sftp就会自动执行。但有个现实问题还是会要密码。要让脚本全自动就得配合sshpass或者配置SSH密钥认证。sshpass -p 你的密码 sftp -b batch.txt user192.168.1.100更推荐的方式是提前配置SSH密钥这样连密码都不用写脚本里直接ssh userhost就行。密钥认证配好后sftp也自动沿用比明文密码安全得多。4.3 批量重命名日期归档文件拉下来后原始文件名可能没有日期要在归档时把日期加进去yesterday$(date -d yesterday %Y%m%d) for file in /data/local/report_*.csv; do base$(basename $file) mv $file /data/archive/${base%.csv}_${yesterday}.csv donedate -d yesterday %Y%m%d在Linux上可以拿到昨天的日期格式是20250101。basename去掉路径只留文件名${base%.csv}再去掉后缀最后拼成report_20250101_20250101.csv这种带日期的归档名。4.4 执行远程命令SSH加一条命令字符串SSH本身支持直接在后面跟命令不需要登录交互界面ssh user192.168.1.100 systemctl status nginx | head -3如果想顺便拿回返回结果直接赋值给变量status$(ssh user192.168.1.100 systemctl is-active nginx) echo nginx状态: $status这里有个细节如果你在循环里反复执行ssh每次都会重新建立连接很慢。更优做法是配置SSH Connection Multiplexing或者把多条远程命令一次性打包ssh user192.168.1.100 uptime df -h /data free -m 4.5 完整脚本示例与调试思路把上面串成一个完整脚本#!/bin/bash # 配置区 REMOTE_HOSTuser192.168.1.100 REMOTE_DIR/data/remote LOCAL_DIR/data/local ARCHIVE_DIR/data/archive YESTERDAY$(date -d yesterday %Y%m%d) mkdir -p $ARCHIVE_DIR # 第一步从SFTP拉取昨天数据 cd $LOCAL_DIR || exit 1 sftp $REMOTE_HOST EOF lcd $LOCAL_DIR cd $REMOTE_DIR get report_${YESTERDAY}.csv bye EOF # 第二步归档重命名 for file in report_${YESTERDAY}.csv; do [ -f $file ] || { echo 文件不存在: $file; exit 1; } mv $file ${ARCHIVE_DIR}/${file%.csv}_${YESTERDAY}.csv done # 第三步远程确认服务状态 ssh $REMOTE_HOST systemctl is-active nginx脚本不一定一次就能跑通调试最重要的是加bash -x执行它会一行行打印命令展开后的真实内容bash -x daily_pull.sh看到哪一步变量没展开、哪一步文件路径不对一目了然。这一步比任何调试技巧都实用。5. Shell实战中的高频坑空格、IFS、通配符与set -e5.1 变量赋值等号两边不能有空格这个坑新手必踩而且报错很奇怪name zhangsan # 错会被解析成执行name命令 namezhangsan # 对原因很简单Shell用空格分词name zhangsan被拆成了三个部分Shell以为你要执行一个叫name的命令。写脚本时养成习惯等号紧贴变量名和值。5.2 文件名带空格会拆词根本原因在于IFSShell默认的IFSInternal Field Separator是空格、制表符、换行。这意味着在for循环里遇到空格就会把内容拆开。# 目录下有两个文件: my photo.jpg、work.jpg for file in *.jpg; do echo $file done # 输出: my 和 photo.jpg 和 work.jpg这就是为什么我前面反复强调变量要加双引号。更稳妥的做法是用while read处理find . -name *.jpg -print0 | while IFS read -r -d file; do mv $file ${file%.jpg}.jpeg done-print0让文件名以\0分隔read -d 按\0读取不管文件名里有空格还是换行都安全。写文件处理类脚本时推荐养成这个习惯。5.3 通配符匹配不到文件时会保留“字面量”这可能是Shell脚本里最隐蔽的坑。当前目录没有.log文件时for file in *.log; do echo $file done # 输出: *.log没错通配符匹配不到任何文件时Shell不会报错而是把*.log原样传给循环。这是POSIX标准行为。要避免可以开启nullglob选项shopt -s nullglob for file in *.log; do echo $file done # 没有输出或者在循环里加存在性判断for file in *.log; do [ -f $file ] || continue echo $file done5.4 set -e的“好心办坏事”很多教程建议脚本开头加set -e意思是一旦任何命令返回非零退出码脚本立刻退出。想法是好的但实际项目里经常被它坑得怀疑人生。看这个场景set -e grep error app.log echo 检查完成如果app.log里没有“error”grep返回1脚本直接就挂了“检查完成”根本不会打印。可你明明只是想做一次搜索搜索无结果并不是错误。解决方法是把可能失败的命令放进条件判断里set -e if grep error app.log; then echo 发现error else echo 没有error fi echo 检查完成或者容忍单个命令失败grep error app.log || trueset -e不是不能用而是要知道它在某些场景下会“误伤”。我的建议是脚本前期可以开set -e兜底等逻辑稳定后把可能失败的敏感命令单独用条件判断包起来。5.5 管道里的while为什么丢变量写一个统计文件行数的脚本count0 cat data.txt | while read line; do count$((count 1)) done echo 总行数: $count # 输出0这个脚本会输出0原因在于管道右边的while是在子Shell里执行的子Shell里改了count父Shell看不到。各个Shell的实现细节不完全一样但bash默认就是这样。解决思路有两个。一是用进程替换count0 while read line; do count$((count 1)) done (cat data.txt) echo 总行数: $count二是用{把管道最后的命令包进来count0 cat data.txt | { while read line; do count$((count 1)) done echo 总行数: $count # 在这里面输出 }5.6 善用shellcheck这个“老中医”说句实在话上面这些坑靠人脑记是记不完的。真正有效的办法是装一个shellcheck# Ubuntu/Debian sudo apt install shellcheck # 检查脚本 shellcheck myscript.sh它会直接告诉你哪些地方有隐患、为什么有隐患、建议怎么改。比如变量没加引号、set -e在管道里的潜在问题、可移植性警告等等。我自己的习惯是写完一个超过20行的脚本先跑一遍shellcheck再上生产。它能帮你规避掉90%的低级事故。6. 面试官爱问的Shell题其实就这几类6.1 文本处理类统计日志关键词出现次数这个几乎是必考题。给定一个access.log统计每个IP出现的次数从高到低排序awk {print $1} access.log | sort | uniq -c | sort -rn拆解一下awk {print $1}取第一列默认IP在第一列sort排序让相同的IP聚在一起uniq -c统计重复次数sort -rn按数字倒序排。四段式是文本统计的标准流水线建议背下来。6.2 系统巡检类批量ping测试主机存活给一串IP找出哪些能通、哪些不通for ip in 192.168.1.{1..254}; do ping -c 1 -W 1 $ip /dev/null 21 if [ $? -eq 0 ]; then echo $ip 在线 fi done192.168.1.{1..254}是Shell的花括号展开会自动生成1到254的完整IP列表。-W 1让ping的超时时间变成1秒不然254个IP一个个等默认超时要等到天荒地老。6.3 语法理解类单双引号区别与$、$*区别这两组问题出现频率极高直接就考概念是否清楚单引号所见即所得$不解析双引号解析变量但不展开通配符反引号/$()执行命令并取输出。$和$*的区别一句话说$把每个参数当作独立个体$*把全部参数拼成一个整体。更直观的验证#!/bin/bash echo \$ 循环: for arg in $; do echo [$arg] done echo \$* 循环: for arg in $*; do echo [$arg] done传入三个参数运行时./test.sh a b c d$会输出三行$*只会输出一行因为$*把整个参数列表压成了一个字符串。6.4 脚本纠错类为什么[ $var x ]会报错写出这样的条件判断十有八九会跪name if [ $name zhangsan ]; then echo 匹配 fi变量为空时这行展开成[ zhangsan ][命令会认为第一个参数是直接语法错误。正确写法是给变量加双引号if [ $name zhangsan ]; then或者用更现代的[[if [[ $name zhangsan ]]; then[[是bash的关键字对空变量和特殊字符的处理更宽容也支持正则和、||。新写的脚本建议统一用[[。6.5 编一个小工具找到某个目录下最大的文件这个我面试时出过能同时考察find、管道和排序find /data -type f -printf %s %p\n | sort -rn | head -5find的-printf %s %p\n直接输出文件大小和路径sort -rn按数字倒序head -5取前五名。一行命令搞定优雅且高效。7. 别把Shell局限在Linux嵌入式、网络设备、UEFI里的Shell7.1 EFI ShellUEFI环境下的“急救命令行”很多人不知道UEFI固件里其实也内置了一个Shell环境叫EFI Shell。它可以在操作系统启动之前加载驱动、读写文件、运行诊断工具。热搜词里那个efi shell cannot find required map name我遇到过几次本质是Shell启动时找不到设备映射表常见原因是UEFI引导配置被重置或者启动项缺失。在EFI Shell里map命令查看设备映射fs0:切换文件系统ls、cd这些和Linux下几乎一样。一旦Linux启动不了又能看到磁盘时EFI Shell往往是抢救数据的最后一根稻草。它的命令思维和Linux Shell是相通的会Linux Shell的人上手很快。7.2 网络设备上的Shell华为交换机的命令行世界网络设备的命令行虽然不是标准的Shell但操作习惯非常类似。华为交换机上最常见的操作就是进入系统视图、配置VLAN、调试端口。热搜词里有一条“华为交换机 shell request failed on channel 0”这个报错我查过几次一般出现在SSH登录设备时多半是SSH会话协商失败、设备并发连接数满了或者客户端/服务端版本不兼容导致传输通道异常。排查思路其实和在Linux里排查SSH失败一模一样先确认端口通不通、服务有没有起来、再查认证方式。学Linux Shell时建立的“分层排查”思路放到网络设备、数据库、中间件里全部通用。7.3 嵌入式世界letter shell与STM32交互式命令行在STM32这类嵌入式平台上做开发调试手段很有限printf是主力但东西多了就不够用。letter shell是一个开源的嵌入式命令行组件作用是给单片机做一个交互式Shell你通过串口输入命令它执行后返回结果。它的设计思想和Linux Shell几乎一致命令注册、参数解析、自动补全、历史记录。C代码里每条命令对应一个函数之后在串口终端输入命令名就能触发。嵌入式开发里有了这层Shell调试效率能翻一番——不用改代码、重新烧录直接在运行态查看变量、调用功能。7.4 安卓调试里的Shelladb shell不只是postman安卓设备能搜到“shell 安卓 11 模拟 手机晃动”这类需求多是自动化测试要模拟传感器、触摸事件用的工具就是ADB。adb shell本身就是一个跑在安卓系统上的Shell环境里面也是Linux那套命令体系。# 模拟屏幕点击 adb shell input tap 500 1000 # 模拟滑动 adb shell input swipe 500 1000 500 200 # 查看当前Activity adb shell dumpsys activity activities | grep mResumedActivity写自动化测试脚本时用Shell把这些命令串起来比写完整Android工程轻量得多。这正好验证了那句话Shell的思维一旦建立换个环境只是换一套命令集的问题。我个人学Shell最大的体会是它不是一个需要“学完”的知识点而是一个越用越熟练的工具。语法花一两天就能过完真正值钱的是遇到问题时的拆解思路——把一个大任务拆成一个个小命令用管道和循环再组合起来。把这篇文章里的基础过了之后建议你拿自己的重复性工作练手写坏几个脚本踩过几次坑比看十篇教程都管用。最后再分享一个小技巧每次写完脚本都问问自己“如果文件名带空格会怎样”“如果目标文件不存在会怎样”“如果网络断了会怎样”多想三个极端情况你的脚本就会比大多数人的靠谱。