Shell 脚本编程
Shell 脚本是 Linux 自动化的核心技能。本章将介绍如何编写 Bash 脚本,从基础语法到高级技巧。
什么是 Shell 脚本?
Shell 脚本是一种解释型脚本语言,由 Shell 逐行解释执行。它可以:
- 自动化任务:批量处理文件、定时备份、系统维护
- 组合命令:将多个命令组合成复杂的工作流
- 系统管理:监控服务、日志分析、自动部署
- 简化操作:将复杂操作封装成简单命令
解释:Shell 脚本不需要编译,修改后直接运行,非常适合系统管理和快速开发。
脚本基本结构
Shebang
脚本第一行称为 Shebang,指定脚本使用的解释器:
#!/bin/bash # 使用 Bash 解释
#!/bin/sh # 使用 POSIX Shell
#!/usr/bin/env bash # 使用 env 查找 bash(更可移植)
解释:#! 是特殊标记,后面跟解释器路径。系统会根据这一行选择正确的解释器执行脚本。
创建和运行脚本
# 创建脚本文件
vim hello.sh
写入以下内容:
#!/bin/bash
# 这是一个简单的示例脚本
echo "Hello, World!"
echo "当前用户: $USER"
echo "当前目录: $(pwd)"
运行脚本:
# 方式 1:添加执行权限后直接运行
chmod +x hello.sh
./hello.sh
# 方式 2:使用解释器运行
bash hello.sh
# 方式 3:在当前 Shell 中运行(会影响当前环境)
source hello.sh
# 或
. hello.sh
解释:
chmod +x赋予执行权限,让脚本可以像程序一样运行source命令在当前 Shell 执行脚本,脚本中的变量和函数会影响当前环境- 推荐使用
./script.sh或bash script.sh方式运行
注释
#!/bin/bash
# 这是单行注释
: '
这是多行注释
使用冒号和单引号
可以跨越多行
'
echo "Hello" # 行尾注释
# 多行注释的另一种方式
: <<'COMMENT'
多行注释
使用 here document 语法
COMMENT
解释:注释帮助理解代码逻辑。良好的注释习惯是脚本可维护性的关键。
变量
变量定义和使用
#!/bin/bash
# 变量定义(等号两边不能有空格)
name="张三"
age=25
score=95.5
# 变量使用(使用 $ 符号)
echo "姓名: $name"
echo "年龄: $age"
echo "成绩: $score"
# 推荐使用 ${} 明确变量边界
echo "姓名是${name},今年${age}岁"
# 只读变量
readonly PI=3.14159
# PI=3.14 # 会报错:不能修改只读变量
# 删除变量
unset score
echo "$score" # 输出空行
解释:
- Bash 变量默认是字符串类型,数值运算需要特殊处理
- 变量名区分大小写,建议使用小写字母命名自定义变量
${}语法可以避免变量名混淆,如${name}_file
变量命名规则
# 合法变量名
name="合法"
_name="合法"
NAME="合法"
name1="合法"
# 非法变量名
# 1name="非法" # 不能以数字开头
# my-name="非法" # 不能包含连字符
# my name="非法" # 不能包含空格
特殊变量
#!/bin/bash
echo "脚本名称: $0" # 脚本文件名
echo "第一个参数: $1" # 第一个参数
echo "第二个参数: $2" # 第二个参数
echo "参数个数: $#" # 参数个数
echo "所有参数: $@" # 所有参数(作为独立字符串)
echo "所有参数: $*" # 所有参数(作为单个字符串)
echo "上一命令退出状态: $?" # 上一个命令的退出状态
echo "当前进程 PID: $$" # 当前进程 ID
echo "后台进程 PID: $!" # 最近后台进程 ID
# 运行示例:./script.sh arg1 arg2 arg3
$@ vs $* 的区别:
#!/bin/bash
# 演示 $@ 和 $* 的区别
echo "使用 \$@:"
for arg in "$@"; do
echo " 参数: $arg"
done
echo "使用 \$*:"
for arg in "$*"; do
echo " 参数: $arg" # 所有参数作为一个整体
done
# 运行:./test.sh "hello world" "foo bar"
# $@ 输出两个参数:hello world 和 foo bar
# $* 输出一个参数:hello world foo bar
数据类型
字符串
#!/bin/bash
# 字符串定义
str1='单引号字符串' # 单引号:原样输出,不解析变量和转义
str2="双引号字符串" # 双引号:解析变量和转义字符
str3="多行
字符串示例"
# 变量解析
name="张三"
echo '姓名: $name' # 输出:姓名: $name(原样输出)
echo "姓名: $name" # 输出:姓名: 张三(解析变量)
# 字符串拼接
greeting="你好, "$name"!"
greeting2="你好, ${name}!"
# 字符串长度
str="Hello World"
echo "长度: ${#str}" # 输出:11
# 子字符串
echo "从位置 0 开始 5 个字符: ${str:0:5}" # Hello
echo "从位置 6 开始: ${str:6}" # World
echo "从右边开始截取: ${str:0-5}" # World
# 查找子字符串
string="Hello World"
if [[ $string == *"World"* ]]; then
echo "包含 World"
fi
# 替换子字符串
echo "${string/World/Linux}" # 替换第一个:Hello Linux
echo "${string//o/O}" # 替换所有:HellO WOrld
# 删除子字符串
echo "${string#He}" # 删除开头最短匹配:llo World
echo "${string##*o}" # 删除开头最长匹配:rld
echo "${string%World}" # 删除结尾最短匹配:Hello
echo "${string%%o*}" # 删除结尾最长匹配:Hell
解释:
- 单引号内的内容完全原样输出,适合包含特殊字符的字符串
- 双引号支持变量展开和转义,是最常用的字符串形式
${#var}获取字符串长度${var:start:length}截取子字符串
数组
#!/bin/bash
# 数组定义
arr1=(1 2 3 4 5)
arr2=("apple" "banana" "cherry")
arr3=([0]=a [2]=b [4]=c) # 索引可以不连续
# 数组赋值
arr[0]="first"
arr[1]="second"
arr[5]="fifth" # 索引可以不连续
# 访问数组元素
echo "第一个元素: ${arr[0]}"
echo "第二个元素: ${arr[1]}"
# 所有元素
echo "所有元素: ${arr[@]}"
echo "所有元素: ${arr[*]}"
# 数组长度
echo "元素个数: ${#arr[@]}"
echo "元素个数: ${#arr[*]}"
# 数组索引
echo "所有索引: ${!arr[@]}"
# 遍历数组
echo "遍历数组:"
for item in "${arr[@]}"; do
echo " $item"
done
# 带索引遍历
echo "带索引遍历:"
for i in "${!arr[@]}"; do
echo " 索引 $i: ${arr[$i]}"
done
# 添加元素
arr+=("new_element")
arr+=(x y z) # 添加多个元素
# 删除元素
unset arr[1] # 删除指定索引的元素
# 数组切片
numbers=(0 1 2 3 4 5 6 7 8 9)
echo "切片: ${numbers[@]:2:3}" # 2 3 4(从索引 2 开始取 3 个)
解释:
- Bash 数组是零索引的,支持稀疏数组(索引不连续)
"${arr[@]}"将数组展开为独立的元素,推荐使用"${arr[*]}"将所有元素合并为一个字符串
关联数组(字典)
#!/bin/bash
# 声明关联数组
declare -A user
# 赋值
user[name]="张三"
user[age]=25
user[city]="北京"
# 或一次性定义
declare -A person=(
[name]="李四"
[age]=30
[city]="上海"
)
# 访问
echo "姓名: ${user[name]}"
echo "年龄: ${user[age]}"
# 获取所有键和值
echo "所有键: ${!user[@]}"
echo "所有值: ${user[@]}"
# 遍历
for key in "${!user[@]}"; do
echo "$key: ${user[$key]}"
done
# 获取长度
echo "元素个数: ${#user[@]}"
解释:关联数组(类似其他语言的字典/哈希表)需要用 declare -A 显式声明。
运算符
算术运算
#!/bin/bash
# 方式 1:$(( )) 推荐
a=10
b=3
echo "加法: $((a + b))" # 13
echo "减法: $((a - b))" # 7
echo "乘法: $((a * b))" # 30
echo "除法: $((a / b))" # 3(整数除法)
echo "取余: $((a % b))" # 1
echo "幂运算: $((a ** 2))" # 100
# 方式 2:let 命令
let c=a+b
echo "let 结果: $c"
# 方式 3:expr 命令
result=$(expr $a + $b)
echo "expr 结果: $result"
# 方式 4:$[ ](旧语法,不推荐)
echo "$[a + b]"
# 自增自减
i=5
echo "i = $i"
((i++)) # 自增
echo "i++ = $i" # 6
((i--)) # 自减
echo "i-- = $i" # 5
# 复合赋值
((i += 10)) # i = i + 10
echo "i += 10: $i"
# 使用 bc 进行浮点运算
float_result=$(echo "scale=2; 10 / 3" | bc)
echo "浮点除法: $float_result" # 3.33
解释:
$(( ))是推荐的算术运算语法,简洁高效- Bash 只支持整数运算,浮点运算需要使用
bc或awk scale设置bc的小数位数
比较运算
#!/bin/bash
# 数值比较
a=10
b=20
# 在 [[ ]] 中使用
if [[ $a -eq $b ]]; then echo "相等"; fi # equal
if [[ $a -ne $b ]]; then echo "不相等"; fi # not equal
if [[ $a -lt $b ]]; then echo "小于"; fi # less than
if [[ $a -le $b ]]; then echo "小于等于"; fi # less or equal
if [[ $a -gt $b ]]; then echo "大于"; fi # greater than
if [[ $a -ge $b ]]; then echo "大于等于"; fi # greater or equal
# 在 (( )) 中使用数学符号
if (( a < b )); then echo "a 小于 b"; fi
if (( a != b )); then echo "a 不等于 b"; fi
if (( a >= 5 && b <= 30 )); then echo "符合条件"; fi
# 字符串比较
str1="hello"
str2="world"
if [[ $str1 == $str2 ]]; then echo "字符串相等"; fi
if [[ $str1 != $str2 ]]; then echo "字符串不相等"; fi
if [[ $str1 < $str2 ]]; then echo "字典序小于"; fi
if [[ $str1 > $str2 ]]; then echo "字典序大于"; fi
# 字符串判断
if [[ -n $str1 ]]; then echo "字符串非空"; fi # non-zero length
if [[ -z $str1 ]]; then echo "字符串为空"; fi # zero length
解释:
- 数值比较:
-eq、-ne、-lt、-le、-gt、-ge - 在
(( ))中可以直接使用<、>、==、!=进行数值比较 - 字符串比较:
==、!=、<、>
文件测试
#!/bin/bash
file="/etc/passwd"
# 文件存在性测试
[[ -e $file ]] && echo "文件存在" # exists
[[ -f $file ]] && echo "是普通文件" # regular file
[[ -d $file ]] && echo "是目录" # directory
[[ -L $file ]] && echo "是符号链接" # symbolic link
[[ -b $file ]] && echo "是块设备" # block device
[[ -c $file ]] && echo "是字符设备" # character device
[[ -S $file ]] && echo "是套接字" # socket
[[ -p $file ]] && echo "是命名管道" # named pipe
# 文件权限测试
[[ -r $file ]] && echo "可读" # readable
[[ -w $file ]] && echo "可写" # writable
[[ -x $file ]] && echo "可执行" # executable
# 文件属性测试
[[ -s $file ]] && echo "文件非空" # non-zero size
[[ -h $file ]] && echo "是符号链接" # synonym for -L
# 文件比较
file2="/etc/shadow"
[[ $file -nt $file2 ]] && echo "file 比 file2 新" # newer than
[[ $file -ot $file2 ]] && echo "file 比 file2 旧" # older than
逻辑运算
#!/bin/bash
# 逻辑与、或、非
a=true
b=false
# 在 [[ ]] 中
[[ $a && $b ]] && echo "与运算" # 与
[[ $a || $b ]] && echo "或运算" # 或
[[ ! $a ]] && echo "非运算" # 非
# 在 (( )) 中
x=1
y=0
(( x && y )) && echo "与运算为真"
(( x || y )) && echo "或运算为真"
(( !y )) && echo "非运算为真"
# 命令的逻辑组合
mkdir test 2>/dev/null && echo "创建成功" || echo "创建失败"
# 成功则执行 && 后面的,失败则执行 || 后面的
# 三目运算符
age=20
result=$(( age >= 18 ? 1 : 0 ))
echo "成年: $result"
条件判断
if 语句
#!/bin/bash
# 基本 if
age=18
if [[ $age -ge 18 ]]; then
echo "已成年"
fi
# if-else
score=75
if [[ $score -ge 60 ]]; then
echo "及格"
else
echo "不及格"
fi
# if-elif-else
score=85
if [[ $score -ge 90 ]]; then
echo "优秀"
elif [[ $score -ge 80 ]]; then
echo "良好"
elif [[ $score -ge 60 ]]; then
echo "及格"
else
echo "不及格"
fi
# 嵌套 if
age=25
has_license=true
if [[ $age -ge 18 ]]; then
if [[ $has_license == "true" ]]; then
echo "可以驾驶"
else
echo "需要考驾照"
fi
else
echo "未成年,不能驾驶"
fi
# 单行 if
[[ $score -ge 60 ]] && echo "及格" || echo "不及格"
case 语句
#!/bin/bash
# 基本 case
read -p "请输入选项 (start/stop/restart/status): " action
case $action in
start)
echo "启动服务..."
;;
stop)
echo "停止服务..."
;;
restart)
echo "重启服务..."
;;
status)
echo "查看状态..."
;;
*)
echo "未知选项: $action"
echo "用法: $0 {start|stop|restart|status}"
;;
esac
# 模式匹配
file="test.jpg"
case $file in
*.jpg|*.jpeg|*.png)
echo "图片文件"
;;
*.mp4|*.avi|*.mkv)
echo "视频文件"
;;
*.txt|*.md)
echo "文本文件"
;;
*)
echo "其他文件"
;;
esac
# 字符范围
char="B"
case $char in
[a-z])
echo "小写字母"
;;
[A-Z])
echo "大写字母"
;;
[0-9])
echo "数字"
;;
*)
echo "其他字符"
;;
esac
# 使用 ;;& 继续匹配(Bash 4.0+)
value="hello"
case $value in
*ell*)
echo "包含 ell"
;;& # 继续匹配后面的模式
hell*)
echo "以 hell 开头"
;;&
*o)
echo "以 o 结尾"
;;
esac
解释:
;;类似其他语言的break,终止 case 分支;&执行下一个分支的命令(不匹配模式);;&继续匹配后面的模式
循环
for 循环
#!/bin/bash
# 遍历列表
for i in 1 2 3 4 5; do
echo "数字: $i"
done
# 遍历范围
for i in {1..5}; do
echo "数字: $i"
done
# 指定步长
for i in {1..10..2}; do
echo "奇数: $i" # 1, 3, 5, 7, 9
done
# C 风格 for 循环
for ((i=1; i<=5; i++)); do
echo "计数: $i"
done
# 遍历文件
for file in *.txt; do
echo "处理文件: $file"
done
# 遍历命令输出
for user in $(cat /etc/passwd | cut -d: -f1); do
echo "用户: $user"
done
# 遍历数组
fruits=("apple" "banana" "cherry")
for fruit in "${fruits[@]}"; do
echo "水果: $fruit"
done
# 遍历目录
for dir in */; do
echo "目录: $dir"
done
while 循环
#!/bin/bash
# 基本 while
count=1
while [[ $count -le 5 ]]; do
echo "计数: $count"
((count++))
done
# 读取文件
while IFS= read -r line; do
echo "行: $line"
done < file.txt
# 读取文件(带行号)
line_num=1
while IFS= read -r line; do
echo "第 $line_num 行: $line"
((line_num++))
done < file.txt
# 无限循环
while true; do
echo "按 Ctrl+C 退出"
sleep 1
done
# 或使用 :
while :; do
echo "无限循环"
sleep 1
done
# 条件控制
num=0
while [[ $num -lt 10 ]]; do
((num++))
if [[ $num -eq 5 ]]; then
continue # 跳过本次循环
fi
if [[ $num -eq 8 ]]; then
break # 退出循环
fi
echo "数字: $num"
done
until 循环
#!/bin/bash
# until 循环(条件为假时执行)
count=1
until [[ $count -gt 5 ]]; do
echo "计数: $count"
((count++))
done
# 等待服务启动
until systemctl is-active --quiet nginx; do
echo "等待 nginx 启动..."
sleep 1
done
echo "nginx 已启动"
select 循环(菜单)
#!/bin/bash
# 创建菜单
PS3="请选择 (输入数字): " # 提示符
options=("选项1" "选项2" "选项3" "退出")
select opt in "${options[@]}"; do
case $opt in
"选项1")
echo "你选择了选项1"
;;
"选项2")
echo "你选择了选项2"
;;
"选项3")
echo "你选择了选项3"
;;
"退出")
echo "再见!"
break
;;
*)
echo "无效选项: $REPLY"
;;
esac
done
循环控制
#!/bin/bash
# break - 退出循环
for i in {1..10}; do
if [[ $i -eq 5 ]]; then
break # 退出循环
fi
echo "i = $i"
done
# continue - 跳过本次迭代
for i in {1..5}; do
if [[ $i -eq 3 ]]; then
continue # 跳过 3
fi
echo "i = $i"
done
# break N - 退出 N 层循环
for i in {1..3}; do
for j in {1..3}; do
if [[ $i -eq 2 && $j -eq 2 ]]; then
break 2 # 退出两层循环
fi
echo "i=$i, j=$j"
done
done
函数
函数定义和调用
#!/bin/bash
# 方式 1:使用 function 关键字
function greet() {
echo "你好,世界!"
}
# 方式 2:不使用 function 关键字
greet2() {
echo "Hello, World!"
}
# 调用函数
greet
greet2
函数参数
#!/bin/bash
# 带参数的函数
greet() {
echo "你好,$1!" # $1 是第一个参数
echo "你的年龄是 $2 岁"
echo "参数个数: $#"
echo "所有参数: $@"
}
# 调用并传参
greet "张三" 25
# 参数的默认值
connect() {
local host=${1:-localhost} # 默认 localhost
local port=${2:-3306} # 默认 3306
echo "连接到 $host:$port"
}
connect # 连接到 localhost:3306
connect "192.168.1.1" 8080 # 连接到 192.168.1.1:8080
返回值
#!/bin/bash
# 使用 return 返回状态码
is_even() {
local num=$1
if (( num % 2 == 0 )); then
return 0 # 成功
else
return 1 # 失败
fi
}
is_even 4
if [[ $? -eq 0 ]]; then
echo "4 是偶数"
fi
# 使用 echo 返回值
get_square() {
local num=$1
echo $((num * num))
}
result=$(get_square 5)
echo "5 的平方是 $result"
# 返回多个值
get_min_max() {
local arr=("$@")
local min=${arr[0]}
local max=${arr[0]}
for num in "${arr[@]}"; do
((num < min)) && min=$num
((num > max)) && max=$num
done
echo "$min $max"
}
read min max <<< $(get_min_max 3 7 2 9 1)
echo "最小值: $min, 最大值: $max"
局部变量
#!/bin/bash
# 默认变量是全局的
global_var="全局变量"
test_scope() {
local local_var="局部变量" # 使用 local 声明局部变量
global_var="修改全局变量"
echo "函数内 local_var: $local_var"
echo "函数内 global_var: $global_var"
}
test_scope
echo "函数外 local_var: $local_var" # 空,因为局部变量
echo "函数外 global_var: $global_var" # 修改后的值
递归函数
#!/bin/bash
# 阶乘
factorial() {
local n=$1
if (( n <= 1 )); then
echo 1
else
local prev=$(factorial $((n - 1)))
echo $((n * prev))
fi
}
result=$(factorial 5)
echo "5! = $result" # 120
# 斐波那契数列
fibonacci() {
local n=$1
if (( n <= 1 )); then
echo $n
else
local a=$(fibonacci $((n - 1)))
local b=$(fibonacci $((n - 2)))
echo $((a + b))
fi
}
result=$(fibonacci 10)
echo "F(10) = $result" # 55
输入输出
用户输入
#!/bin/bash
# 基本输入
read -p "请输入姓名: " name
echo "你好,$name!"
# 静默输入(密码)
read -s -p "请输入密码: " password
echo
echo "密码已接收"
# 带超时的输入
read -t 5 -p "5秒内输入 (否则使用默认值): " input
input=${input:-"默认值"}
echo "输入: $input"
# 限制输入长度
read -n 1 -p "按任意键继续..." key
echo
echo "你按了: $key"
# 读取到数组
read -a arr -p "输入多个单词 (空格分隔): "
echo "第一个: ${arr[0]}"
echo "所有: ${arr[@]}"
# 读取整行
read -r -p "输入包含反斜杠的文本: " text
echo "输入: $text"
格式化输出
#!/bin/bash
# printf 格式化输出
name="张三"
age=25
score=95.5
# 基本格式
printf "姓名: %s\n" $name
printf "年龄: %d\n" $age
printf "成绩: %.1f\n" $score
# 格式化表格
printf "%-10s %-8s %-5s\n" "姓名" "年龄" "成绩"
printf "%-10s %-8d %-5.1f\n" "张三" 25 95.5
printf "%-10s %-8d %-5.1f\n" "李四" 30 88.0
printf "%-10s %-8d %-5.1f\n" "王五" 28 92.3
# 格式说明符
# %s - 字符串
# %d - 整数
# %f - 浮点数
# %x - 十六进制
# %o - 八进制
# %e - 科学计数法
# 宽度和对齐
printf "|%10s|\n" "右对齐" # 右对齐,宽度10
printf "|%-10s|\n" "左对齐" # 左对齐,宽度10
printf "|%010d|\n" 123 # 前导零,宽度10
重定向
#!/bin/bash
# 输出重定向
echo "普通输出"
echo "到文件" > output.txt # 覆盖
echo "追加内容" >> output.txt # 追加
# 错误重定向
ls /nonexistent 2> error.log # 只重定向错误
ls /nonexistent 2>&1 # 错误合并到标准输出
ls /nonexistent &> all.log # 输出和错误都重定向
# 输入重定向
while read line; do
echo "处理: $line"
done < input.txt
# Here Document(多行输入)
cat << EOF
这是一个
多行文本
变量: $name
EOF
# Here String(单行输入)
grep "pattern" <<< "搜索这个字符串"
# /dev/null(丢弃输出)
some_command > /dev/null 2>&1 # 丢弃所有输出
管道
#!/bin/bash
# 管道连接命令
cat file.txt | grep "error" | wc -l
# 管道中的变量
files=$(ls | grep "\.txt$")
echo "文本文件: $files"
# 进程替换
# 比较两个目录
diff <(ls dir1) <(ls dir2)
# while 读取管道
ls -la | while read -r line; do
echo "处理: $line"
done
信号处理
#!/bin/bash
# 信号处理函数
cleanup() {
echo ""
echo "正在清理..."
rm -f /tmp/temp_file_$$
echo "清理完成,退出"
exit 0
}
# 捕获信号
trap cleanup SIGINT SIGTERM # Ctrl+C 和 kill 命令
# 忽略信号
# trap '' SIGINT # 忽略 Ctrl+C
# 捕获脚本退出
trap 'echo "脚本退出";' EXIT
# 捕获调试信息
# trap 'echo "执行: $BASH_COMMAND"' DEBUG
echo "脚本 PID: $$"
echo "按 Ctrl+C 测试信号处理..."
# 模拟长时间运行
while true; do
echo "运行中... (按 Ctrl+C 终止)"
sleep 1
done
常用信号:
| 信号 | 编号 | 说明 |
|---|---|---|
| SIGINT | 2 | Ctrl+C |
| SIGTERM | 15 | kill 默认信号 |
| SIGKILL | 9 | 强制终止(不可捕获) |
| SIGHUP | 1 | 终端关闭 |
| SIGQUIT | 3 | Ctrl+\ |
| SIGUSR1 | 10 | 用户自定义 |
| SIGUSR2 | 12 | 用户自定义 |
调试技巧
调试模式
#!/bin/bash
# 方式 1:脚本开头设置
set -x # 显示执行的命令
set -e # 命令失败时退出
set -u # 使用未定义变量时报错
set -o pipefail # 管道中的命令失败时退出
# 常用组合
set -euo pipefail
# 方式 2:运行时启用
# bash -x script.sh
# bash -v script.sh # 显示读取的行
# 方式 3:部分调试
echo "开始调试..."
set -x
some_command
set +x
echo "调试结束"
常用调试技巧
#!/bin/bash
# 打印变量
echo "DEBUG: var = $var"
printf "DEBUG: array = %s\n" "${array[@]}"
# 打印函数调用栈
print_stack() {
local frame=0
while caller $frame; do
((frame++))
done
}
# 检查命令是否存在
if ! command -v jq &> /dev/null; then
echo "错误: 需要安装 jq"
exit 1
fi
# 使用 ShellCheck 检查脚本
# shellcheck script.sh
# 使用 bashdb 调试器
# bashdb script.sh
实战案例
系统监控脚本
#!/bin/bash
# 系统监控脚本
monitor_system() {
echo "===== 系统监控 ====="
echo "时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo
# CPU 使用率
echo "CPU 使用率:"
top -bn1 | grep "Cpu(s)" | awk '{print " 使用率: " 100-$8 "%"}'
# 内存使用
echo "内存使用:"
free -h | awk '/Mem:/ {printf " 已用: %s / %s (%.1f%%)\n", $3, $2, ($3/$2)*100}'
# 磁盘使用
echo "磁盘使用:"
df -h | awk '/^\/dev/ {printf " %s: %s / %s (%s)\n", $6, $3, $2, $5}'
# 网络连接
echo "网络连接数:"
ss -s | awk '/TCP:/ {print " TCP:", $2}'
}
monitor_system
日志分析脚本
#!/bin/bash
# 分析 Nginx 访问日志
analyze_nginx_log() {
local log_file=${1:-/var/log/nginx/access.log}
echo "===== 日志分析 ====="
echo "日志文件: $log_file"
echo
# 请求数统计
echo "总请求数: $(wc -l < "$log_file")"
# 状态码统计
echo "状态码分布:"
awk '{print $9}' "$log_file" | sort | uniq -c | sort -rn | head -10
# 访问量前 10 的 IP
echo "访问量前 10 的 IP:"
awk '{print $1}' "$log_file" | sort | uniq -c | sort -rn | head -10
# 最常访问的 URL
echo "最常访问的 URL:"
awk '{print $7}' "$log_file" | sort | uniq -c | sort -rn | head -10
}
analyze_nginx_log "$@"
自动备份脚本
#!/bin/bash
# 自动备份脚本
set -e
# 配置
SOURCE_DIR="/var/www/html"
BACKUP_DIR="/backup"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="backup_${DATE}.tar.gz"
LOG_FILE="/var/log/backup.log"
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# 检查目录
if [[ ! -d "$SOURCE_DIR" ]]; then
log "错误: 源目录不存在: $SOURCE_DIR"
exit 1
fi
mkdir -p "$BACKUP_DIR"
# 创建备份
log "开始备份: $SOURCE_DIR"
tar -czf "${BACKUP_DIR}/${BACKUP_FILE}" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")" 2>/dev/null
if [[ $? -eq 0 ]]; then
log "备份成功: ${BACKUP_DIR}/${BACKUP_FILE}"
log "备份大小: $(du -h "${BACKUP_DIR}/${BACKUP_FILE}" | cut -f1)"
else
log "错误: 备份失败"
exit 1
fi
# 删除 7 天前的备份
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +7 -delete
log "已清理 7 天前的旧备份"
# 可选:同步到远程
# rsync -avz "${BACKUP_DIR}/${BACKUP_FILE}" user@remote:/backup/
log "备份任务完成"
批量文件处理
#!/bin/bash
# 批量重命名图片文件
rename_images() {
local dir=${1:-.}
local prefix=${2:-image}
local count=1
for file in "$dir"/*.{jpg,jpeg,png} 2>/dev/null; do
if [[ -f "$file" ]]; then
ext="${file##*.}"
new_name="${prefix}_$(printf "%04d" $count).${ext}"
mv "$file" "$dir/$new_name"
echo "重命名: $file -> $new_name"
((count++))
fi
done
echo "处理完成,共重命名 $((count-1)) 个文件"
}
rename_images "$@"
最佳实践
脚本规范
#!/bin/bash
# =============================================================================
# 脚本名称: example.sh
# 功能描述: 示例脚本,展示脚本规范
# 作 者: 作者名
# 创建日期: 2024-03-15
# 版 本: 1.0.0
# =============================================================================
set -euo pipefail # 严格模式
IFS=$'\n\t' # 设置分隔符
# 常量定义
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
# 帮助信息
usage() {
cat << EOF
用法: $SCRIPT_NAME [选项] <参数>
选项:
-h, --help 显示帮助信息
-v, --version 显示版本信息
-d, --debug 启用调试模式
示例:
$SCRIPT_NAME -d input.txt
EOF
}
# 版本信息
version() {
echo "$SCRIPT_NAME version 1.0.0"
}
# 主函数
main() {
# 参数解析
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-v|--version)
version
exit 0
;;
-d|--debug)
set -x
shift
;;
*)
break
;;
esac
done
# 主逻辑
echo "脚本执行完成"
}
# 入口
main "$@"
编码建议
- 使用
set -euo pipefail- 让脚本在错误时立即退出 - 变量使用
${}- 明确变量边界 - 字符串用双引号 - 防止空格问题
- 使用
[[ ]]- 比[ ]更强大 - 函数化代码 - 提高可读性和复用性
- 添加注释 - 解释复杂逻辑
- 错误处理 - 检查命令执行结果
- 日志记录 - 方便排查问题
小结
本章我们学习了:
- Shell 脚本的基本结构(shebang、注释)
- 变量和数据类型(字符串、数组、关联数组)
- 运算符(算术、比较、逻辑)
- 条件判断(if、case)
- 循环结构(for、while、until、select)
- 函数定义和调用
- 输入输出和重定向
- 信号处理
- 调试技巧
- 实战案例
练习
- 编写一个脚本,统计指定目录下的文件数量和总大小
- 编写一个脚本,监控某个进程,如果进程不存在则自动重启
- 编写一个脚本,实现简单的计算器功能(加减乘除)
- 编写一个脚本,批量修改文件名(如将所有
.txt改为.bak) - 编写一个脚本,解析 CSV 文件并输出指定列