Shell 脚本编程完整教程

一、Shell 分类与 Bash 初始化文件

1. Shell 分类

# 常见的 Shell 类型
# 1. Bourne Shell (sh) - 最经典
# 2. Bourne Again Shell (bash) - Linux 默认
# 3. Korn Shell (ksh) - 兼容 sh 的增强版
# 4. C Shell (csh) - 类似 C 语言语法
# 5. Z Shell (zsh) - 功能强大,macOS 默认

# 查看当前使用的 Shell
echo $SHELL
echo $0

# 查看可用的 Shell
cat /etc/shells

2. Bash 初始化文件说明

# 启动文件加载顺序:

# 1. 登录 Shell 的加载顺序
#    /etc/profile → ~/.bash_profile → ~/.bash_login → ~/.profile
#    → /etc/bash.bashrc → ~/.bashrc

# 2. 非登录 Shell
#    ~/.bashrc → /etc/bash.bashrc

# 文件作用说明:

# /etc/profile - 系统全局配置
# ~/.bash_profile - 用户登录配置
# ~/.bashrc - 用户交互式 Shell 配置
# ~/.profile - 用户配置(通用)
# /etc/bash.bashrc - 系统 bashrc 配置

二、Shell 脚本基础

1. 创建和运行脚本

#!/bin/bash
# 创建脚本文件
# script.sh

# 1. 使用 . 或 source 执行(在当前Shell中执行)
. script.sh
source script.sh

# 2. 使用 bash 执行
bash script.sh

# 3. 作为可执行文件
chmod +x script.sh
./script.sh

2. Export 和环境变量

#!/bin/bash
# export_demo.sh

# 局部变量(只在当前Shell中有效)
LOCAL_VAR="I'm local"
echo "局部变量: $LOCAL_VAR"

# 环境变量(可被子进程继承)
export GLOBAL_VAR="I'm global"
echo "环境变量: $GLOBAL_VAR"

# 查看所有环境变量
printenv
# 或
env

# 查看特定环境变量
echo $PATH
echo $HOME
echo $USER

# 设置永久环境变量
# 1. 当前会话
export MY_VAR="value"

# 2. 永久设置(添加到 ~/.bashrc 或 ~/.bash_profile)
echo 'export MY_VAR="value"' >> ~/.bashrc
source ~/.bashrc

三、Shell 变量

1. 变量基础

#!/bin/bash
# variable_demo.sh

# 定义变量(等号两边不能有空格)
name="John"
age=25
PI=3.1415926

# 使用变量
echo "Name: $name"
echo "Age: ${age}"  # 推荐使用${},更清晰

# 只读变量
readonly readonly_var="This is read only"
# readonly_var="change"  # 这行会报错

# 删除变量
unset name

详情参考 变量详细说明

2. 特殊变量

#!/bin/bash
# special_variables.sh

echo "当前脚本: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "所有参数: $@"
echo "参数个数: $#"
echo "上个命令的退出状态: $?"
echo "当前进程ID: $$"
echo "后台最后一个进程ID: $!"
echo "所有参数(一个字符串): $*"

# 示例
./special_variables.sh arg1 arg2 arg3
# 输出:
# 当前脚本: ./special_variables.sh
# 第一个参数: arg1
# 第二个参数: arg2
# 所有参数: arg1 arg2 arg3
# 参数个数: 3

3. 变量扩展

#!/bin/bash
# variable_expansion.sh

# 默认值
name=${1:-"Guest"}
echo "Hello, $name"

# 检查变量是否设置
value=${VAR?"VAR 没有设置!"}

# 字符串长度
str="Hello World"
echo "长度: ${#str}"

# 子字符串
echo "子串(1-5): ${str:0:5}"
echo "从第6个开始: ${str:6}"

# 模式匹配
filename="document.txt"
echo "去掉.txt: ${filename%.txt}"
echo "去掉doc: ${filename#doc}"

# 大小写转换
text="Hello World"
echo "大写: ${text^^}"
echo "小写: ${text,,}"

四、顺序执行

1. 命令组合

#!/bin/bash
# sequential.sh

# 顺序执行
echo "第一步"
echo "第二步"
echo "第三步"

# 命令分隔符
date; pwd; whoami

# 命令分组
{ echo "开始"; ls -l; echo "结束"; } > output.txt

