Bash 零基础教程

Bash 错误处理与流程控制

在编写脚本时,尤其是系统管理相关的脚本,错误处理(Error handling)是打造健壮且可靠程序的关键环节。如果没有妥善的错误检查机制,脚本可能会在发生错误时“静默失败”,进而导致数据损坏,或者让系统处于不一致的危险状态。

通过引入控制流语句,你可以主动探测潜在的问题并采取相应的措施,例如:记录错误日志、显示提示信息,或者让脚本优雅地安全退出。本章将重点讲解如何使用 ifelseelifcase 语句,将错误处理机制无缝融入到我们的系统管理实战案例中。

1. 使用 if、then、else 和 elif 实现错误检查

if 语句是 Bash 中进行错误检查的基石。它允许脚本根据条件的真假(true 或 false)来执行不同的代码块。你可以利用它来检查命令的退出状态、验证用户输入,或者确认文件是否存在。

1.1 检查命令退出状态

在 Bash 中,每一个执行完毕的命令都会返回一个退出状态码(exit status)。这是一个数字代码,用来指示命令是否执行成功。通常情况下,状态码 0 代表成功,而任何非零值(non-zero)都代表失败。特殊变量 $? 专门用于保存上一条执行命令的退出状态码。

代码示例:

#!/bin/bash
# 尝试创建一个新目录
mkdir /path/to/new/directory

# 检查 mkdir 命令的退出状态
if [ $? -eq 0 ]; then
  echo "目录创建成功。"
else
  echo "创建目录失败。"
fi

在这个例子中,mkdir 命令尝试创建一个目录。紧接着,if 语句检查 $? 的值。如果它等于 0(-eq 0),则打印成功消息;否则,显示错误消息。

1.2 验证用户输入

用户输入往往是导致脚本报错的常见源头。在处理用户输入的数据之前,验证其是否符合脚本的要求是至关重要的。

代码示例:

#!/bin/bash
# 提示用户输入一个数字
read -p "请输入一个正整数: " num

# 检查输入是否为正整数
if ! [[ "$num" =~ ^[0-9]+$ ]] || [ "$num" -le 0 ]; then
  echo "输入无效。请输入一个正整数。"
  exit 1
else
  echo "你输入了: $num"
fi

这个脚本要求用户输入一个正整数。if 语句首先使用正则表达式 (^[0-9]+$) 来检查输入是否仅由数字组成,随后检查该整数是否大于 0。如果任意一个条件不满足(false),脚本将打印错误信息,并通过 exit 1 强制退出。

1.3 检查文件存在性与权限

许多脚本的运行都高度依赖于文件和目录。在尝试读取或修改它们之前,必须先检查这些文件是否存在,以及当前用户是否拥有正确的权限。

代码示例:

#!/bin/bash
# 指定要检查的文件路径
file="/path/to/my/file.txt"

# 检查文件是否存在且具有可读权限
if [ ! -f "$file" ]; then
  echo "错误: 文件 '$file' 不存在。"
  exit 1
elif [ ! -r "$file" ]; then
  echo "错误: 文件 '$file' 不可读。"
  exit 1
else
  echo "文件 '$file' 存在并且具有可读权限。"
fi

此脚本分别检查了指定文件是否存在 (-f) 以及是否可读 (-r)。如果其中任何一个条件为假,都会显示相应的错误提示并退出脚本。elif(else if 的缩写)语句允许我们按顺序对多个条件进行链式检查。

2. 使用 case 语句实现错误检查

当一个变量或表达式可能出现多种不同的值时,case 语句提供了一种非常优雅的处理方式。虽然 if 语句非常适合处理简单的二元条件,但在应对多种互斥的可能性(例如处理不同的错误代码)时,case 语句通常更具可读性和可维护性。

2.1 处理不同的错误代码

假设某个命令可能会返回多种不同的错误代码,每种代码代表一种特定的失败类型。我们可以使用 case 语句来针对性地处理每一种错误。

代码示例:

