一、Shell 脚本基础认知
1.1 什么是 Shell 脚本
Shell 脚本是由一系列 Shell 命令组成的文本文件,通过解释器(如 Bash)执行,用于自动化重复操作、批量处理任务或实现复杂的系统管理逻辑。其核心价值在于:
- 自动化:替代人工执行一系列命令(如批量备份、日志清理)。
- 批量处理:对多个文件或服务器执行相同操作(如批量修改配置)。
- 跨平台:在 Linux、macOS 等类 Unix 系统中通用,Windows 可通过 WSL 运行。
1.2 环境准备与运行方式
- 脚本文件格式:以.sh为扩展名(非强制),首行必须指定解释器:
#!/bin/bash # 声明使用Bash解释器
#!/bin/sh # 兼容POSIX标准的Shell(可能是Bash的简化版)
- 运行方法:
# 1. 赋予执行权限后直接运行(推荐)
chmod +x script.sh
./script.sh # 必须用相对/绝对路径,不能直接写script.sh(除非在PATH目录)
# 2. 通过解释器运行(无需执行权限)
bash script.sh
sh script.sh
- 常用编辑器:
-
- 入门:nano script.sh(简单直观)。
-
- 进阶:vim script.sh(语法高亮、快捷键高效),推荐开启set nu(显示行号)和set syntax=sh(语法高亮)。
二、Shell 脚本基础语法
2.1 变量定义与使用
- 基本语法:
# 定义变量(等号前后无空格)
name="Alice"
age=25
# 使用变量($变量名或${变量名},{}用于区分边界)
echo "Name: $name"
echo "Age next year: $((age + 1))" # 数值运算
echo "Full info: ${name}_${age}" # 拼接字符串
# 只读变量(无法修改)
readonly PI=3.14
# PI=3.1415 # 会报错:./script.sh: line 8: PI: readonly variable
# 删除变量( unset后变量不可用)
unset age
echo "Age after unset: $age" # 输出空
- 环境变量与位置参数:
# 环境变量(全局可用,通常大写)
echo "Home directory: $HOME"
echo "Path: $PATH"
export WORK_DIR="/data" # 声明为环境变量,子进程可访问
# 位置参数(脚本后跟随的参数,$0是脚本名,$1-$9是参数,${10}及以上需加{})
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $*" # 所有参数作为单个字符串
echo "All arguments: $@" # 所有参数作为独立字符串(推荐循环使用)
echo "Number of arguments: $#" # 参数个数
2.2 基本运算
- 数值运算:
# 方法1:$((表达式))
a=10
b=3
echo "a + b = $((a + b))" # 13
echo "a * b = $((a * b))" # 30
echo "a / b = $((a / b))" # 3(整数除法)
echo "a % b = $((a % b))" # 1(取余)
# 方法2:expr命令(注意空格)
echo "a - b = $(expr $a - $b)" # 7(减法)
# 方法3:bc工具(支持浮点数)
echo "3.5 + 2.1" | bc # 5.6
echo "scale=2; 10 / 3" | bc # 3.33(scale指定小数位数)
- 字符串运算:
str1="hello"
str2="world"
# 拼接
echo "$str1 $str2" # hello world
# 长度
echo "Length of str1: ${#str1}" # 5
# 截取(${变量:起始位置:长度},起始位置从0开始)
echo "Substring of str2: ${str2:1:3}" # orl(从索引1取3个字符)
2.3 条件判断(if 语句)
- 基本语法:
# 格式1:单分支
if [ 条件 ]; then
命令
fi
# 格式2:双分支
if [ 条件 ]; then
命令1
else
命令2
fi
# 格式3:多分支
if [ 条件1 ]; then
命令1
elif [ 条件2 ]; then
命令2
else
命令3
fi
- 常用条件表达式:
# 数值比较(-eq相等,-ne不等,-gt大于,-lt小于,-ge大于等于,-le小于等于)
num1=10
num2=20
if [ $num1 -lt $num2 ]; then
echo "$num1 < $num2"
fi
# 字符串比较(=相等,!=不等,-z空字符串,-n非空字符串)
str="test"
if [ "$str" = "test" ]; then # 变量加引号防止空值报错
echo "String matches"
fi
if [ -z "$empty_str" ]; then # 判断是否为空
echo "empty_str is empty"
fi
# 文件判断
file="test.txt"
if [ -f "$file" ]; then # -f:是否为普通文件
echo "$file exists"
elif [ -d "dir" ]; then # -d:是否为目录
echo "dir is a directory"
else
echo "$file not found"
fi
注意:[ ]前后必须有空格,变量建议加引号(如"$str"),避免变量为空时语法错误。
2.4 循环语句(for/while)
- for 循环:
# 遍历列表
for fruit in apple banana "cherry pie"; do # 带空格的元素需用引号
echo "Fruit: $fruit"
done
# 遍历数字范围({起始..结束})
for i in {1..5}; do
echo "Count: $i"
done
# 遍历文件(*.txt匹配所有txt文件)
for file in /tmp/*.txt; do
if [ -f "$file" ]; then # 防止无匹配时遍历到"*.txt"字符串
echo "Processing $file"
fi
done
- while 循环:
# 基本用法
count=1
while [ $count -le 3 ]; do
echo "Loop $count"
count=$((count + 1)) # 自增
done
# 无限循环(配合break退出)
num=0
while true; do
num=$((num + 1))
echo "Num: $num"
if [ $num -eq 3 ]; then
break # 退出循环
fi
done
# 读取文件内容(逐行处理)
while read line; do
echo "Line: $line"
done < test.txt # 从test.txt读取内容
2.5 函数定义与调用
- 基本语法:
# 定义函数
function hello { # 或直接写 hello() {
echo "Hello, $1" # $1是函数的第一个参数
}
# 调用函数
hello "World" # 输出:Hello, World
# 带返回值的函数(通过$?获取,返回值是状态码,0-255)
add() {
return $(( $1 + $2 )) # 返回和(仅适合小数值)
}
add 3 5
echo "3 + 5 = $?" # 输出:8
# 复杂返回值(通过echo输出,调用时用$()捕获)
get_full_name() {
local first=$1 # local声明局部变量
local last=$2
echo "${first} ${last}" # 输出结果
}
full_name=$(get_full_name "John" "Doe")
echo "Full name: $full_name" # 输出:John Doe
三、Shell 脚本核心技巧
3.1 输入输出重定向
- 基本重定向:
# 标准输出重定向(>覆盖,>>追加)
echo "Hello" > output.txt # 写入文件,覆盖原有内容
echo "World" >> output.txt # 追加到文件
# 标准错误重定向(2>错误输出,&>同时重定向 stdout和stderr)
ls non_existent_file 2> error.log # 错误信息写入error.log
command &> all_output.log # 所有输出(正常+错误)写入同一文件
# 输入重定向(<从文件读取输入)
sort < numbers.txt # 对numbers.txt内容排序
# 管道(|将前一个命令的输出作为后一个命令的输入)
ls -l | grep ".sh" # 列出所有.sh文件
cat access.log | wc -l # 统计日志行数
3.2 数组与关联数组
- 普通数组(索引数组):
# 定义数组
fruits=("apple" "banana" "cherry")
# 访问元素(索引从0开始)
echo "First fruit: ${fruits[0]}" # apple
# 遍历数组
for fruit in "${fruits[@]}"; do # 用@确保带空格的元素正确处理
echo $fruit
done
# 数组长度
echo "Number of fruits: ${#fruits[@]}" # 3
# 添加元素
fruits+=("date")
- 关联数组(键值对,类似字典):
# 声明关联数组(Bash 4.0+支持)
declare -A user # -A表示关联数组
# 赋值
user["name"]="Alice"
user["age"]=25
# 访问
echo "Name: ${user["name"]}" # Alice
# 遍历键值对
for key in "${!user[@]}"; do # !获取所有键
echo "$key: ${user[$key]}"
done
3.3 字符串处理高级技巧
- 替换与删除:
str="hello world, hello shell"
# 替换第一个匹配
echo "${str/hello/hi}" # hi world, hello shell
# 替换所有匹配
echo "${str//hello/hi}" # hi world, hi shell
# 从开头删除最短匹配
echo "${str#hello }" # world, hello shell
# 从开头删除最长匹配
echo "${str##*hello }" # shell
# 从结尾删除最短匹配
echo "${str% shell}" # hello world, hello
# 从结尾删除最长匹配
echo "${str%%, *}" # hello world
- 大小写转换:
lower="hello"
upper="WORLD"
echo "${lower^^}" # HELLO(转大写,Bash 4.0+)
echo "${upper,,}" # world(转小写)
3.4 命令替换与进程替换
- 命令替换:将命令输出作为变量值,用$(命令)或`命令`(推荐前者,嵌套更清晰):
# 获取当前日期
today=$(date +%Y-%m-%d) # 2023-10-01
echo "Today is $today"
# 嵌套使用
file_count=$(ls /tmp | wc -l)
echo "Files in /tmp: $file_count"
- 进程替换:将命令输出作为临时文件,用<(命令):
# 比较两个命令的输出(无需创建临时文件)
diff <(ls /tmp) <(ls /var/tmp) # 比较/tmp和/var/tmp的文件列表
# 同时处理多个命令输出
paste <(ls /tmp) <(ls /var/tmp) # 并列显示两个目录的文件
四、Shell 脚本实战案例
4.1 系统监控脚本
#!/bin/bash
# 功能:监控CPU、内存、磁盘使用率并告警
# 使用:./system_monitor.sh [阈值CPU%] [阈值内存%] [阈值磁盘%]
# 检查参数
if [ $# -ne 3 ]; then
echo "用法:$0 <cpu_threshold> <mem_threshold> <disk_threshold>"
exit 1
fi
cpu_threshold=$1
mem_threshold=$2
disk_threshold=$3
# 获取当前使用率
cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4}') # 用户+系统CPU
mem_usage=$(free | grep Mem | awk '{print $3/$2 * 100}')
disk_usage=$(df -h / | tail -1 | awk '{print $5}' | sed 's/%//')
# 告警函数
alert() {
local type=$1
local value=$2
local threshold=$3
echo "[$(date +%Y-%m-%d_%H:%M:%S)] 告警:$type使用率过高:$value%(阈值:$threshold%)"
# 可添加邮件告警:echo "..." | mail -s "告警" admin@example.com
}
# 检查CPU
if (( $(echo "$cpu_usage > $cpu_threshold" | bc -l) )); then
alert "CPU" "$cpu_usage" "$cpu_threshold"
fi
# 检查内存
if (( $(echo "$mem_usage > $mem_threshold" | bc -l) )); then
alert "内存" "$mem_usage" "$mem_threshold"
fi
# 检查磁盘
if [ $disk_usage -gt $disk_threshold ]; then
alert "磁盘(/)" "$disk_usage" "$disk_threshold"
fi
# 输出正常信息
echo "[$(date +%Y-%m-%d_%H:%M:%S)] 监控正常:CPU=$cpu_usage% 内存=$mem_usage% 磁盘=$disk_usage%"
4.2 日志分析脚本
#!/bin/bash
# 功能:分析Nginx访问日志,统计TOP 10 IP和404请求
# 使用:./log_analysis.sh access.log
log_file=$1
# 检查文件是否存在
if [ ! -f "$log_file" ]; then
echo "错误:日志文件$log_file不存在"
exit 1
fi
echo "===== TOP 10 访问IP ====="
# 提取第1列(IP),排序去重计数,逆序排列取前10
awk '{print $1}' "$log_file" | sort | uniq -c | sort -nr | head -n 10
echo -e "\n===== 404 请求URL ====="
# 提取状态码为404的行,打印URL(第7列)和IP(第1列)
awk '$9 == 404 {print $1, $7}' "$log_file" | sort | uniq -c | sort -nr
echo -e "\n===== 访问量统计 ====="
total=$(wc -l "$log_file" | awk '{print $1}')
echo "总请求数:$total"
echo "成功请求(200):$(grep " 200 " "$log_file" | wc -l)"
echo "错误请求(5xx):$(grep " [5-9][0-9][0-9] " "$log_file" | wc -l)"
4.3 批量文件处理脚本
#!/bin/bash
# 功能:批量重命名文件(添加前缀和日期),并压缩备份
# 使用:./batch_rename.sh /path/to/files "prefix_"
dir=$1
prefix=$2
date=$(date +%Y%m%d)
# 检查目录
if [ ! -d "$dir" ]; then
echo "错误:目录$dir不存在"
exit 1
fi
# 进入目录
cd "$dir" || exit
# 创建备份目录
backup_dir="backup_$date"
mkdir -p "$backup_dir"
# 遍历文件(排除目录)
for file in *; do
if [ -f "$file" ]; then # 只处理文件
new_name="${prefix}${date}_${file}"
echo "重命名:$file -> $new_name"
mv "$file" "$new_name"
# 复制到备份目录
cp "$new_name" "$backup_dir/"
fi
done
# 压缩备份
echo "压缩备份:$backup_dir.tar.gz"
tar -zcf "${backup_dir}.tar.gz" "$backup_dir"
rm -rf "$backup_dir" # 删除临时备份目录
echo "处理完成,备份文件:${backup_dir}.tar.gz"
4.4 自动化部署脚本
#!/bin/bash
# 功能:从Git拉取代码,编译,部署到生产环境
# 使用:./deploy.sh [分支名,默认main]
branch=${1:-main} # 默认为main分支
app_dir="/opt/myapp"
git_repo="https://github.com/example/myapp.git"
tmp_dir="/tmp/myapp_build"
# 检查依赖
check_dependency() {
if ! command -v $1 &> /dev/null; then
echo "错误:未安装$1,请先安装"
exit 1
fi
}
check_dependency "git"
check_dependency "make"
check_dependency "docker" # 假设用Docker部署
# 清理临时目录
rm -rf "$tmp_dir"
mkdir -p "$tmp_dir"
# 拉取代码
echo "拉取代码:$git_repo 分支:$branch"
git clone -b "$branch" "$git_repo" "$tmp_dir" || { echo "拉取代码失败"; exit 1; }
# 编译(假设使用Makefile)
echo "编译代码..."
cd "$tmp_dir" || exit
make || { echo "编译失败"; exit 1; }
# 构建Docker镜像
echo "构建Docker镜像..."
docker build -t myapp:"$branch" . || { echo "构建镜像失败"; exit 1; }
# 停止旧版本
echo "停止旧版本..."
docker stop myapp &> /dev/null
docker rm myapp &> /dev/null
# 启动新版本
echo "启动新版本..."
docker run -d --name myapp -p 8080:8080 -v "$app_dir/data:/app/data" myapp:"$branch" || { echo "启动失败"; exit 1; }
echo "部署成功!版本:$(git -C "$tmp_dir" rev-parse --short HEAD)" # 输出Git commit短哈希
五、Shell 脚本进阶技巧
5.1 脚本调试方法
- 基本调试:
# 方法1:运行时加-x参数(显示执行的每一行命令)
bash -x script.sh
# 方法2:脚本内加set -x(从set位置开始调试)
#!/bin/bash
set -x # 开启调试
command1
set +x # 关闭调试
command2
- 高级调试:
-
- set -e:脚本中任何命令失败(返回非 0 状态)则立即退出,避免错误累积。
-
- set -u:引用未定义的变量时报错,避免静默错误。
-
- set -o pipefail:管道中任何命令失败,整个管道返回失败状态(默认只看最后一个命令)。
-
- 推荐开头使用:set -euo pipefail
5.2 信号处理与陷阱
- 捕获信号:
# 定义清理函数
cleanup() {
echo "收到退出信号,清理临时文件..."
rm -rf /tmp/my_temp_files
exit 0
}
# 捕获信号:SIGINT(Ctrl+C)、SIGTERM(kill命令)
trap cleanup SIGINT SIGTERM
# 模拟长时间运行的任务
echo "运行中,按Ctrl+C测试信号处理..."
while true; do
sleep 1
done
-
- 用途:脚本被强制终止时,清理临时文件、关闭进程等,避免资源泄露。
5.3 函数库与模块化
- 创建函数库:
# 文件名:lib/common.sh
# 日志函数
log() {
echo "[$(date +%Y-%m-%d_%H:%M:%S)] $1"
}
# 错误处理函数
error_exit() {
log "错误:$1"
exit 1
}
# 检查权限函数
check_root() {
if [ $EUID -ne 0 ]; then # EUID=0表示root
error_exit "需用root权限运行"
fi
}
- 在脚本中引用:
#!/bin/bash
# 导入函数库(使用绝对路径或相对路径)
source /path/to/lib/common.sh # 或 . /path/to/lib/common.sh
log "开始执行脚本..."
check_root # 调用函数库中的函数
# 业务逻辑
if [ ! -d "/data" ]; then
error_exit "/data目录不存在"
fi
六、Shell 脚本最佳实践
6.1 脚本规范
- 开头注释:说明脚本功能、作者、使用方法、参数含义:
#!/bin/bash
# 功能:批量备份指定目录到远程服务器
# 作者:运维团队
# 版本:1.0
# 使用:./backup.sh <本地目录> <远程用户@主机> <远程目录>
# 示例:./backup.sh /data backup@192.168.1.100 /backup/data
- 变量命名:
-
- 全局变量:大写字母 + 下划线(如BACKUP_DIR)。
-
- 局部变量:小写字母 + 下划线(如local_file)。
-
- 避免使用$0-$9以外的单字母变量(易混淆)。
- 错误处理:每个关键步骤检查返回值,失败时明确提示原因,避免静默失败。
6.2 效率优化技巧
- 减少子进程:Shell 调用外部命令(如awk、grep)会创建子进程,耗时较长,简单逻辑用内置语法替代:
# 低效:多次调用外部命令
for file in $(ls *.txt); do ... done
# 高效:直接遍历
for file in *.txt; do ... done # 避免ls的子进程
- 批量操作:用xargs或管道替代循环,处理大量文件更高效:
# 低效:循环删除
for file in /tmp/*.log; do rm "$file"; done
# 高效:批量删除
rm -f /tmp/*.log # 或 find /tmp -name "*.log" -delete
- 使用内置命令:优先用 Bash 内置命令(如echo、printf、test),而非外部命令(如expr)。
6.3 常见陷阱与避坑指南
- 空变量处理:变量可能为空时,必须加引号,否则会语法错误:
str=""
# 错误:[ ]中会变成[ ],语法错误
if [ $str = "test" ]; then ... fi
# 正确:加引号
if [ "$str" = "test" ]; then ... fi
- 文件名含特殊字符:文件名含空格、星号等时,遍历需用"${files[@]}":
# 错误:会将"a b.txt"拆分为"a"和"b.txt"
files=$(ls *".txt")
for file in $files; do ... done
# 正确:用数组
files=(*.txt)
for file in "${files[@]}"; do ... done
- 比较浮点数:[ ]不支持浮点数比较,必须用bc或awk:
a=3.5
b=2.8
# 错误:-gt只支持整数
if [ $a -gt $b ]; then ... fi
# 正确:用bc
if (( $(echo "$a > $b" | bc -l) )); then ... fi
七、学习资源与进阶方向
7.1 推荐学习资源
- 在线教程:
-
- Shell Scripting Tutorial:零基础入门到进阶。
-
- Linux Command Line:免费电子书,涵盖 Shell 脚本。
- 书籍:
-
- 《Linux Shell 脚本攻略》:实用案例丰富,适合实践。
-
- 《Advanced Bash-Scripting Guide》:深入讲解 Bash 高级特性。
- 练习平台:
-
- Exercism.io:交互式 Shell 脚本练习。
-
- LeetCode Shell 题库:算法类 Shell 脚本练习。
7.2 进阶方向
- 自动化运维:结合 Ansible、GitLab CI/CD,用 Shell 脚本实现部署流水线。
- 系统工具开发:编写实用工具(如日志分析器、资源监控工具),发布到系统 PATH。
- Shell 脚本调试与性能优化:学习bashdb调试器,用shellcheck进行静态代码检查。
通过大量实践(如编写日常运维脚本),才能真正掌握 Shell 脚本的精髓。从简单的单任务脚本开始,逐步挑战复杂的系统管理脚本,遇到问题时善用man bash和搜索引擎,积累经验后自然能得心应手。