尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Shell脚本从基础到实战:变量、循环、判断与自动化运维

Shell脚本从基础到实战:变量、循环、判断与自动化运维 我做了十多年Linux运维被问得最多的问题就是Shell脚本怎么学要不要学说实话如果你只是把Linux当成一个带图形界面的普通系统来用那不学Shell也能过日子。但只要你碰过一次服务器、碰过一次自动化部署、碰过一次批量处理日志你就会明白Shell脚本这东西躲不开。今天这篇是这个系列的第一篇核心就一件事把Linux操作系统里的Shell脚本从基础到实战给你捋一遍。不是背命令而是理解它的套路。我会把变量、循环、判断、grep配合、后台执行这些最常用的点结合我实际踩过的坑一个一个拆开讲。适合Linux运维、后端开发、测试工程师、嵌入式方向的同学甚至做数据分析的只要你有重复性操作Shell脚本都能帮你省出大把时间。1. 为什么运维和开发都得会Shell脚本1.1 Shell脚本到底解决什么问题可以先把Shell脚本理解为“命令的菜谱”。你平时在终端里一条一条敲命令就好比做菜时每一步都临时想先放油、再放葱、再下锅。而Shell脚本就是把这些步骤写下来下次直接照着执行还能加判断、循环、参数让流程自动化。我印象最深的一次是有年做服务器巡检一百多台机器每台都要查磁盘、内存、CPU还要把结果汇总到一张表里。一台一台登录敲命令敲到怀疑人生。后来我花了一个晚上写了个Shell脚本第二天早上直接批量跑十分钟出结果。从那以后我就清楚Shell脚本不是锦上添花是吃饭的家伙。它最核心的价值是三点一是批量处理一条命令对一堆机器或一堆文件做同样的事二是定时执行配合crontab每天自动跑备份、清理日志三是逻辑控制加上if判断和for循环之后脚本能根据实际情况决定下一步干什么而不是无脑重复。1.2 哪些场景最吃香谁最该学不同岗位学Shell的侧重点不完全一样但底层逻辑是通的运维工程师日常巡检、日志分析、服务启停、备份恢复、批量部署这些几乎全是Shell脚本的活。后端开发环境初始化、构建发布、启动前检查、数据迁移脚本能把这些过程固化成产品的一部分。测试工程师自动化测试前的环境准备、批量造数据、测试结果收集Shell脚本很顺手。嵌入式工程师交叉编译环境的配置、烧录流程、日志抓取很多时候也离不开脚本辅助。数据分析师批量预处理数据文件、重命名、格式转换Shell脚本比手动操作高效得多。所以你不需要先背几百个Linux命令再学脚本。我的建议是反过来从一个具体需求出发用到什么查什么写多了就熟练了。Shell脚本不是在学会了之后才用的而是在用的时候学会的。2. 先把地基打好Shell脚本基础语法与执行方式2.1 脚本第一行为什么是#!/bin/bash所有Shell脚本第一行几乎都是#!/bin/bash这行叫shebang。它的意思是告诉操作系统这个脚本应该用哪个解释器来执行。有人会问写成#!/bin/sh行不行在不少老系统上/bin/sh被链接到了dash而dash的语法比bash严格比如不支持[[ ]]、不支持数组、不支持一些扩展正则写法。同一段脚本用bash跑没问题换成dash可能直接报错。所以我的习惯是所有自己写的脚本一律用#!/bin/bash省心。如果你不确定系统里bash在哪个位置可以执行which bash查看。绝大多数现代Linux发行版bash都在/bin/bash直接写这一行没问题。2.2 执行方式bash、./、source 到底有什么区别新手最大的困惑是同样是运行脚本为什么有人敲bash hello.sh有人敲./hello.sh还有人敲source hello.sh这三者的区别我做了张表执行方式是否需要可执行权限是否新开子Shell典型使用场景bash script.sh不需要是临时调试、查看效果./script.sh需要是正式执行脚本source script.sh不需要否加载配置文件、让变量留在当前环境bash script.sh相当于明确告诉系统用bash解释器去解析这个文件不要求文件有x权限所以临时调试非常方便。./script.sh则要求文件有可执行权限脚本自己通过shebang决定用哪个解释器。这两种方式都会新开一个子Shell脚本里定义的变量跑完就消失不会污染当前终端环境。source就不一样了它是在当前Shell里直接执行脚本内容所以脚本里设置的变量、切换的目录都会保留在当前终端里。这个特性在加载配置文件时特别有用比如改完/etc/profile之后source /etc/profile可以立刻生效不用重新登录。2.3 新手最容易栽的三个坑第一个坑是脚本在Windows上编辑过传到Linux后执行报错。症状很诡异明明命令看着没问题但提示bad interpreter: /bin/bash^M或者$\r: command not found。原因就是Windows的换行符是CRLFLinux只认LF。解决办法是装一个dos2unix工具执行dos2unix script.sh没工具的话用vim打开执行:set ffunix再保存。第二个坑是执行时报Permission denied。这个最简单给脚本加上执行权限就行chmod x script.sh。第三个坑是输入./script.sh时报command not found但你明明看到文件就在当前目录。原因可能有两个一是你少写了./直接在终端敲了script.sh而当前目录不在PATH变量里Shell自然找不到二是脚本没有shebang行系统不知道用哪个解释器去执行。第一个原因最常见解决办法就是老老实实敲./script.sh。3. 变量和输入Shell脚本的“内存”与“入口”3.1 变量赋值时等号两边不能有空格Shell脚本里给变量赋值格式是变量名值注意等号两边绝对不能有空格。很多人初学时不理解为什么其实原因和Shell的解析机制有关在Shell的命令行里空格是用来分隔命令、参数、选项的。如果你写name testShell会把name当成一个命令来执行然后给它传了两个参数和test结果当然报错。正确写法是namezhangsan age28变量的值里如果有空格必须用双引号包起来比如file_namemy project.tar.gz。引用变量时可以写$name也可以写${name}。后面这种写法更严谨尤其在变量名后面还要拼接其他字符时比如prefix_${name}_suffix这样Shell才能准确识别变量名的边界。3.2 双引号、单引号和反引号别搞混变量、命令替换、引号这三件事是新手最容易绕晕的地方。我直接用例子说明nameworld echo hello $name # 输出: hello world echo hello $name # 输出: hello $name echo today is $(date %F) # 输出: today is 2025-06-14双引号里的变量会被展开成具体值单引号里的内容则原样输出$name不会变成world。所以如果你想让用户看到的提示信息里包含特殊符号或者不需要变量展开就用单引号如果希望变量、命令结果动态插入文本中就用双引号。还有一个老写法是反引号date它和$(date)一样都是命令替换但反引号在嵌套时容易出现转义问题所以现在我都推荐$()写法它更清晰也支持嵌套。3.3 位置参数与read交互输入脚本除了写死逻辑还得能接收外部输入。最常见的是位置参数就是执行脚本时跟在脚本名后面的那些值#!/bin/bash echo 脚本名: $0 echo 第一个参数: $1 echo 第二个参数: $2 echo 参数个数: $# echo 所有参数: $比如执行./test.sh alice 30输出里$1就是alice$2就是30。写工具脚本时位置参数特别有用比如./deploy.sh production可以指定环境。除了位置参数read命令也可以用交互方式获取输入。read -p 请输入用户名: username会先打印提示文字然后等你输入内容赋给变量。如果输入的是密码建议加-s参数隐藏回显避免密码暴露在屏幕上。3.4 实战写一个交互式创建用户的脚本结合前面说的变量、输入和判断我写一个实用的新建用户脚本#!/bin/bash read -p 请输入要创建的用户名: username read -s -p 请输入初始密码: password echo if id $username /dev/null; then echo 用户 $username 已存在 else useradd $username echo $password | passwd --stdin $username echo 用户 $username 创建成功 fi这个脚本用到几个小技巧id $username用来判断用户是否存在/dev/null把正常输出和错误输出都丢弃我们只要它的退出码passwd --stdin表示从标准输入读取密码适合脚本环境。不过要注意passwd --stdin在部分发行版默认不支持比如某些Ubuntu版本。兼容性更好的写法是echo $username:$password | chpasswd。我自己会把这两种方式都写上注释上线前根据实际系统选择。4. 循环和判断控制脚本流程的核心4.1 for循环的几种写法直接对比Shell脚本的for循环有很多种写法不同场景用不同写法能省不少事。我挑四种最常用的# 写法一花括号展开 for i in {1..5}; do echo 第 $i 次 done # 写法二seq命令适合变量边界 end100 for i in $(seq 1 $end); do echo $i done # 写法三遍历当前目录下的文件 for file in /etc/*.conf; do echo 配置文件: $file done # 写法四C语言风格 for ((i0; i10; i)); do echo $i done花括号展开{1..5}最简单直观但不支持变量边界比如{1..$end}是不会展开的。需要动态范围时用$(seq 1 $end)更合适。遍历文件时直接在循环列表里写通配符Shell会在执行前自动展开成匹配的文件列表。C风格for ((...))最灵活什么都能干但语法相对复杂一点。4.2 if判断和test命令的语法细节if判断的基础结构是if [ 条件 ]; then # 条件成立时执行 elif [ 另一个条件 ]; then # 上一个不成立但这个成立时执行 else # 都不成立时执行 fi这里第一个要记住的坑是[不是语法符号它其实是一个命令叫test。正因为如此条件两边必须留空格。写[$name a]会报错因为[这个命令找不到它的参数边界。正确写法是[ $name a ]。第二个要记住的坑是变量一定要加双引号。比如[ $name a ]如果name是空的不加双引号会变成[ a ]导致unary operator expected错误。加了双引号后空变量会被展开成空字符串条件表达式不会报语法错。常见的判断条件有这些-f file文件是否存在且是普通文件-d dir目录是否存在-z string字符串是否为空-n string字符串是否非空-eq、-ne、-gt、-lt数字比较、!字符串比较另外bash还支持[[ ]]扩展写法。它比[ ]更强大支持正则匹配~而且内部变量不需要外层的双引号也不容易踩空格坑。我现在的脚本基本都是用[[ ]]只有在需要严格兼容POSIX sh时才会退回[ ]。4.3 实战批量重命名文件循环和判断结合起来能做很多实际的事。我举个例子把当前目录下所有.txt文件重命名为.log后缀#!/bin/bash for file in *.txt; do # 防止没有匹配到任何文件时file 等于字面量 *.txt [ -e $file ] || continue new_name${file%.txt}.log mv $file $new_name echo 重命名: $file - $new_name done${file%.txt}的语法是去掉变量值末尾的.txt然后拼上.log实现后缀替换。判断[ -e $file ] || continue是一个很重要的安全习惯当目录下没有.txt文件时*.txt不会被展开而是保持字面量如果你直接去mv就会出现mv: cannot stat *.txt的报错。加了这个判断就能跳过不存在的情况。5. grep在Shell脚本里的高级玩法5.1 grep不是简单过滤工具很多人在终端里用grep习惯就是grep 关键字 文件把匹配的行打出来。但在脚本里grep的价值远不止这个。我常用到的参数是这几个-E使用扩展正则表达式可以写a|b这类更丰富的模式-c只输出匹配的行数-q静默模式不输出匹配内容只返回退出码-i忽略大小写-v反向匹配输出不包含关键词的行-n显示匹配行的行号参数不同脚本里用法的侧重点完全不同。比如你要知道日志里有几行报错用grep -c你只需要判断有没有报错用grep -q你要把不包含调试信息的行再交给下一个命令处理用grep -v。5.2 利用退出码做判断grep在找到匹配时返回退出码0没有匹配时返回退出码1。这个特性在脚本里非常适合做逻辑判断。比方说你在启动服务之前要先检查配置里有没有开启某个选项#!/bin/bash if grep -q ^debug_modetrue /etc/myapp/config.ini; then echo 当前开启了调试模式 else echo 未开启调试模式 fi有人可能会写if [ $(grep -c error log) -gt 0 ]这样也能实现但效率低因为grep要先数完所有行。用grep -q则只要找到一个匹配就立即返回不用等全部读完日志特别大的时候性能差异还是很明显的。5.3 实战统计日志中的错误我经常要统计Nginx或应用日志里某个错误码出现的次数超过阈值就得报警。下面这个脚本就是干这个事的#!/bin/bash log_file/var/log/nginx/access.log error_count$(grep -c 500 $log_file) echo 500错误次数: $error_count if [ $error_count -gt 100 ]; then echo 警告500错误数量超过阈值请立即检查 fi注意 500 都带空格这是为了避免匹配到类似1500这种包含数字500但不表示状态码的行。日志格式如果不够规范很容易匹配出误报写grep模式的时候一定要多想一层。如果你要同时看多个日志文件可以配合for循环for log in /var/log/nginx/*.log; do count$(grep -c 500 $log) echo $log: $count 次500错误 done把grep的过滤能力、退出码能力和循环结合起来能做出非常灵活的巡检脚本。6. 后台执行与交互输入的实操技巧6.1 、nohup、setsid 有什么区别写脚本久了你会发现不是所有任务都适合在前台跑。比如一个数据库备份要跑半小时你不可能一直盯着终端。这时候就需要后台执行。最简单的做法是在命令末尾加比如./backup.sh 。这个符号会把进程放到后台终端可以继续做别的事。但这里有个坑如果你直接关掉终端后台进程很可能收到挂断信号SIGHUP然后退出。所以如果任务要持续很久还需要用nohup忽略挂断信号nohup ./backup.sh backup.log 21 这里面 backup.log是把标准输出重定向到文件21是把错误输出也重定向到同一个地方。这两个都要写否则后台进程在终端关闭后可能因为没有输出位置而异常。还有个小工具叫setsid它能让进程彻底脱离当前会话进入新的会话更彻底地“脱离掌控”。实际工作中我用得最多的组合就是nohup 简单直接。6.2 交互式输入密码的几种思路后台执行加交互输入这是很多人的痛点。比如脚本要远程SSH到另一台服务器执行命令对方会提示输入密码。可你的脚本已经在后台跑了没人坐在屏幕前面敲键盘怎么办思路一用expect。expect是一个专门处理交互式输入的工具可以模拟键盘输入。基本用法是这样的#!/bin/bash expect EOF spawn ssh user192.168.1.10 uptime expect password: send 你的密码\r expect eof EOFspawn启动命令expect等待程序输出指定的提示词send发送我们需要输入的内容。这套机制能处理绝大多数交互场景。思路二用管道直接把密码传给命令。像sudo -S就支持从标准输入读取密码echo 你的密码 | sudo -S apt-get update但这种方式对部分命令不适用而且密码会出现在命令行历史里有安全风险。关于安全问题我要多说一句生产环境最好不要在脚本里硬编码密码。我把密码放在密钥认证或凭据管理工具里或者通过环境变量注入。SSH任务优先配置公钥认证这样从根本上不需要交互输入密码。6.3 组合实践数据库备份脚本怎么跑起来我把后台执行、重定向、定时任务串起来给你看一个完整的数据库备份脚本#!/bin/bash backup_dir/data/backup/mysql date_str$(date %Y%m%d_%H%M%S) db_namemydb db_userbackup_user db_password${DB_PASS} mkdir -p $backup_dir mysqldump -u$db_user -p$db_password $db_name $backup_dir/${db_name}_${date_str}.sql # 只保留最近7天的备份 find $backup_dir -name *.sql -mtime 7 -delete echo 备份完成: ${db_name}_${date_str}.sql这里我把数据库密码放在环境变量DB_PASS里脚本不直接写明文密码。执行时这样跑export DB_PASS你的密码 nohup /data/scripts/backup_mysql.sh /data/logs/backup_mysql.log 21 然后用crontab -e加一条定时任务比如每天凌晨2点执行0 2 * * * /data/scripts/backup_mysql.sh /data/logs/backup_mysql.log 21这样备份任务就完全自动化了不用人盯。这就是Shell脚本在真实运维环境里最常见的落地方式。7. 我在实际工作中积累的避坑清单7.1 常见报错速查表写脚本踩坑不可怕可怕的是报错看不懂。我把这些年最常遇到的报错整理成一个表你直接对照查报错信息原因解决办法command not found命令拼写错误、路径不在PATH中、脚本没有./检查命令拼写使用绝对路径执行时写./script.shPermission denied脚本没有执行权限chmod x script.shbad interpreter: /bin/bash^MWindows换行符CRLF导致dos2unix script.sh或vim中:set ffunixsyntax error near unexpected token括号、if/then/fi不匹配打开脚本检查关键字和括号[: unary operator expected[ ]条件中变量为空或少了双引号写[ $var value ]No such file or directory但文件存在脚本缺少shebang行添加#!/bin/bash7.2 写脚本的四个好习惯第一脚本开头加set -euo pipefail。这一句是很多资深运维的标配。-e表示遇到错误立即退出-u表示使用未定义的变量时报错-o pipefail表示管道中任何一个命令失败整个管道就返回失败。新手可能觉得太严格但养成这个习惯之后很多隐性问题会在早期暴露。第二写完之后先做语法检查。bash -n script.sh只检查语法不真正执行特别适合上线前快速排查。如果脚本逻辑复杂再用bash -x script.sh调试它会打印每一步执行的命令和结果定位问题一目了然。第三变量尽量加双引号。不管是echo $var还是[ $var x ]双引号能避免因为空格、通配符、空值引起的一堆诡异问题。第四复杂脚本要学会用函数。把重复的逻辑封装成函数脚本的可读性和可维护性会提升好几个等级。我见过有些人写500行的Shell脚本连个注释都没有过一个月自己都看不懂那种痛苦的经历我不希望你也体会。最后分享一个我个人的小习惯我在写脚本时会在文件顶部写清楚脚本用途、作者、创建日期。很多人觉得这多余但真遇到生产事故需要快速排查时一段清晰的注释能帮你在几分钟内回忆起整个脚本的目的而不是对着几百行代码从头猜。Shell脚本这东西写得清楚不仅机器能跑人也看得懂几个月后你自己回来改才不会骂自己。
返回列表