Bash 零基础教程

Bash 函数

随着你的 Bash 脚本变得越来越复杂,仅仅依靠顺序执行的命令流会使得代码变得臃肿且难以维护。就像在其他编程语言中一样,Bash 中的函数 (Functions) 提供了一种封装可重用代码块的方法,极大地提升了脚本的模块化程度、可读性和可维护性。

与其在脚本的多个地方重复编写相同的一串命令,不如将它们定义为一个函数,并在需要时随时调用它。这不仅能让你的脚本更加整洁、易于调试,还能让你把庞大复杂的任务拆解成更小、更易于管理的子任务。

掌握如何定义和调用函数,是你迈向编写更健壮、更专业的 Bash 脚本的重要一步,这也为我们逐步构建更高级的系统自动化运维解决方案铺平了道路。

1. 在 Bash 中定义函数

在 Bash 中,函数本质上是组合在一起并赋予了一个名称的命令块。当你“调用(call)”这个名称时,该代码块内的所有命令都会被执行。

Bash 提供了两种定义函数的语法,这两种语法都被广泛接受,并且执行效果完全相同。

1.1 语法 1:使用 function 关键字

这是定义函数最明确、最直观的方式,因为它直接使用了 function 关键字。

function my_function_name {
  # 当调用 my_function_name 时要执行的命令
  echo "这里是 my_function_name 函数的内部。"
  ls -l
}
  • function 关键字: 明确声明紧随其后的是一个函数定义。
  • my_function_name: 这是你为函数指定的名称。名称应该具有描述性,并遵循标准的 Bash 命名规范(通常小写,使用下划线分隔单词,类似于变量名,例如 check_statusbackup_files)。
  • { ... }: 花括号包含了函数的主体 (body),即函数被调用时将要执行的所有命令。

1.2 语法 2:使用标准括号 ()

这种语法在许多其他编程语言(如 C, Java, JavaScript)中非常常见,在 Bash 中同样有效且非常流行。

another_function_name () {
  # 当调用 another_function_name 时要执行的命令
  echo "这里是 another_function_name 函数的内部。"
  pwd
}
  • another_function_name: 函数的名称。
  • (): 紧跟在函数名后面的空括号表明这是一个函数。虽然在其他语言中括号通常用来存放参数,但在 Bash 的这种语法风格中,它们主要作为函数声明语法的一部分。(注意:在 Bash 中,参数是在调用函数时跟在函数名后面传递的,而不是写在定义时的这些括号里)。
  • { ... }: 同样,花括号包含了函数的主体。

1.3 定义函数的重要注意事项

  • 定义位置 (Placement): 函数必须在被调用之前进行定义。Bash 是按顺序自上而下执行脚本的,如果你尝试在函数定义被解析之前调用它,Bash 会报出“command not found(找不到命令)”的错误。良好的编程习惯是:将所有函数定义放在脚本的开头,或者将它们放在一个单独的文件中,然后在脚本开头使用 source 命令引入。
  • 命名规范 (Naming Conventions): 选择清晰、具有描述性的名称。避免使用与现有的 Bash 内置命令或别名(aliases)冲突的名称,以免产生意外行为(例如,不要把函数命名为 lscd)。
  • 可读性 (Readability): 保持函数主体专注于一个单一的逻辑任务。如果一个函数变得太长或者执行了多个不相关的任务,请考虑将其拆分为更小、更专业的多个函数。

2. 在 Bash 中调用函数

一旦函数被定义好,调用它就非常简单了。你只需要像使用任何普通的 Bash 命令一样,直接写出它的名字即可。

# 1. 定义一个函数
function greet_user {
  echo "你好!这里是 greet_user 函数!"
  echo "今天是 $(date +%F)"
}

# 2. 调用该函数
greet_user

greet_user 被调用时,Shell 会跳转到该函数的定义处,执行花括号内的所有命令,执行完毕后,再返回到脚本中调用该函数的位置,继续往下执行后续代码。

让我们用一个稍微复杂一点的例子来演示:

#!/bin/bash

# --- 函数定义区域 ---

# 用于显示分隔线的函数
function display_separator {
  echo "----------------------------------------"
}

# 用于检查基本系统运行时间和负载的函数
check_system_info () {
  echo "正在收集系统信息..."
  uptime
}

# 用于以长格式列出当前目录内容的函数
list_current_directory () {
  echo "列出当前目录的内容:"
  ls -lh
}

# --- 主脚本执行区域 ---

echo "开始生成系统状态报告..."

display_separator
check_system_info
display_separator
list_current_directory
display_separator

echo "系统状态报告生成完毕。"

在这个脚本中:

  1. 我们混合使用了两种语法定义了 display_separatorcheck_system_infolist_current_directory 三个函数。
  2. 在“主脚本执行区域”,我们按顺序调用了这些函数。这使得脚本的主体逻辑非常清晰,输出也井然有序。

3. 实用案例演示

让我们把函数应用到与我们系统管理实战相关的场景中。

3.1 案例 1:创建标准化的日志记录函数

假设我们的系统管理脚本需要记录各种事件。与其在脚本中到处重复编写 echo "时间戳: ... 消息: ...",我们可以使用一个函数来统一处理。

