Linux Shell 编程实战:让自动化替你“搬砖”
引言
如果你是一个运维工程师,每天可能要重复执行几十次同样的命令——登录服务器、查日志、清缓存、重启服务……这些重复劳动不仅枯燥,而且容易出错。
如果你是一个开发人员,每次部署都要手动敲一串命令,环境稍微有点差异就报错,排错半天。
如果你是一个刚接触 Linux 的新手,面对黑乎乎的终端,不知道从哪里下手。
其实,这些问题都有一个共同的解决方案——Shell 脚本。
Shell 脚本本质上是 Linux 命令的“批处理”文件。把你平时手动敲的那些命令按顺序写到一个文件里,加上一点逻辑判断和循环,它就变成了一个自动化工具。一次编写,永久复用。可以说Shell脚本是Linux操作系统的精华,不懂Shell那就没掌握Linux操作系统运维的精髓。
今天这篇文章,我们就来系统学习 Linux Shell 编程:从基础语法到流程控制,从函数封装到调试技巧,再到几个拿来就能用的实战案例。读完这篇,你就能写出自己的第一个生产级脚本。
一、Shell 是什么?为什么值得学?
Shell 是用户与 Linux 内核之间的桥梁。你在终端里敲的每一个命令,都是 Shell 在帮你解释并调用内核执行。
Shell 不仅仅是一个命令解释器,它本身也是一门功能相当强大的编程语言,易编写、易调试、灵活性强。
Shell 脚本的核心价值在于自动化:
- 自动化处理:把重复性操作写成脚本,一键执行
- 批量管理:通过循环结构同时管理数百台设备
- 资源高效:低内存占用,适合服务器环境
- 灵活定制:可组合命令构建复杂逻辑
常见的 Shell 类型包括Bash(最主流,Linux 默认)、Zsh、Ksh 等。Bash 凭借强大的脚本功能和广泛的社区支持,成为生产环境的首选。
💡一个真实的效率对比:手动操作,每小时仅能处理 5-10 台服务器;而通过 Shell 脚本,单台服务器操作时间可压缩至秒级,配合循环结构可同时管理数百台设备,效率提升数十倍。
二、第一个 Shell 脚本:Hello World
在开始之前,我们先写一个最简单的脚本,感受一下流程。
第一步:创建脚本文件
vimhello.sh第二步:写入内容
#!/bin/bash# 这是我的第一个 Shell 脚本echo"Hello, World!"#!/bin/bash是shebang,告诉系统用哪个解释器来执行这个脚本#开头的行是注释,不会被执行echo是输出命令
第三步:赋予执行权限并运行
chmod+x hello.sh ./hello.sh输出:
Hello, World!💡小贴士:脚本也可以用
bash hello.sh或sh hello.sh直接运行,无需执行权限。
三、变量:脚本的“记忆单元”
变量是编程语言的基石。Shell 中的变量无需声明类型,直接赋值即可使用。
3.1 变量定义与引用
# 定义变量(等号两侧不能有空格!)name="Linux Shell"# 字符串count=100# 数字(Shell 中默认也是字符串)pi=3.14# 浮点数# 引用变量(加 $ 符号)echo$nameecho${name}# 推荐用花括号,避免歧义关键规则:
- 等号两侧不能有空格
- 变量名不能以数字开头
- 可以使用下划线,不能使用破折号或标点符号
- 变量引用需加
$符号 - 推荐使用
${var}而非$var,避免歧义
3.2 单引号 vs 双引号
字符串可以用单引号或双引号,区别很大:
name="Alice"echo'Hello, $name'# 输出:Hello, $name(原样输出,变量不解析)echo"Hello,$name"# 输出:Hello, Alice(变量被解析)规则:
- 单引号:原样输出,里面的变量和转义字符都不生效
- 双引号:变量会被解析,转义字符生效
3.3 特殊变量(非常重要!)
Shell 提供了一些内置的特殊变量:
| 变量 | 含义 |
|---|---|
$0 | 当前脚本的名称 |
$1~$9 | 脚本的第 1~9 个参数 |
$# | 传递的参数个数 |
$* | 所有参数(作为一个整体字符串) |
$@ | 所有参数(每个作为独立字符串) |
$? | 上一条命令的退出状态码(0=成功,非0=失败) |
$$ | 当前 Shell 的进程 ID |
示例:
#!/bin/bashecho"脚本名:$0"echo"第一个参数:$1"echo"参数个数:$#"echo"所有参数:$@"运行:
./test.sh hello world# 输出:# 脚本名: ./test.sh# 第一个参数: hello# 参数个数: 2# 所有参数: hello world3.4 数组
Shell 支持数组,下标从 0 开始:
# 定义数组fruits=("apple""banana""orange")# 访问单个元素echo${fruits[0]}# 输出:apple# 访问所有元素echo${fruits[@]}# 输出:apple banana orange# 获取数组长度echo${#fruits[@]}# 输出:3# 遍历数组forfruitin"${fruits[@]}";doecho"I like$fruit"done四、流程控制:让脚本有“脑子”
有了变量只能存数据,有了流程控制,脚本才能做判断、走分支、循环执行——这才是编程语言的灵魂。
4.1 if 条件判断
if 语句支持数值、字符串、文件状态等多种判断。
基本语法:
if[条件];then# 条件成立时执行elif[另一个条件];then# 另一个条件成立时执行else# 所有条件都不成立时执行fi常见条件测试:
| 测试类型 | 运算符 | 含义 |
|---|---|---|
| 数值比较 | -eq | 等于 |
-ne | 不等于 | |
-gt | 大于 | |
-lt | 小于 | |
-ge | 大于等于 | |
-le | 小于等于 | |
| 文件测试 | -f | 存在且为普通文件 |
-d | 存在且为目录 | |
-e | 文件存在 | |
-x | 文件可执行 | |
| 字符串 | = | 相等 |
!= | 不等 | |
-z | 为空 |
实战示例:
#!/bin/bash# 数值比较a=10if[$a-gt5];thenecho"$a大于 5"fi# 文件存在性检查if[-f"/etc/passwd"];thenecho"passwd 文件存在"elseecho"passwd 文件不存在"fi# 字符串匹配(推荐使用 [[ ]])if[["$name"==*"error"*]];thenecho"包含错误信息"fi💡推荐:使用
[[ ]]替代[ ],前者支持更丰富的模式匹配(如通配符*)。
4.2 case 多分支选择
当有多个分支时,case比多个if-elif更清晰:
#!/bin/bashcase$1instart)echo"启动服务...";;stop)echo"停止服务...";;restart)echo"重启服务...";;*)echo"用法:$0{start|stop|restart}";;esac4.3 for 循环
for 循环用于遍历列表或范围:
# 遍历列表forfileinlog1.txt log2.txt log3.txt;doecho"处理文件:$file"done# 遍历数组files=("log1.txt""log2.txt""log3.txt")forfilein"${files[@]}";doecho"处理文件:$file"done# 数字序列循环foriin{1..5};doecho"第$i次循环"done# C 语言风格for((i=1;i<=5;i++));doecho"第$i次循环"done实战:批量重命名文件:
# 将所有 .txt 文件重命名为 .backupforfilein*.txt;domv"$file""${file%.txt}.backup"done4.4 while 循环
while 循环适合不确定次数的场景,条件为真时持续执行:
#!/bin/bashcount=5while[$count-gt0];doecho"剩余次数:$count"count=$((count-1))done4.5 until 循环
until 和 while 相反,条件为假时执行,直到条件变为真:
count=1until[$count-gt5];doecho"计数:$count"count=$((count+1))done4.6 循环控制:break 和 continue
# break:跳出整个循环foriin{1..10};doif[$i-eq5];thenbreakfiecho$idone# 输出:1 2 3 4# continue:跳过本次循环foriin{1..5};doif[$i-eq3];thencontinuefiecho$idone# 输出:1 2 4 5五、函数:让代码可复用
函数是模块化编程的核心。把重复的逻辑封装成函数,脚本更清晰、更易维护。
5.1 定义与调用
# 定义函数(两种写法等价)functiongreet(){echo"Hello,$1!"}# 或者greet(){echo"Hello,$1!"}# 调用函数(不加括号)greet"World"# 输出:Hello, World!5.2 函数参数与返回值
函数通过$1、$2等位置变量接收参数。返回值用return指定(0-255),$?获取:
#!/bin/bash# 带参数的函数backup_file(){localsrc=$1localdest=$2cp"$src""$dest"&&echo"备份成功:$dest"||echo"备份失败"}# 带返回值的函数check_number(){if[$1-gt0];thenreturn0# 表示成功/真elsereturn1# 表示失败/假fi}# 调用backup_file"/var/log/syslog""/backup/syslog.log"check_number5if[$?-eq0];thenecho"是正数"fi5.3 局部变量
用local关键字声明的变量只在函数内部有效,不会污染全局:
#!/bin/bashmy_func(){localvar="我是局部变量"echo"函数内:$var"}my_funcecho"函数外:$var"# 输出为空,因为 var 是局部变量六、调试技巧:让 Bug 无处遁形
Shell 脚本的调试是新手最容易卡住的地方。下面介绍几种实用的调试方法。
6.1 echo 大法(最基础)
在关键位置打印变量值,是最简单直接的调试方式:
#!/bin/bashname="Alice"echo"DEBUG: name=$name"# 打印变量值6.2 set -x:跟踪执行(最推荐)
set -x会让 Shell 在执行每条命令前,把命令打印出来:
#!/bin/bashset-x# 开启调试模式name="Alice"echo"Hello,$name"set+x# 关闭调试模式输出会显示每一条命令及其展开后的结果,非常适合定位逻辑错误。
6.3 set -e:遇错即停
默认情况下,脚本中某条命令失败后会继续执行,可能导致更严重的连锁问题。set -e让脚本在遇到任何错误时立即退出:
#!/bin/bashset-e# 任何命令失败就退出rm/nonexistent/file# 这行会报错,脚本立即退出echo"这行不会执行"6.4 组合技:set -euxo pipefail
这是生产环境脚本的黄金组合:
#!/bin/bashset-euxopipefail各选项含义:
-e:命令失败立即退出-u:使用未定义变量时报错-x:打印执行的每条命令-o pipefail:管道中任何命令失败都算失败
6.5 trap:捕获错误与信号
trap可以在脚本被中断或出错时执行清理操作:
#!/bin/bashcleanup(){echo"脚本被终止,执行清理..."rm-f/tmp/temp_file}# 捕获 Ctrl+C 和脚本错误trapcleanup SIGINT SIGTERM ERR# 正常逻辑...6.6 语法检查:bash -n
不执行脚本,仅检查语法错误:
bash-nscript.sh七、实战案例:拿来就能用
案例一:批量检查服务器存活
#!/bin/bash# check_servers.sh - 批量 Ping 检测服务器存活servers=("192.168.1.1""192.168.1.2""192.168.1.3")foripin"${servers[@]}";doifping-c1-W1$ip>/dev/null2>&1;thenecho"✅$ip存活"elseecho"❌$ip不可达"fidone案例二:Nginx 服务管理脚本
#!/bin/bash# nginx_ctl.sh - Nginx 服务管理NGINX_HOME="/usr/local/nginx"NGINX_PID="$NGINX_HOME/logs/nginx.pid"start(){if[-f$NGINX_PID]&&kill-0$(cat$NGINX_PID)2>/dev/null;thenecho"Nginx 已经在运行"else$NGINX_HOME/sbin/nginxecho"Nginx 启动成功"fi}stop(){if[-f$NGINX_PID];thenkill-QUIT$(cat$NGINX_PID)echo"Nginx 已停止"elseecho"Nginx 未运行"fi}reload(){if[-f$NGINX_PID];thenkill-HUP$(cat$NGINX_PID)echo"Nginx 配置已重载"elseecho"Nginx 未运行"fi}case$1instart)start;;stop)stop;;reload)reload;;restart)stop&&sleep1&&start;;*)echo"用法:$0{start|stop|restart|reload}";;esac案例三:磁盘使用率监控告警
#!/bin/bash# disk_monitor.sh - 磁盘使用率监控THRESHOLD=80EMAIL="admin@example.com"df-h|grep-vE'^Filesystem|tmpfs|devtmpfs'|whilereadline;dousage=$(echo$line|awk'{print $5}'|sed's/%//')mount=$(echo$line|awk'{print $6}')if[$usage-gt$THRESHOLD];thenecho"警告:$mount使用率已达$usage%"|mail-s"磁盘告警"$EMAILfidone八、生产环境最佳实践
8.1 脚本头部模板
#!/bin/bash#-------------------------------------------------------------------# 脚本名称: xxx.sh# 功能描述: xxx# 作者: xxx# 版本: 1.0# 创建日期: $(date +%Y-%m-%d)#-------------------------------------------------------------------set-euopipefail8.2 使用绝对路径
在脚本中尽量使用绝对路径,避免因 PATH 环境变量不同导致命令找不到。
8.3 变量加引号
变量引用时始终加双引号,避免空格或特殊字符导致意外解析:
# ❌ 不推荐file=$1rm$file# ✅ 推荐file="$1"rm"$file"8.4 检查命令是否存在
if!command-vnginx&>/dev/null;thenecho"nginx 命令不存在,请先安装"exit1fi8.5 函数封装,避免重复
重复的逻辑封装成函数,提高可维护性。
8.6 日志记录
log(){echo"[$(date'+%Y-%m-%d %H:%M:%S')]$*">>/var/log/my_script.log}log"脚本开始执行"总结
Shell 编程是 Linux 运维和自动化领域的核心技能。从基础语法到流程控制,从函数封装到调试技巧,掌握这些知识,你就能把日常的重复劳动交给脚本去完成。
记住三句话:
- Shell 是人与系统交互的桥梁:掌握它,你就掌握了 Linux 的“遥控器”。
- 脚本的本质是命令的自动化组合:把重复的事写成脚本,让机器替你“搬砖”。
- 先能跑,再优化:不要追求一开始就写出完美的脚本,先写出能用的,再逐步完善。
Shell 脚本的威力在于——一次编写,永久复用;一处编写,处处运行。希望这篇文章能帮你迈出 Shell 编程的第一步,写出属于你自己的自动化工具!
本篇文章主要是让读者对shell编程有一个大致的认识和了解,后续将带您更加深入的学习shell编程的相关知识,如果你有任何问题或经验分享,欢迎在评论区留言讨论!