# 子Shell执行
(echo "在子Shell中"; cd /tmp; pwd)
echo "回到原目录: $(pwd)"

2. 输入输出

#!/bin/bash
# io_demo.sh

# 标准输出
echo "正常输出"

# 标准错误
echo "错误信息" >&2

# 重定向
echo "输出到文件" > output.txt
echo "追加到文件" >> output.txt
ls nofile 2> error.txt
ls -l > all.txt 2>&1
ls -l &> all_output.txt

# 输入重定向
cat << EOF
多行文本
第二行
第三行
EOF

# 管道
cat /etc/passwd | grep root
ls -l | wc -l

五、判断语句

1. if 语句

#!/bin/bash
# if_demo.sh

# 基本语法
if [ condition ]; then
    commands
fi

# if-else
if [ condition ]; then
    commands1
else
    commands2
fi

# if-elif-else
if [ condition1 ]; then
    commands1
elif [ condition2 ]; then
    commands2
else
    commands3
fi

2. 测试表达式

#!/bin/bash
# test_expressions.sh

# 字符串比较
str1="hello"
str2="world"

if [ "$str1" = "$str2" ]; then
    echo "字符串相等"
fi

if [ "$str1" != "$str2" ]; then
    echo "字符串不相等"
fi

if [ -z "$str1" ]; then
    echo "字符串为空"
fi

if [ -n "$str1" ]; then
    echo "字符串非空"
fi

# 数值比较
num1=10
num2=20

if [ $num1 -eq $num2 ]; then
    echo "相等"
fi

if [ $num1 -ne $num2 ]; then
    echo "不相等"
fi

if [ $num1 -gt $num2 ]; then
    echo "大于"
fi

if [ $num1 -lt $num2 ]; then
    echo "小于"
fi

if [ $num1 -ge 10 ]; then
    echo "大于等于10"
fi

if [ $num1 -le 10 ]; then
    echo "小于等于10"
fi

# 文件测试
file="test.txt"

if [ -f "$file" ]; then
    echo "是普通文件"
fi

if [ -d "$file" ]; then
    echo "是目录"
fi

if [ -e "$file" ]; then
    echo "文件存在"
fi

if [ -r "$file" ]; then
    echo "可读"
fi

if [ -w "$file" ]; then
    echo "可写"
fi

if [ -x "$file" ]; then
    echo "可执行"
fi

if [ -s "$file" ]; then
    echo "文件不为空"
fi

3. case 语句

#!/bin/bash
# case_demo.sh

# 基本语法
case "$variable" in
    pattern1)
        commands1
        ;;
    pattern2)
        commands2
        ;;
    pattern3|pattern4)
        commands3
        ;;
    *)
        default_commands
        ;;
esac

# 示例
fruit="apple"

case "$fruit" in
    apple)
        echo "It's an apple"
        ;;
    banana|orange)
        echo "It's a banana or orange"
        ;;
    "water melon")
        echo "It's a water melon"
        ;;
    *)
        echo "Unknown fruit"
        ;;
esac

六、循环语句

1. for 循环

#!/bin/bash
# for_loop.sh

# 基本语法
for variable in list; do
    commands
done

# 示例1:遍历列表
for fruit in apple banana orange; do
    echo "I like $fruit"
done

# 示例2:遍历文件
for file in *.txt; do
    echo "Processing $file"
done

# 示例3:C风格for循环
for ((i=1; i<=5; i++)); do
    echo "Count: $i"
done

# 示例4:遍历命令输出
for user in $(cut -d: -f1 /etc/passwd | head -5); do
    echo "User: $user"
done

# 示例5:遍历数组
fruits=("apple" "banana" "orange")
for fruit in "${fruits[@]}"; do
    echo "Fruit: $fruit"
done

2. while 循环

#!/bin/bash
# while_loop.sh

# 基本语法
while condition; do
    commands
done

# 示例1:计数器
count=1
while [ $count -le 5 ]; do
    echo "Count: $count"
    ((count++))
done

# 示例2:读取文件
while IFS= read -r line; do
    echo "Line: $line"
done < /etc/passwd

# 示例3:无限循环
while true; do
    echo "Press Ctrl+C to stop"
    sleep 1
done

# 示例4:读取输入
echo "Type 'quit' to exit"
while read input; do
    if [ "$input" = "quit" ]; then
        break
    fi
    echo "You typed: $input"
done

3. until 循环

