跳转到内容
搜索 RunEntLinux 文档

仅搜索 RunEntLinux 通用文档,各发行版文档使用各自的搜索入口。

    浏览当前文档目录

    Shell 脚本入门

    适用于所有 Enterprise Linux 发行版(Bash 4+/5)

    在命令行里敲了上百次的命令,能不能一次写好、反复运行?这就是 Shell 脚本的价值。它把一串命令、判断和循环组织成一个文件,让备份、检查、部署这类重复工作自动完成。本页带你从 shebang 写起,到能写出健壮、可调试的实用脚本。

    Enterprise Linux(CentOS Stream、AlmaLinux、Rocky Linux 等)默认的交互 Shell 就是 Bash,所以本页全程使用 Bash 语法。

    • 怎么创建、赋予执行权限并运行一个脚本
    • 变量、引号、命令替换与位置参数
    • if / case 条件判断和 for / while / until 循环
    • 函数、退出码与 set -euo pipefail 健壮性写法
    • 读取输入、算术、数组与 here-document
    • 用 bash -x 调试、用 ShellCheck 静态检查
    • 两个可直接运行的实战脚本:目录备份与服务健康检查
    • 一台运行 EL 的系统,或任意带 Bash 4+/5 的 Linux
    • 会用文本编辑器(vim、nano 均可)
    • 了解基本命令行操作(cd、ls、cat 等)
    • 部分章节需要 sudo 权限(安装 ShellCheck、读取 systemd 状态)

    新建一个文件 hello.sh,写入以下内容:

    hello.sh
    #!/usr/bin/env bash
    echo "你好,$USER!"
    echo "当前目录是 $(pwd)"

    第一行 #!/usr/bin/env bash 叫 shebang,告诉系统用哪个解释器运行这个文件。相比写死的 #!/bin/bash,/usr/bin/env bash 会在 PATH 中查找 bash,可移植性更好。

    接着给脚本加上执行权限,然后运行:

    赋予执行权限并运行
    $ chmod +x hello.sh
    $ ./hello.sh

    注意 ./ 不能省略。当前目录通常不在 PATH 里,直接敲 hello.sh 会提示找不到命令。

    变量赋值时等号两边不能有空格,引用时用 $ 前缀:

    变量基础
    name="centos"
    greeting="Hello, ${name}" # 推荐写法:用 ${} 明确边界
    echo "$greeting"

    引号的规则是脚本最容易踩坑的地方:

    • 双引号 "...":会展开变量,但能防止「单词拆分」和通配符展开。绝大多数情况都用双引号。
    • 单引号 '...':原样输出,不展开任何变量。
    • 不加引号:变量里的空格会把一个值拆成多个,* 会被当成通配符。
    为什么要加引号
    file="my report.txt"
    rm "$file" # 正确:删除名为 "my report.txt" 的一个文件
    # rm $file # 错误:会尝试删除 "my" 和 "report.txt" 两个文件

    习惯写 "${var}":双引号保证安全,花括号让变量名边界清晰(例如 "${name}_backup")。

    用 $(...) 把一条命令的输出赋给变量。它可以嵌套,比老式的反引号 `...` 更清晰,应优先使用:

    命令替换
    today=$(date +%Y-%m-%d)
    count=$(ls -1 | wc -l)
    echo "今天是 ${today},当前目录有 ${count} 个条目"

    脚本接收的命令行参数通过位置变量获取:

    变量含义
    $0脚本自身的名字
    $1 $2 …第 1、第 2 个参数
    $#参数个数
    $@所有参数(加引号 "$@" 时每个参数独立保留)
    $?上一条命令的退出码
    args.sh
    #!/usr/bin/env bash
    echo "脚本名:$0"
    echo "第一个参数:$1"
    echo "参数个数:$#"
    echo "全部参数:$@"
    带参数运行
    $ ./args.sh alma rocky

    Bash 用 if/elif/else/fi 做分支,测试条件推荐用 [[ ... ]](Bash 内建,比老式 [ ... ] 更安全、支持更多语法):

    if 条件判断
    if [[ "$1" == "start" ]]; then
    echo "启动中…"
    elif [[ "$1" == "stop" ]]; then
    echo "停止中…"
    else
    echo "用法:$0 {start|stop}"
    fi

    常用测试运算符:

    类别写法含义
    字符串[[ "$a" == "$b" ]]相等
    字符串[[ -z "$a" ]] / [[ -n "$a" ]]为空 / 非空
    数值[[ "$a" -eq "$b" ]]等于
    数值[[ "$a" -lt "$b" ]] / -gt / -le / -ge小于 / 大于 等
    文件[[ -f "$p" ]]是普通文件
    文件[[ -d "$p" ]]是目录
    文件[[ -e "$p" ]]路径存在

    用 &&(前一条成功才执行后一条)和 ||(前一条失败才执行后一条)做简短判断:

    逻辑短路
    mkdir -p /opt/app && echo "目录已就绪"
    ping -c1 example.com || echo "网络不通"

    匹配固定取值时,case 比一长串 elif 更清晰:

    case 分支
    case "$1" in
    start) echo "启动" ;;
    stop) echo "停止" ;;
    restart) echo "重启" ;;
    *) echo "用法:$0 {start|stop|restart}" ;;
    esac

    每个分支以 ;; 结尾,*) 是兜底分支,整体以 esac(case 倒写)结束。

    for 循环的几种形式
    # 遍历列表
    for distro in alma rocky centos; do
    echo "发行版:$distro"
    done
    # 数字范围
    for i in {1..5}; do
    echo "第 $i 次"
    done
    # 遍历文件(注意加引号,应对文件名含空格)
    for f in /etc/*.conf; do
    echo "配置文件:$f"
    done

    while 在条件为真时循环,until 则相反——在条件为假时循环,直到条件成立:

    while 与 until
    count=1
    while [[ $count -le 3 ]]; do
    echo "while: $count"
    count=$(( count + 1 ))
    done
    n=1
    until [[ $n -gt 3 ]]; do
    echo "until: $n"
    n=$(( n + 1 ))
    done

    用 break 提前跳出循环,用 continue 跳过本次进入下一轮:

    break 与 continue
    for i in {1..10}; do
    [[ $i -eq 3 ]] && continue # 跳过 3
    [[ $i -eq 6 ]] && break # 到 6 就停
    echo "$i"
    done

    函数把重复逻辑封装起来,参数同样用 $1、$2 获取:

    函数定义与传参
    log() {
    echo "[$(date +%T)] $1"
    }
    add() {
    local sum=$(( $1 + $2 )) # local 让变量只在函数内可见
    return 0 # return 返回退出码(0~255),不是返回值
    }
    log "开始计算"
    add 3 4
    echo "退出码:$?"

    每条命令执行后都有一个退出码:0 表示成功,非零表示失败。用 $? 读取上一条命令的退出码,用 exit N 让脚本以指定码退出:

    退出码
    if ! command -v git &>/dev/null; then
    echo "未安装 git" >&2
    exit 1
    fi
    exit 0

    退出码是脚本之间、脚本与 systemd / CI 之间沟通成败的标准方式,务必让脚本在失败时返回非零。

    几乎所有「正经」脚本都会在开头加上这一行:

    健壮脚本的开头
    #!/usr/bin/env bash
    set -euo pipefail

    逐项解释:

    • set -e:任何命令失败(返回非零)就立即终止脚本,避免错误被忽略后继续往下跑。
    • set -u:引用未定义的变量时报错退出,能及早发现拼写错误(如把 $name 写成 $naem)。
    • set -o pipefail:管道中只要有一环失败,整条管道就算失败。默认情况下管道退出码只看最后一条命令,容易漏掉前面的错误。

    三者合起来让脚本「快速失败」,而不是带着错误状态硬撑。

    读取输入
    read -r -p "请输入主机名:" hostname
    echo "你输入的是:$hostname"

    read -r 的 -r 表示不把反斜杠当转义符,应当总是带上,否则路径里的 \ 会被吃掉。

    整数运算用 $(( )):

    算术运算
    a=10
    b=3
    echo "和:$(( a + b ))"
    echo "商:$(( a / b ))" # 整数除法,结果为 3
    echo "余:$(( a % b ))"
    数组
    distros=(alma rocky centos)
    echo "第一个:${distros[0]}"
    echo "全部:${distros[@]}"
    echo "数量:${#distros[@]}"
    distros+=(fedora) # 追加元素
    for d in "${distros[@]}"; do
    echo "- $d"
    done

    用 <<EOF 把多行文本直接喂给命令,常用于生成配置文件:

    here-document
    cat > /tmp/info.txt <<EOF
    主机名:$(hostname)
    日期:$(date +%F)
    EOF

    EOF 只是约定俗成的结束标记,可换成任意词。若不希望展开其中的变量,把开头写成 <<'EOF'(给标记加单引号)。

    写法作用
    cmd > file标准输出覆盖写入 file
    cmd >> file标准输出追加到 file
    cmd 2> file标准错误写入 file
    cmd > file 2>&1标准输出和标准错误都写入 file
    cmd1 | cmd2把 cmd1 的输出作为 cmd2 的输入
    重定向与管道示例
    # 把正常输出和错误都记进日志
    ./deploy.sh > deploy.log 2>&1
    # 统计当前登录用户数
    who | wc -l
    # 只看错误,正常输出丢弃
    ./check.sh 2> errors.log > /dev/null

    2>&1 中的 2 是标准错误、1 是标准输出,含义是「把标准错误指向标准输出当前所在的地方」,所以它要写在 > file 之后才有效。

    让脚本逐行打印实际执行的命令,是排错最快的办法:

    开启调试运行
    $ bash -x ./myscript.sh

    也可以在脚本内部只对某一段开启调试:

    局部调试
    set -x # 开始打印执行轨迹
    risky_function
    set +x # 关闭

    set -x 会把每条展开后的命令前缀以 + 打印出来,能直观看到变量到底取到了什么值。

    ShellCheck 是脚本界的「拼写检查器」,能在运行前发现引号缺失、未定义变量、可疑写法等问题。它在 EPEL 源中提供:

    安装 ShellCheck(来自 EPEL)
    $ sudo dnf install epel-release
    $ sudo dnf install ShellCheck
    检查脚本
    $ shellcheck myscript.sh

    ShellCheck 会指出问题并附上编号(如 SC2086),照着建议改即可。养成「写完先 shellcheck 一遍」的习惯,能避开本页提到的绝大多数坑。

    实战一:带时间戳的目录备份脚本

    Section titled “实战一:带时间戳的目录备份脚本”

    把指定目录打包成带日期的 tar.gz,存到备份目录:

    /opt/scripts/backup-dir.sh
    #!/usr/bin/env bash
    #
    # backup-dir.sh —— 将指定目录打包备份,文件名带时间戳
    # 用法:./backup-dir.sh <源目录> [备份目录]
    #
    set -euo pipefail
    # 第一个参数是源目录;第二个可选,默认为 /var/backups
    src="${1:-}"
    dest="${2:-/var/backups}"
    # 参数检查
    if [[ -z "$src" ]]; then
    echo "用法:$0 <源目录> [备份目录]" >&2
    exit 1
    fi
    if [[ ! -d "$src" ]]; then
    echo "错误:源目录不存在:$src" >&2
    exit 1
    fi
    # 确保备份目录存在
    mkdir -p "$dest"
    # 用源目录名 + 时间戳组成归档文件名
    base=$(basename "$src")
    timestamp=$(date +%Y%m%d-%H%M%S)
    archive="${dest}/${base}-${timestamp}.tar.gz"
    echo "正在备份 ${src} -> ${archive}"
    # -C 切换到父目录,避免归档里出现绝对路径
    tar -czf "$archive" -C "$(dirname "$src")" "$base"
    echo "完成,大小:$(du -h "$archive" | cut -f1)"

    运行示例:

    运行备份脚本
    $ chmod +x /opt/scripts/backup-dir.sh
    $ ./backup-dir.sh /etc /var/backups

    检查一组 systemd 服务是否处于 active 状态,任意一个异常就以非零退出码结束——这样它能直接被监控系统或 CI 识别为失败:

    /opt/scripts/health-check.sh
    #!/usr/bin/env bash
    #
    # health-check.sh —— 检查关键服务是否正常运行
    # 退出码:0 = 全部正常;非 0 = 有服务异常(数字为异常服务数)
    #
    set -euo pipefail
    # 要检查的服务列表,按需修改
    services=(sshd chronyd firewalld)
    failed=0
    for svc in "${services[@]}"; do
    # is-active 在服务运行时返回 active 且退出码 0
    if systemctl is-active --quiet "$svc"; then
    echo "[OK] $svc 正在运行"
    else
    echo "[FAIL] $svc 未运行" >&2
    failed=$(( failed + 1 ))
    fi
    done
    if [[ "$failed" -eq 0 ]]; then
    echo "全部服务正常。"
    exit 0
    else
    echo "有 ${failed} 个服务异常。" >&2
    exit "$failed"
    fi

    运行示例:

    运行健康检查
    $ chmod +x /opt/scripts/health-check.sh
    $ ./health-check.sh
    $ echo "退出码:$?"

    systemctl is-active --quiet 在服务运行时退出码为 0,否则非 0,正好用作 if 的判断条件。脚本最后用异常服务数作为退出码,方便外部程序判断严重程度。

    报错 [: command not found 或 [[: command not found

    Section titled “报错 [: command not found 或 [[: command not found”

    方括号 [ 其实是一条命令,前后必须有空格。写成 if [$x -eq 1] 会被当成命令 [$x 而报错。正确写法是 if [[ "$x" -eq 1 ]],注意 [[、变量、运算符之间都留空格。

    变量没加引号导致空格或通配符出错

    Section titled “变量没加引号导致空格或通配符出错”

    未加引号的变量会发生「单词拆分」和通配符展开。文件名含空格时 cp $file /backup 会出错,$dir/* 在某些情况下会被意外展开。几乎所有变量引用都应写成 "$var"。 用 ShellCheck 能自动揪出这类问题(SC2086)。

    set -e 不会对所有失败都退出。处在 if 条件、&&、|| 左侧,或函数中被调用的命令,其失败可能不触发退出。如果你有意让某条命令允许失败,明确写 cmd || true;反过来,如果发现脚本「带病继续运行」,检查它是不是恰好落在这些豁免位置。

    bad interpreter: No such file or directory(CRLF 换行)

    Section titled “bad interpreter: No such file or directory(CRLF 换行)”

    在 Windows 上编辑脚本、或从某些编辑器复制时,行尾可能是 CRLF(\r\n)。shebang 行末尾多出的 \r 会让系统去找不存在的解释器 /usr/bin/env bash\r,于是报 bad interpreter。修复方法:

    转换换行符
    $ sudo dnf install dos2unix
    $ dos2unix myscript.sh

    或用 sed -i 's/\r$//' myscript.sh。养成在 Linux 上编辑脚本、并配置编辑器使用 LF 换行的习惯。