运维笔记:Shell 脚本入门到实践

一、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
  • 常用编辑器
    • 进阶: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 推荐学习资源

  • 在线教程
  • 书籍
    • 《Linux Shell 脚本攻略》:实用案例丰富,适合实践。
    • 《Advanced Bash-Scripting Guide》:深入讲解 Bash 高级特性。
  • 练习平台

7.2 进阶方向

  • 自动化运维:结合 Ansible、GitLab CI/CD,用 Shell 脚本实现部署流水线。
  • 系统工具开发:编写实用工具(如日志分析器、资源监控工具),发布到系统 PATH。
  • Shell 脚本调试与性能优化:学习bashdb调试器,用shellcheck进行静态代码检查。

通过大量实践(如编写日常运维脚本),才能真正掌握 Shell 脚本的精髓。从简单的单任务脚本开始,逐步挑战复杂的系统管理脚本,遇到问题时善用man bash和搜索引擎,积累经验后自然能得心应手。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值