Bash 零基础教程

Bash 函数进阶:如何向函数传递参数并处理

在 Bash 脚本中,函数允许我们将命令组合在一起,从而实现代码的模块化。为了让这些函数变得灵活且可重用,它们通常需要接收以“参数 (arguments)”形式提供的输入。这就像我们在之前的章节中使用变量来存储和操作数据一样。

向函数传递参数赋予了脚本动态行为的能力。这意味着同一个函数每次被调用时,都可以处理不同的数据,而你完全不需要去修改函数内部的代码。

1. 在函数内部访问参数

当你在调用函数时跟上参数,Bash 会使用特殊的位置参数 (positional parameters) 将这些参数提供给函数内部使用。这些参数与用于整个脚本的命令行参数非常相似,但它们的作用域仅局限于该函数内部

  • $1, $2, $3, ...: 代表传递给函数的单个参数。$1 指的是第一个参数,$2 是第二个,以此类推。
  • $@: 代表所有的参数,且将每个参数视为一个独立的字符串。当它被展开时,等同于 "$1" "$2" "$3" ...。这在处理包含空格的参数时至关重要。
  • $*: 代表所有的参数,但将它们拼接成一个单一的字符串。当被展开时,等同于 "$1 $2 $3 ..."。在需要遍历参数的场景中,它的实用性通常不如 $@
  • $#: 代表传递给函数的参数的总个数。
  • $0: 注意!在函数内部,$0 仍然指的是脚本本身的名字,而不是函数的名字。

让我们看一个简单的函数 greet_user,它接收一个名字作为参数并打印问候语。

#!/bin/bash

# 定义一个名为 greet_user 的函数
greet_user() {
  echo "你好, $1!" # 使用 $1 来访问第一个参数
}

# 带着参数调用该函数
greet_user "Alice"

# 用不同的参数再次调用该函数
greet_user "Bob"

在这个例子中,执行 greet_user "Alice" 时,"Alice" 被作为第一个参数传递进去,随后在函数内部可以通过 $1 获取到它。输出结果将是:

你好, Alice!
你好, Bob!

2. 处理多个参数

函数可以接收多个参数。你可以根据它们的位置,使用 $1$2 等来分别访问它们。

让我们创建一个名为 create_file 的函数,它接收一个文件名和一些内容作为参数,然后创建一个包含指定内容的文件。

#!/bin/bash

# 定义一个创建文件并写入内容的函数
create_file() {
  local filename="$1" # 将第一个参数(文件名)存储到局部变量中
  local content="$2"  # 将第二个参数(内容)存储到局部变量中
  
  # 检查是否同时提供了两个参数
  if [ -z "$filename" ] || [ -z "$content" ]; then
    echo "用法: create_file <文件名> <内容>"
    return 1 # 返回非零值表示出错
  fi
  
  echo "$content" > "$filename" # 将内容写入文件
  echo "已创建文件: '$filename',内容为: '$content'"
}

# 调用函数并传入文件名和内容
create_file "report.txt" "这是今天的每日报告数据。"

# 再次调用函数,传入不同的文件和内容
create_file "summary.log" "今日系统活动摘要。"

# 演示参数不足时的调用
create_file "empty.txt"

上述脚本的输出结果如下:

已创建文件: 'report.txt',内容为: '这是今天的每日报告数据。'
已创建文件: 'summary.log',内容为: '今日系统活动摘要。'
用法: create_file <文件名> <内容>
注意: 这里使用了 local 关键字将 filenamecontent 声明为 create_file 函数的局部变量。这能防止它们与脚本全局作用域中可能存在的同名变量发生冲突。

3. 遍历所有的参数

当一个函数需要处理未知数量的参数时,$@ 就显得无比珍贵了。它将每一个参数都视为独立的字符串,非常适合用在 for 循环中。

看下面这个 process_items 函数,它接收任意数量的物品并逐个打印出来。

#!/bin/bash

# 定义一个处理任意数量物品的函数
process_items() {
  echo "正在处理 $# 个物品:" # 使用 $# 获取参数总数
  for item in "$@"; do       # 使用 "$@" 遍历所有参数
    echo "- 物品: $item"
  done
  echo "--- 处理结束 ---"
}

