脚本的严格模式

⚙️ 说在前面!

在 Bash 脚本中,严格模式(通常指 set -euo pipefail)就像是给你的脚本安装了“安全气囊”和“自动刹车”。虽然它极大地提升了稳定性,但也会在某些场景下显得“过于敏感”。

以下是基于 Google 编程风格自动化运维实践 的优缺点深度对比:


💓 优点:为什么要推崇严格模式?

1. 防止“灾难性”事故(set -u)

未定义变量是脚本中最常见的隐患。

  • 普通模式:执行 rm -rf "/tmp/${my_var}"/*,如果变量名拼错了(如 ${my_vaar}),脚本会静默执行 rm -rf /tmp/*,导致执行了预期之外的错误的数据清空操作。

  • 严格模式:脚本会立即报错 my_vaar: unbound variable 并停止执行,从而保护系统。

2. 避免“带伤运行”(set -e)

  • 普通模式:脚本会忽略中间步骤的失败。例如:

    1. cd /non_existent_dir ( cd 某个目录失败)

    2. rm -rf * (而该命令又紧接着执行!这会导致删除了当前目录的几乎所有文件,而不是预期的 cd 到那个目录,删除那个目录里边的文件)

  • 严格模式:在 cd 失败的瞬间,脚本会立刻“熔断”,保护后续操作不发生。

3. 暴露管道隐患(set -o pipefail)

默认情况下,Bash 只关心管道中 最后一个 命令的返回值。

  • 普通模式grep "error" log.txt | sort,即使 log.txt 不存在,只要 sort 成功执行,脚本就认为整行成功。

  • 严格模式:只要管道中任何一环出错,整行都会被判定为失败。


💔 缺点:它会带来哪些麻烦?

1. 对“预期内失败”不够友好

有些命令返回非零值是正常的(如 grep 没搜到内容,或 diff 发现文件不同)。开启严格模式后,你必须为这些命令编写显式的“绕过”逻辑(如使用 if 结构),这增加了代码量。

2. 环境变量处理较繁琐

如果脚本需要读取一些可选的环境变量,set -u 会导致脚本在变量缺失时崩溃。你不得不使用 ${VAR:-default} 这种繁琐的写法来设置默认值。

3. 兼容性挑战

如果你在脚本中 source(引入)了一些第三方的旧脚本或复杂的库,这些库如果没有按照严格模式编写,可能会直接导致你的主脚本在初始化时崩溃,而直接退出执行,因为 source 的本质是引入其他脚本的逻辑到当前shell环境,它不会开启子shell进程,因此它遵循当前shell的严格模式,一旦这个被引入的脚本有任意命令出错都会导致当前shell进程终止退出。


📇 总结对比表

特性 严格模式 (-euo pipefail) 普通模式 (默认)
错误容忍度 零容忍:发现错误立即终止脚本 高度容忍:忽略错误继续执行
代码量 略多(需处理所有潜在失败) 极少(只写业务逻辑)
调试难度 :报错点即是故障点 :错误可能在运行很久后才爆发
适用场景 生产环境、系统运维、备份脚本 简单的单行临时测试、交互式操作

💡 最后的科学建议

在管理你的 1G 内存/50G 磁盘 的 AlmaLinux 运维项目中:

  1. 管理脚本(如入口脚本)必须开启严格模式。因为这些脚本涉及 SSH 连接、目录删除和进程重启,任何小错都可能导致自动化流程失去控制。

  2. 简单查询脚本:可以灵活处理。

  3. 避坑指南:如果你在严格模式下感到痛苦,通常是因为 逻辑判断不够显式 。记住 Google 的原则: 通过 if 显式捕获每一个可能返回非零值的命令

  4. 不要抱怨:由于开启了严格模式,在初期你可能会频繁地看到脚本报错终止执行, 而这正是 Bash 在强行逼着你写更严谨、健壮、安全的代码 ,久而久之,你的脚本逻辑会变得更强壮,你的 Bash 编程能力也会得到很大提升。

💡 如何科学使用 Bash

想要写出优秀的 Bash 脚本,离不开以下准则:

  1. 启用严格模式 :使用严格模式,让 Bash 全程保护着你,降低误操作带来的搞坏系统的风险。

  2. 遵循 Google 风格指南 :该指南旨在让你优雅、安全、愉快地入门 Bash 脚本编程。

  3. 全程启用 Shellcheck 检查Shellcheck 能识别脚本中大部分语法错误或风险点,让你的脚本逻辑更加健壮,为你使用 Bash 脚本又添加一道安全防线。

  4. 阅读优秀的 Bash 项目 :阅读优秀的脚本能显著提升 Bash 编程能力,同时也能规范你的脚本风格。

  5. 一颗细腻的心 :拥有一个细腻缜密的心思,又为你的脚本添加一道人体安全防线。

  6. 对 Linux 系统数据操作需时刻保持 敬畏之情 :数据无价,操作需谨慎。

如果你不了解这些东西,认为 Bash 很简单,只是浅显地观摩网上教程,然后随意编写脚本,那么大概率你会制造出来一坨屎一样的东西。请记住:"优秀、健壮且安全的代码风格无关脚本逻辑的复杂程度!"

试想一下,如果一个东西本身就是一坨屎,你会关心这坨屎是大还是小吗?很明显,它只会让你感到恶心,无论大小,可别强调你的脚本逻辑写得多么复杂且智能,也别强调你的脚本逻辑多么简单而去无视这些安全规范,因为最后吃到 “灾难性” 事故回旋镖的大概率就是你自己。

既然你已经权衡了优缺点,是否需要我为你整理一个针对严格模式的“万能异常处理模板”,让你在享受安全性的同时,不再被频繁的报错打断?

“万能异常处理模板” 见后续小结 Google Gemini AI 提供......

相关规范资源地址:

Google风格指南Google风格指南(中文版)Bash脚本天花板(airgeddon)shellcheck项目

AstroNvim集成shellcheck

AstroNvim安装

集成社区 Bash 插件:AstroCommunity

#进入个人配置目录
╰─ cd ~/.config/nvim/lua

#编辑社区插件配置
➜ cat community.lua
-- if true then return {} end -- WARN: REMOVE THIS LINE TO ACTIVATE THIS FILE

-- AstroCommunity: import any community modules here
-- We import this file in `lazy_setup.lua` before the `plugins/` folder.
-- This guarantees that the specs are processed before any user plugins.

---@type LazySpec
return {
  "AstroNvim/astrocommunity",
  { import = "astrocommunity.pack.lua" },
  { import = "astrocommunity.pack.bash" },
  { import = "astrocommunity.colorscheme.catppuccin" },
  { import = "astrocommunity.color.transparent-nvim" },
  -- import/override with your plugins folder
}

#使得AstroNvim集成shellcheck并使其正常工作
#进入个人配置目录
╰─ cd ~/.config/nvim/lua/plugins

#编辑none-ls插件配置文件
➜ cat none-ls.lua
-- if true then return {} end -- WARN: REMOVE THIS LINE TO ACTIVATE THIS FILE

-- Customize None-ls sources

---@type LazySpec
return {
  "nvimtools/none-ls.nvim",
  dependencies = {
    "gbprod/none-ls-shellcheck.nvim",
  },
  opts = function(_, config)
    -- config variable is the default configuration table for the setup function call
    -- local null_ls = require "null-ls"

    -- Check supported formatters and linters
    -- https://github.com/nvimtools/none-ls.nvim/tree/main/lua/null-ls/builtins/formatting
    -- https://github.com/nvimtools/none-ls.nvim/tree/main/lua/null-ls/builtins/diagnostics
    config.sources = {
      -- Set a formatter
      -- null_ls.builtins.formatting.stylua,
      -- null_ls.builtins.formatting.prettier,
      require "none-ls-shellcheck.diagnostics",
      require "none-ls-shellcheck.code_actions",
    }
    return config -- return final config table
  end,
}

⚙️ 使用严格模式

在编写高质量的 Bash 脚本时,严格模式(Strict Mode) 虽然不是语法强制要求的,但在生产环境下几乎是 必须的 。它能让你在脚本出错时立即停止,而不是带着错误的中间结果继续运行,从而避免破坏系统数据。

1. 什么是 Bash 严格模式?

最常用的严格模式被称为 Unofficial Bash Strict Mode。你可以通过在脚本开头添加以下代码来设置:

# 设置严格模式
set -o errexit   # 等同于 set -e:遇到错误(非零状态码)立即退出
set -o nounset   # 等同于 set -u:遇到未定义的变量名时报错并退出
set -o pipefail  # 管道中只要有一个子命令失败,整个管道就视为失败
IFS=$'\n\t'      # 修改字段分隔符,减少空格导致的解析错误(可选),因为有需要空格字符串变量作为循环条件的情况,空格字符串变量还能双引号引用作为整体处理

2. 为什么要开启严格模式?

  • 防止隐式错误蔓延:例如 rm -rf "/tmp/${my_var}"/*,如果 ${my_var} 未定义且未开启 nounset,命令会变成 rm -rf /tmp//*,后果不堪设想。

  • 调试效率高:脚本会在出错的那一行立刻停下,配合 xtraceset -x)可以快速定位问题。


3. 如何优雅地绕过严格模式?

在开启 set -e 后,任何返回非零状态码的命令都会导致脚本终止。如果你预知某个命令可能会失败(例如 grep 没搜到内容,或尝试删除一个可能不存在的文件),需要使用“防御性”写法。

根据 Google Shell Style Guide 的原则,代码应当具备高可读性,避免晦涩的简写(如 || true 虽然常用,但在严格风格中不够显式)。

方案 A:使用 if 语句(最推荐,最符合 Google 风格)

Google 风格指南建议通过结构化的判断来处理逻辑。if 语句会自动处理命令的状态码,不会触发 errexit

# 检查进程是否存在,pgrep 找不到目标会返回 1,但 if 会捕获它
if pgrep "my_process" >/dev/null 2>&1; then
  readonly process_exists=true
else
  readonly process_exists=false
fi

方案 B:显式错误忽略(即使不简写,也不推荐,不太符合 Google 风格指南)

如果你只是想简单地忽略错误,不要使用 || : 这种简写,使用更清晰的表达方式:

# 尝试删除临时文件,即使文件不存在也不报错退出
rm tmp_file.txt || true

方案 C:使用临时关闭模式(针对复杂逻辑块)

如果你有一整块代码逻辑已知是不稳定的,可以局部关闭严格模式。

# 在简单线性脚本中启用/关闭严格模式
================================================================================
# 暂时关闭错误退出
set +o errexit

# 执行一系列可能失败的旧逻辑
maybe_fail_command_1
maybe_fail_command_2

# 重新开启错误退出
set -o errexit
================================================================================

# 在函数中启用/关闭严格模式
================================================================================
#!/bin/bash
set -o errexit

# 这是一个已知的“容错”函数
function safe_failure_function() {
  # 1. 局部关闭严格模式
  set +o errexit

  echo "正在尝试一些可能会失败的操作..."
  command_that_might_fail_1
  command_that_might_fail_2

  # 2. 重新开启严格模式,确保不影响后续代码
  set -o errexit
}

function main() {
  safe_failure_function
  echo "函数执行完毕,严格模式已重新生效。"
}

main "$@"
================================================================================

4.综合案例:Google 风格的绕过代码

以下是一个将严格模式与异常处理结合的完整示例:

#!/bin/bash
# 遵循 Google 编程风格的严格脚本示例

set -o errexit
set -o nounset
set -o pipefail

function main() {
  local -r target_dir="/tmp/ansible_logs"

  # 场景 1:创建目录,即使已存在也不报错
  # Google 风格建议先做检查,或确保命令本身幂等
  if [[ ! -d "${target_dir}" ]]; then
    mkdir "${target_dir}"
  fi

  echo "✅ 看到我说明【场景 1】 科学绕过了严格模式"

  # 场景 2:允许 grep 失败(找不到匹配项时不退出脚本)
  # 使用显式的逻辑判断,不要直接运行 grep
  local search_result
  # 直接将赋值放在 if 判断中
  # 欠佳代码:执行了两次 grep 第一次是为了判断状态,第二次是为了获取内容,额外的性能开销
  if grep "ERROR" /var/log/syslog >/dev/null 2>&1; then
    search_result="$(grep "ERROR" /var/log/syslog)"
    echo "❌ Found errors: ${search_result}"
  else
    search_result=""
    echo "✅ No errors found."
  fi

  # 科学改写:只执行一次 grep,同时捕获结果和状态
  local search_result
  if search_result="$(grep "ERROR" /var/log/syslog 2>/dev/null)"; then
    echo "❌ Found errors: ${search_result}"
  else
    search_result=""
    echo "✅ No errors found."
  fi

  if [[ -n "${search_result}" ]]; then
    echo "❌ Found errors."
  fi

  echo "✅ 看到我说明【场景 2】 科学绕过了严格模式"

  # 场景 3:删除可能不存在的文件
  # 显式检查文件是否存在,避免 rm 报错
  if [[ -f "non_existent_file.tmp" ]]; then
    rm "non_existent_file.tmp"
  fi

  # 使用 -f 参数,即使文件不存在,rm 也会返回状态码 0
  rm -f "non_existent_file.tmp"

  # 尝试删除并捕获状态,即使失败也执行后续逻辑(不简写为 || true)
  if ! rm "non_existent_file.tmp"; then
    # 这里可以留空或记录日志,重点是 if 捕获了非零状态码,防止了脚本退出
    :
  fi

  echo "✅ 看到我说明【场景 3】 科学绕过了严格模式"

  # 场景 4:捕获可能失败的命令退出状态码
  # 你不能裸奔执行该可能出错的命令,因为严格模式
  # 如果命令 some_error_command 出错,脚本退出
  # 该退出码捕获命令 command_status=$? 永远不会执行
  # 你将永远获取不到错误状态码
  # local command_status
  # some_error_command
  # command_status=$?
  # 科学改写:
  local command_status
  if some_error_command >/dev/null 2>&1; then
    command_status=0
  else
    command_status=$?
  fi

  echo "该命令 some_error_command 退出码为:${command_status}"

  # WARN: 但不能这样改写:
  # 因为 ! 逻辑非,会在命令 some_error_command 失败时进入真分支
  # 然而此时这条语句的 $? 已经被 ! 悄悄地置为 0 了
  # 所以导致最终获取的退出状态码始终是 0
  # 最终影响后续所有基于 command_status 的逻辑判断
  local command_status
  if ! some_error_command >/dev/null 2>&1; then
    command_status=$?
  else
    command_status=0
  fi

  echo "该命令 some_error_command 退出码为:${command_status}"

  echo "✅ 看到我说明【场景 4】 科学绕过了严格模式"
}

main "$@"

总结建议

  1. 始终开启 set -euo pipefail:这对管理你的 Ansible 管理机(AlmaLinux)环境非常有帮助。

  2. 避免简写:在脚本中使用 || true 是可以接受的,但应尽量使用 if 语句来显式处理逻辑,遵循 Google 风格指南。

  3. 变量保护:始终使用 ${var} 并在必要时加双引号,配合 set -u 效果最佳。

你的 Ansible 项目已经非常成熟了,是否需要我为你提供一个“严格模式”下的常用 Bash 工具函数库(例如带颜色的日志输出、环境检查函数),方便你在管理机上更安全地运行维护脚本?

⚙️ 科学模板

这个模板遵循 Google Shell Style Guide 的核心原则:使用 main 函数封装、常量大写命名、变量局部化(local)、并且使用显式的逻辑判断来处理严格模式下的“预期内失败”。

在你的 AlmaLinux 管理机上,你可以将此作为所有运维脚本的基准。


Google 风格:Bash 严格模式万能模板

#!/bin/bash
#
# 脚本描述:Ansible 运维自动化入口模板
# 遵循 Google Shell Style Guide (https://google.github.io/styleguide/shellguide.html)

# 1. 开启严格模式 (Unofficial Bash Strict Mode)
set -o errexit   # 遇到错误立即退出
set -o nounset   # 使用未定义变量立即报错
set -o pipefail  # 管道中任一命令失败则整行失败
# 注意:Google 风格不强制要求修改 IFS,但在处理文件路径时建议设置为 $'\n\t'

# 2. 定义全局常量 (只读)
readonly LOG_FILE="/var/log/ops_deploy.log"
readonly SCRIPT_NAME="${0##*/}"

