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