# 调用函数,传入多个单词物品
process_items "苹果" "香蕉" "樱桃"

# 调用函数,传入包含空格的字符串物品
process_items "红 苹果" "绿 香蕉" "甜 樱桃 派"

# 调用函数,不传任何物品
process_items

process_items 脚本的输出结果:

正在处理 3 个物品:
- 物品: 苹果
- 物品: 香蕉
- 物品: 樱桃
--- 处理结束 ---
正在处理 3 个物品:
- 物品: 红 苹果
- 物品: 绿 香蕉
- 物品: 甜 樱桃 派
--- 处理结束 ---
正在处理 0 个物品:
--- 处理结束 ---

请注意 "$@" 是如何完美处理包含空格的参数的,它将带空格的字符串完整地保留为一个独立的物品。如果我们在循环中使用的是 "$*"(即 for item in "$*"),那么整个参数列表会被当成一整个大字符串,达不到逐个处理的效果。如果不用引号直接使用 $*,那么 "红 苹果" 就会被错误地拆分成 "红" 和 "苹果" 两个独立的物品。

4. 综合实战:用户账户管理脚本

让我们将“带参数的函数”整合到我们的系统管理实战案例中。在之前的模块中,我们编写了一些带错误检查的系统管理任务。现在,我们可以使用带参数的函数让特定的任务变得可复用且代码更加整洁。

假设我们需要几个函数来添加用户、删除用户以及检查用户是否存在。每个函数都需要将用户名 (username) 作为参数。

#!/bin/bash

# 日志记录函数 (复用错误处理章节的概念)
log_message() {
  local level="$1"    # 第一个参数:日志级别 (如 INFO, WARNING)
  local message="$2"  # 第二个参数:日志内容
  echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $message"
}

# 检查用户是否存在的函数
# 参数 1: username
user_exists() {
  local username="$1"
  # 将标准输出和标准错误都重定向到 /dev/null(丢弃输出)
  if id "$username" &>/dev/null; then
    return 0 # 用户存在 (返回 0 表示成功/真)
  else
    return 1 # 用户不存在 (返回 1 表示失败/假)
  fi
}

# 添加新用户的函数
# 参数 1: username
add_user() {
  local username="$1"
  if user_exists "$username"; then
    log_message "WARNING" "用户 '$username' 已存在。跳过添加操作。"
    return 1
  else
    # 在真实场景中,你会使用 'sudo useradd "$username"'
    # 为了安全演示,这里我们仅仅模拟这个动作
    echo "模拟执行: useradd '$username'"
    log_message "INFO" "用户 '$username' 添加成功 (模拟)。"
    return 0
  fi
}

# 删除现有用户的函数
# 参数 1: username
delete_user() {
  local username="$1"
  if user_exists "$username"; then
    # 在真实场景中,你会使用 'sudo userdel "$username"'
    echo "模拟执行: userdel '$username'"
    log_message "INFO" "用户 '$username' 删除成功 (模拟)。"
    return 0
  else
    log_message "WARNING" "用户 '$username' 不存在。无法删除。"
    return 1
  fi
}

# --- 主脚本逻辑 ---
echo "--- 用户管理操作开始 ---"

# 尝试添加一个新用户
add_user "jdoe"

# 尝试再次添加同一个用户 (测试拦截机制)
add_user "jdoe"

# 尝试删除一个存在的用户 (假设 testuser123 在你系统里不存在,这里会走到 warning)
delete_user "testuser123"

# 尝试删除一个明显不存在的用户
delete_user "nonexistent_user"

# 执行另一次添加操作
add_user "alice"

原理解析:

  • log_message 接收 levelmessage 两个参数。
  • user_exists 接收 username 并静默检查其是否存在。
  • add_user 接收 username,它先调用 user_exists 检查,然后决定是否(模拟)添加。
  • delete_user 逻辑类似,先检查后删除。

每个函数都使用 $1 来访问其核心参数。log_message 函数是一个极佳的工具函数示例,它接收两个参数来生成更详细、格式统一的输出。这种结构让整个脚本变得极具可读性、可维护性和健壮性。