# 3. 工具函数:格式化输出
function log_info() {
  echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] [INFO] $*"
}

function log_error() {
  echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] [ERROR] $*" >&2
}

# 4. 异常处理函数:绕过严格模式的范例
# 每一个已知可能失败的命令,都必须使用 if 显式处理
function perform_tasks() {
  local target_dir="/tmp/ansible_cache"
  
  log_info "正在初始化环境..."

  # 场景 A:幂等操作(删除可能不存在的文件)
  # 不使用 || true,而是使用 -f 参数或 if 判断
  rm -f "${target_dir}/old_cache.tmp"

  # 场景 B:捕获可能不存在的搜索结果 (grep)
  # Google 风格建议:直接在 if 中赋值并判断
  local error_count
  if ! error_count="$(grep -c "ERROR" "${LOG_FILE}" 2>/dev/null)"; then
    error_count=0
  fi
  log_info "当前日志错误数: ${error_count}"

  # 场景 C:执行必须成功的外部脚本/命令
  # 严格模式会自动处理,无需额外操作;若需自定义报错,可使用以下方式
  # 使用 if 结构代替短路逻辑,增强可读性
  if ! ansible --version >/dev/null 2>&1; then
    log_error "未检测到 Ansible 环境,请检查虚拟环境是否激活。"
    return 1
  fi

  # 场景 D:整个逻辑块绕过(临时关闭严格模式)
  # 仅在处理无法控制返回码的第三方遗留代码时使用
  set +o errexit
  log_info "开始执行不稳定逻辑块..."
  # ... 执行一些不稳定的命令 ...
  set -o errexit
}

