运维笔记: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和搜索引擎,积累经验后自然能得心应手。

相关推荐
DeepHacking2 小时前
Ubuntu 22.04 安装 Allow Locked Remote Desktop 扩展:解决锁屏后 mstsc 无法连接的问题
linux·运维·ubuntu
XS0301062 小时前
agent笔记(二)Langchain关键对象
人工智能·笔记·langchain
夜瞬2 小时前
NLP学习笔记05:命名实体识别(NER)入门——从规则方法到 BERT
笔记·学习·自然语言处理
李日灐2 小时前
<3>Linux 基础指令:从时间、查找、文本过滤到 .zip/.tgz 压缩解压与常用热键
linux·运维·服务器·开发语言·后端·面试·指令
The Chosen One9852 小时前
【实物图讲解】硬盘的工作原理笔记
笔记
木雷坞2 小时前
2026年了,NAS拉个Docker镜像还要3小时?技术方案PK与实测对比 🚀
运维·docker·容器
wanhengidc2 小时前
云服务器和物理服务器的不同之处
运维·服务器·网络·网络协议·智能手机
色空大师2 小时前
【linux开放端口-以8848为例】
linux·运维·服务器·防火墙
咋吃都不胖lyh2 小时前
在 Linux 环境下,查看、编辑并使环境变量生效
linux·运维·服务器