跳到主要内容

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.shbash 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 只支持整数运算,浮点运算需要使用 bcawk
  • 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

常用信号

信号编号说明
SIGINT2Ctrl+C
SIGTERM15kill 默认信号
SIGKILL9强制终止(不可捕获)
SIGHUP1终端关闭
SIGQUIT3Ctrl+\
SIGUSR110用户自定义
SIGUSR212用户自定义

调试技巧

调试模式

#!/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 "$@"

编码建议

  1. 使用 set -euo pipefail - 让脚本在错误时立即退出
  2. 变量使用 ${} - 明确变量边界
  3. 字符串用双引号 - 防止空格问题
  4. 使用 [[ ]] - 比 [ ] 更强大
  5. 函数化代码 - 提高可读性和复用性
  6. 添加注释 - 解释复杂逻辑
  7. 错误处理 - 检查命令执行结果
  8. 日志记录 - 方便排查问题

小结

本章我们学习了:

  1. Shell 脚本的基本结构(shebang、注释)
  2. 变量和数据类型(字符串、数组、关联数组)
  3. 运算符(算术、比较、逻辑)
  4. 条件判断(if、case)
  5. 循环结构(for、while、until、select)
  6. 函数定义和调用
  7. 输入输出和重定向
  8. 信号处理
  9. 调试技巧
  10. 实战案例

练习

  1. 编写一个脚本,统计指定目录下的文件数量和总大小
  2. 编写一个脚本,监控某个进程,如果进程不存在则自动重启
  3. 编写一个脚本,实现简单的计算器功能(加减乘除)
  4. 编写一个脚本,批量修改文件名(如将所有 .txt 改为 .bak
  5. 编写一个脚本,解析 CSV 文件并输出指定列

参考资源