# 5. 主函数入口
function main() {
  log_info "脚本 ${SCRIPT_NAME} 启动。"

  # 使用 if 包裹函数调用,是捕获函数内部整体状态的最佳实践
  if ! perform_tasks; then
    log_error "任务执行失败。"
    exit 1
  fi

  log_info "任务顺利完成。"
}

# 6. 安全启动:确保脚本只有在直接运行时才执行 main
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  main "$@"
fi

模板核心逻辑说明

模块 作用 Google 风格要点
set -o 严格模式设置 必须写在脚本顶部,使用长参数名(如 errexit)增加可读性。
local -r 局部只读变量 在函数内部,尽量使用 local 限制作用域,使用 -r 确保变量不被篡改。
main "$@" 入口控制 避免全局污染。通过判断 BASH_SOURCE,使脚本可以被 source 而不立即触发执行。
2>/dev/null 错误重定向 保持终端整洁。对于“预期内失败”的命令,应静默其标准错误输出到黑洞。

为什么这个模板适合你的 1G/50G 服务器?

  1. 内存安全:通过 local 变量和函数封装,内存会在函数执行完后及时释放。

  2. 路径安全set -u 确保你不会因为拼错路径而误删 50G 硬盘里的重要数据。

  3. 可追踪性:规范的日志格式让你在资源受限的环境下,能快速通过 tail -f 监控部署进度。

这个模板现在可以作为你管理机上所有运维操作的“底座”。你想让我把这个逻辑集成到你现有的 site.yml 对应的启动脚本(例如 run_ansible.sh)中吗?

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