#!/bin/bash
# 模拟一个会返回不同错误代码的命令
simulate_command() {
  # 随机返回 0、1 或 2
  exit $((RANDOM % 3))
}

simulate_command

# 检查上一条命令的退出状态
case $? in
  0)
    echo "命令执行成功。"
    ;;
  1)
    echo "错误: 无效的参数。"
    ;;
  2)
    echo "错误: 找不到文件。"
    ;;
  *)
    echo "错误: 未知错误。"
    ;;
esac

在这个例子中,simulate_command 函数会随机返回 0、1 或 2 作为退出状态。case 语句捕获 $? 的值,并为每种可能的错误代码打印不同的提示。最后的 *) 模式充当默认分支(类似于其他语言中的 default),用于捕获任何未预料到的错误代码。

2.2 处理不同的用户选择

case 语句也极其适合用来处理用户在菜单或提示中做出的不同选择。

代码示例:

#!/bin/bash
read -p "请选择一个选项 (a, b, 或 c): " choice

case "$choice" in
  a)
    echo "你选择了选项 a。"
    ;;
  b)
    echo "你选择了选项 b。"
    ;;
  c)
    echo "你选择了选项 c。"
    ;;
  *)
    echo "无效的选择。请在 a、b 或 c 中进行选择。"
    exit 1
    ;;
esac

该脚本提示用户选择一个选项,随后 case 语句根据用户的具体输入执行对应的代码块。如果输入不在预期范围内,则触发默认分支,显示错误并退出。

3. 将错误检查应用于系统管理脚本

让我们回顾一下之前课程中的系统管理脚本,并为其添加错误检查机制,使其变得更加坚不可摧。假设该脚本负责执行文件备份、检查磁盘空间和监控系统资源等任务。这里我们将重点放在为备份功能添加错误处理。

原始(简化版)备份函数:

backup_files() {
  # 定义源目录和备份目标目录
  source_dir="/path/to/source/directory"
  backup_dir="/path/to/backup/directory"
  
  # 如果备份目录不存在则创建它
  mkdir -p "$backup_dir"
  
  # 将文件复制到备份目录
  cp -r "$source_dir"/* "$backup_dir"
}

添加了错误检查的增强版备份函数:

backup_files() {
  # 定义源目录和备份目标目录
  source_dir="/path/to/source/directory"
  backup_dir="/path/to/backup/directory"
  
  # 1. 检查源目录是否存在
  if [ ! -d "$source_dir" ]; then
    echo "错误: 源目录 '$source_dir' 不存在。"
    return 1
  fi
  
  # 2. 如果备份目录不存在则创建它,并检查是否创建成功
  mkdir -p "$backup_dir"
  if [ $? -ne 0 ]; then
    echo "错误: 无法创建备份目录 '$backup_dir'。"
    return 1
  fi
  
  # 3. 将文件复制到备份目录,并检查是否复制成功
  cp -r "$source_dir"/* "$backup_dir"
  if [ $? -ne 0 ]; then
    echo "错误: 文件复制到备份目录失败。"
    return 1
  fi
  
  echo "文件已成功备份到 '$backup_dir'。"
  return 0
}

# 实际调用示例:
backup_files

if [ $? -eq 0 ]; then
  echo "备份流程顺利完成。"
else
  echo "备份流程失败。"
fi

在这个增强版本中,我们添加了以下关键的防御性检查:

  1. 源目录存在性: 使用 [ ! -d "$source_dir" ] 检查源目录是否真实存在。如果不存在,打印错误并返回非零状态码。
  2. 备份目录创建: 检查 mkdir -p 命令的退出状态。如果目录创建失败(可能是因为权限不足),打印错误并返回非零状态码。
  3. 文件复制过程: 检查 cp -r 命令的退出状态。如果复制过程中断或失败,打印错误并返回非零状态码。

注意,我们在函数内部使用了 return 1 语句,这会使函数自身以非零错误码退出(代表失败)。随后,脚本的主体部分通过检查 backup_files 函数执行后的 $? 状态,来决定打印最终的成功或失败提示。