#!/bin/bash
# until_loop.sh

# 基本语法
until condition; do
    commands
done

# 示例1:等待条件满足
count=1
until [ $count -gt 5 ]; do
    echo "Count: $count"
    ((count++))
done

# 示例2:等待服务启动
echo "Waiting for service to start..."
until curl -f http://localhost:8080/health > /dev/null 2>&1; do
    echo "Service not ready, waiting..."
    sleep 2
done
echo "Service is up!"

4. 循环控制

#!/bin/bash
# loop_control.sh

# 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
    fi
    echo "Processing $i"
done

# 嵌套循环
for i in {1..3}; do
    echo "Outer loop: $i"
    for j in {1..3}; do
        echo "  Inner loop: $j"
    done
done

循环语句

七、函数

1. 函数定义

#!/bin/bash
# functions_demo.sh

# 定义函数
function say_hello {
    echo "Hello, $1!"
}

# 另一种定义方式
greet() {
    local name="$1"  # 局部变量
    echo "Greetings, $name!"
    return 0
}

# 调用函数
say_hello "John"
greet "Alice"

# 返回值
get_date() {
    date +%Y-%m-%d
}

# 捕获函数输出
today=$(get_date)
echo "Today is $today"

2. 函数参数

#!/bin/bash
# function_params.sh

# 带参数的函数
print_info() {
    echo "参数个数: $#"
    echo "第一个参数: $1"
    echo "所有参数: $@"
    echo "参数列表: $*"
}

print_info arg1 arg2 arg3

# 返回值示例
add() {
    local result=$(( $1 + $2 ))
    return $result
}

add 10 20
echo "10 + 20 = $?"  # 注意:返回值只能是0-255

八、实用脚本示例

1. 文件备份脚本

#!/bin/bash
# backup.sh

BACKUP_DIR="/var/backups"
SOURCE_DIR="$1"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/backup_${DATE}.tar.gz"

# 检查参数
if [ $# -ne 1 ]; then
    echo "用法: $0 <源目录>"
    exit 1
fi

# 检查源目录是否存在
if [ ! -d "$SOURCE_DIR" ]; then
    echo "错误: 目录 $SOURCE_DIR 不存在"
    exit 1
fi

# 创建备份目录
mkdir -p "$BACKUP_DIR"

# 执行备份
echo "开始备份 $SOURCE_DIR ..."
tar -czf "$BACKUP_FILE" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")"

# 检查备份结果
if [ $? -eq 0 ]; then
    echo "备份成功: $BACKUP_FILE"

    # 删除7天前的备份
    find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +7 -delete
    echo "已清理7天前的备份"
else
    echo "备份失败!"
    exit 1
fi

2. 系统监控脚本

#!/bin/bash
# system_monitor.sh

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# 日志文件
LOG_FILE="/var/log/system_monitor.log"

# 记录日志
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# 检查CPU使用率
check_cpu() {
    local threshold=80
    local cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)

    if [ $(echo "$cpu_usage > $threshold" | bc) -eq 1 ]; then
        log "${RED}警告: CPU使用率 ${cpu_usage}% 超过阈值 ${threshold}%${NC}"
        return 1
    else
        log "${GREEN}正常: CPU使用率 ${cpu_usage}%${NC}"
        return 0
    fi
}

# 检查内存使用率
check_memory() {
    local threshold=85
    local mem_total=$(free | grep Mem | awk '{print $2}')
    local mem_used=$(free | grep Mem | awk '{print $3}')
    local mem_usage=$((mem_used * 100 / mem_total))

    if [ $mem_usage -gt $threshold ]; then
        log "${RED}警告: 内存使用率 ${mem_usage}% 超过阈值 ${threshold}%${NC}"
        return 1
    else
        log "${GREEN}正常: 内存使用率 ${mem_usage}%${NC}"
        return 0
    fi
}

# 检查磁盘空间
check_disk() {
    local threshold=90
    local disk_usage=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')

    if [ $disk_usage -gt $threshold ]; then
        log "${RED}警告: 磁盘使用率 ${disk_usage}% 超过阈值 ${threshold}%${NC}"
        return 1
    else
        log "${GREEN}正常: 磁盘使用率 ${disk_usage}%${NC}"
        return 0
    fi
}

