运维笔记: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

  • 常用编辑器

    • 入门: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 推荐学习资源
  • 在线教程
  • 书籍
    • 《Linux Shell 脚本攻略》:实用案例丰富,适合实践。
    • 《Advanced Bash-Scripting Guide》:深入讲解 Bash 高级特性。
  • 练习平台
7.2 进阶方向
  • 自动化运维:结合 Ansible、GitLab CI/CD,用 Shell 脚本实现部署流水线。
  • 系统工具开发:编写实用工具(如日志分析器、资源监控工具),发布到系统 PATH。
  • Shell 脚本调试与性能优化:学习bashdb调试器,用shellcheck进行静态代码检查。

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

相关推荐
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质2 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
Inhand陈工2 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智2 天前
ARP代理--工作原理
运维·网络·arp·arp代理
shushangyun_2 天前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
闪闪发亮的小星星2 天前
高斯光以及高斯光公式解释
笔记
施努卡机器视觉2 天前
SNK施努卡侧滑门锁上滑轮总成自动化装配线,从零件到组件,全流程精密制造方案
运维·自动化·制造
cqbzcsq2 天前
CellFlow虚拟细胞论文阅读
论文阅读·人工智能·笔记·学习·生物信息
AC赳赳老秦2 天前
用 OpenClaw 搭建服务器故障应急响应系统,自动处理 80% 常见运维故障
android·运维·服务器·python·rxjava·deepseek·openclaw