#!/bin/bash

# 定义日志文件路径
# 在实际场景中,LOG_FILE 可能是一个全局变量。
LOG_FILE="./my_admin_script.log"

# 用于记录脚本开始信息的函数
function log_script_start {
  # 获取当前时间戳
  TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
  # 打印消息到控制台,并追加到日志文件
  echo "[$TIMESTAMP] INFO: 脚本初始化完成。" | tee -a "$LOG_FILE"
}

# 用于记录脚本结束信息的函数
function log_script_end {
  TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
  echo "[$TIMESTAMP] INFO: 脚本执行完毕。" | tee -a "$LOG_FILE"
}

# 为了演示,确保日志文件存在并可写
touch "$LOG_FILE"
chmod 664 "$LOG_FILE"

# --- 主脚本逻辑 ---
echo "管理员脚本已启动。"
log_script_start

# 模拟执行一些任务
echo "正在执行任务 A..."
sleep 1
echo "任务 A 完成。"

echo "正在执行任务 B..."
sleep 1
echo "任务 B 完成。"

log_script_end
echo "管理员脚本运行结束。"

# 显示日志文件最后几条记录
echo -e "\n--- $LOG_FILE 的最新记录 ---"
tail "$LOG_FILE"

原理解析:

  • 我们定义了两个函数:log_script_startlog_script_end
  • 每个函数内部都封装了生成带时间戳的特定日志条目的逻辑。
  • tee -a "$LOG_FILE" 命令同时将输出发送到标准输出(屏幕)和追加到指定的日志文件中。
  • 通过在主程序开头和结尾调用这两个函数,我们确保了脚本执行记录拥有统一、规范的格式。

3.2 案例 2:检查服务状态(占位符演示)

在系统管理中,我们经常需要检查各种服务(如 Web 服务器、数据库)的状态。为了演示函数的结构作用,我们先创建几个“占位符 (placeholder)”函数,它们目前只打印提示信息,后续我们会将真正的检查逻辑填补进去。

#!/bin/bash

# 检查虚拟的 'nginx' 服务状态的函数
function check_nginx_status {
  echo "正在检查 'nginx' Web 服务器的状态..."
  # 这里未来会替换为真实的命令,例如: systemctl is-active nginx
  echo "Nginx 状态: (模拟为正在运行 running)"
}

# 检查虚拟的 'mysql' 服务状态的函数
check_mysql_status () {
  echo "正在检查 'mysql' 数据库服务器的状态..."
  # 这里未来会替换为真实的命令,例如: systemctl is-active mysql
  echo "MySQL 状态: (模拟为已停止 stopped)"
}

# --- 主脚本逻辑 ---
echo "开始每日服务健康巡检..."
check_nginx_status
echo "" # 打印空行,让输出更易读
check_mysql_status
echo ""
echo "每日服务健康巡检完成。"

原理解析:
即便 check_nginx_statuscheck_mysql_status 目前只是用 echo 模拟操作,但它们展示了如何将相关的命令分组到一个有意义的名称下。这使得主脚本的流程一目了然:“开始巡检 -> 查 Nginx -> 查 MySQL -> 结束”。

3.3 案例 3:创建并验证备份目录

在进行文件备份前,确保备份目录存在是一个常见任务。这个例子结合了之前学过的创建目录 (mkdir)、变量以及 if 条件控制语句。

#!/bin/bash

# 定义备份目录路径变量
BACKUP_ROOT="/tmp/my_daily_backups" 

# 用于确保备份目录存在的函数
function prepare_backup_directory {
  echo "正在准备备份目录: $BACKUP_ROOT"
  
  if [ -d "$BACKUP_ROOT" ]; then
    echo "目录 '$BACKUP_ROOT' 已经存在。"
  else
    echo "目录 '$BACKUP_ROOT' 不存在。正在创建..."
    mkdir -p "$BACKUP_ROOT" # -p 选项:如果父目录不存在也会一并创建
    
    # 检查上一条 mkdir 命令的退出状态
    if [ $? -eq 0 ]; then 
      echo "目录 '$BACKUP_ROOT' 创建成功。"
    else
      echo "错误: 无法创建目录 '$BACKUP_ROOT'。"
    fi
  fi
}

# --- 主脚本执行 ---
echo "开始备份流程..."

prepare_backup_directory # 调用函数

# 模拟复制备份文件
echo "正在将备份文件复制到 '$BACKUP_ROOT'..."
touch "$BACKUP_ROOT/file_A_$(date +%F).log"
touch "$BACKUP_ROOT/data_B_$(date +%F).sql"
echo "备份文件创建完毕。"

echo "列出备份目录的内容:"
ls -l "$BACKUP_ROOT"

echo "备份流程结束。"

原理解析:

  • prepare_backup_directory 函数封装了检查目录是否存在 (-d) 以及创建目录 (mkdir -p) 的完整逻辑。
  • 它利用了 if 语句和命令退出状态码 $? 来进行健壮的错误处理。
  • 通过将这坨复杂的逻辑封装成一个函数调用,主脚本就只用专注于宏观流程(准备目录 -> 复制文件),而无需在主流程中暴露繁琐的目录检查细节。