# 主函数
main() {
    log "===== 系统监控开始 ====="

    local errors=0

    check_cpu || ((errors++))
    check_memory || ((errors++))
    check_disk || ((errors++))

    log "===== 系统监控结束 ====="
    log "发现 $errors 个问题"

    if [ $errors -gt 0 ]; then
        # 发送警报
        echo "系统监控发现 $errors 个问题,请查看日志: $LOG_FILE" | mail -s "系统警报" admin@example.com
        return 1
    fi

    return 0
}

# 执行主函数
main "$@"

3. 综合应用:网站部署脚本

#!/bin/bash
# deploy_website.sh

# 配置
WEB_DIR="/var/www/html"
BACKUP_DIR="/var/backups/web"
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/deploy_${DATE}.log"

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# 日志函数
log() {
    local level="$1"
    local message="$2"
    local color=""

    case "$level" in
        INFO) color="$GREEN" ;;
        WARN) color="$YELLOW" ;;
        ERROR) color="$RED" ;;
        *) color="$BLUE" ;;
    esac

    echo -e "${color}[$(date '+%H:%M:%S')] [$level] ${message}${NC}" | tee -a "$LOG_FILE"
}

# 检查参数
if [ $# -ne 1 ]; then
    echo "用法: $0 <网站包文件>"
    exit 1
fi

PACKAGE="$1"

# 检查包文件
if [ ! -f "$PACKAGE" ]; then
    log "ERROR" "包文件 $PACKAGE 不存在"
    exit 1
fi

# 创建备份目录
mkdir -p "$BACKUP_DIR"

# 1. 备份当前网站
backup_website() {
    log "INFO" "开始备份当前网站..."

    local backup_file="${BACKUP_DIR}/website_${DATE}.tar.gz"

    if tar -czf "$backup_file" -C "$WEB_DIR" . 2>> "$LOG_FILE"; then
        log "INFO" "备份成功: $backup_file"
        return 0
    else
        log "ERROR" "备份失败"
        return 1
    fi
}

# 2. 验证包文件
validate_package() {
    log "INFO" "验证包文件..."

    if file "$PACKAGE" | grep -q "gzip compressed"; then
        log "INFO" "包文件验证通过"
        return 0
    else
        log "ERROR" "包文件格式不正确"
        return 1
    fi
}

# 3. 停止Web服务
stop_service() {
    log "INFO" "停止Web服务..."

    if systemctl stop nginx 2>> "$LOG_FILE"; then
        log "INFO" "Nginx已停止"
        return 0
    else
        log "WARN" "停止Nginx失败,尝试强制停止..."
        pkill -9 nginx
        return 0
    fi
}

# 4. 部署新网站
deploy_new() {
    log "INFO" "开始部署新网站..."

    # 清理原目录
    rm -rf "${WEB_DIR}_new"
    mkdir -p "${WEB_DIR}_new"

    # 解压新包
    if tar -xzf "$PACKAGE" -C "${WEB_DIR}_new" 2>> "$LOG_FILE"; then
        log "INFO" "解压成功"

        # 设置权限
        chown -R www-data:www-data "${WEB_DIR}_new"
        chmod -R 755 "${WEB_DIR}_new"

        return 0
    else
        log "ERROR" "解压失败"
        return 1
    fi
}

# 5. 切换网站
switch_website() {
    log "INFO" "切换网站..."

    # 备份原网站
    mv "$WEB_DIR" "${WEB_DIR}_old"

    # 切换到新网站
    mv "${WEB_DIR}_new" "$WEB_DIR"

    log "INFO" "网站切换完成"
    return 0
}

# 6. 启动Web服务
start_service() {
    log "INFO" "启动Web服务..."

    if systemctl start nginx 2>> "$LOG_FILE"; then
        log "INFO" "Nginx已启动"
        return 0
    else
        log "ERROR" "启动Nginx失败"
        return 1
    fi
}

# 7. 健康检查
health_check() {
    log "INFO" "执行健康检查..."

    local max_retry=5
    local retry_count=0

    while [ $retry_count -lt $max_retry ]; do
        if curl -f http://localhost/health > /dev/null 2>&1; then
            log "INFO" "健康检查通过"
            return 0
        fi

        ((retry_count++))
        log "WARN" "健康检查失败,重试 $retry_count/$max_retry..."
        sleep 3
    done

    log "ERROR" "健康检查失败"
    return 1
}

# 8. 回滚函数
rollback() {
    log "ERROR" "部署失败,执行回滚..."

    # 恢复原网站
    if [ -d "${WEB_DIR}_old" ]; then
        rm -rf "$WEB_DIR"
        mv "${WEB_DIR}_old" "$WEB_DIR"
        log "INFO" "网站已回滚"
    fi

    # 尝试启动服务
    systemctl start nginx
}

# 主部署流程
main() {
    log "INFO" "========== 开始网站部署 =========="

    # 执行步骤
    steps=(
        "备份当前网站:backup_website"
        "验证包文件:validate_package"
        "停止Web服务:stop_service"
        "部署新网站:deploy_new"
        "切换网站:switch_website"
        "启动Web服务:start_service"
        "健康检查:health_check"
    )

    local failed_step=""

    for step in "${steps[@]}"; do
        local step_name="${step%:*}"
        local step_func="${step#*:}"

        log "INFO" "执行: $step_name"

        if ! $step_func; then
            failed_step="$step_name"
            break
        fi
    done

    if [ -n "$failed_step" ]; then
        log "ERROR" "部署失败在步骤: $failed_step"
        rollback
        log "INFO" "========== 部署失败 =========="
        return 1
    else
        # 清理旧备份
        find "$BACKUP_DIR" -name "website_*.tar.gz" -mtime +30 -delete
        log "INFO" "已清理30天前的备份"

        log "INFO" "========== 部署成功 =========="
        return 0
    fi
}

# 异常处理
trap 'log "ERROR" "脚本被中断"; rollback; exit 1' INT TERM

# 执行主函数
if main; then
    exit 0
else
    exit 1
fi

九、最佳实践

1. 脚本模板

#!/bin/bash
# script_template.sh

set -euo pipefail  # 严格模式
# -e: 遇到错误退出
# -u: 使用未定义变量时报错
# -o pipefail: 管道中任何命令失败则整个失败

# 配置
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
readonly LOG_FILE="/var/log/${SCRIPT_NAME}.log"

# 颜色定义
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m'

# 日志函数
log() {
    local level="$1"
    local message="$2"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')

    echo -e "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}

# 错误处理
error_exit() {
    log "ERROR" "$1"
    exit 1
}

# 使用帮助
usage() {
    cat << EOF
用法: $SCRIPT_NAME [选项] <参数>

描述: 这是一个脚本模板

选项:
    -h, --help      显示帮助信息
    -v, --version   显示版本信息
    -d, --debug     调试模式

示例:
    $SCRIPT_NAME --debug arg1
EOF
    exit 0
}

# 解析参数
parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -h|--help)
                usage
                ;;
            -v|--version)
                echo "$SCRIPT_NAME version 1.0.0"
                exit 0
                ;;
            -d|--debug)
                set -x
                shift
                ;;
            --)
                shift
                break
                ;;
            -*)
                error_exit "未知选项: $1"
                ;;
            *)
                break
                ;;
        esac
    done
}

# 主函数
main() {
    parse_args "$@"

    log "INFO" "脚本开始执行"

    # 你的代码逻辑

    log "INFO" "脚本执行完成"
}

# 入口点
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

2. 调试技巧

#!/bin/bash
# debug_demo.sh

# 调试模式
# bash -x script.sh
# 或在脚本中
set -x
# 调试代码
set +x

# 调试特定部分
echo "开始调试"
PS4='+ ${LINENO}: '  # 显示行号
set -x
# 调试的代码
set +x
echo "调试结束"

# 使用trap调试
trap 'echo "在行号 $LINENO 退出,退出码: $?"' EXIT

# 记录所有输出
exec 2> debug.log
exec 1>&2
set -x

十、总结

Shell 脚本要点总结:

  1. #! 指定解释器
  2. 变量使用要加引号
  3. 使用 set -euo pipefail 防止错误
  4. 总是验证输入参数
  5. 使用函数组织代码
  6. 添加适当的日志记录
  7. 处理错误和异常
  8. 添加帮助文档

运行脚本:

# 添加执行权限
chmod +x script.sh

# 运行
./script.sh

# 调试运行
bash -x script.sh

# 语法检查
bash -n script.sh

这个教程涵盖了 Shell 脚本编程的主要方面,从基础到高级应用。建议按照顺序学习,并通过实际编写脚本来加深理解。s

作者:严锋  创建时间:2023-09-14 16:33
最后编辑:严锋  更新时间:2025-12-25 10:39