十一、Linux Shell脚本:函数与模块化
作者:IvanCodes
日期:2025年8月11日
专栏:Linux教程
思维导图
随着我们的脚本功能日益强大,代码量不断增加,将所有命令简单堆砌会使脚本变得难以阅读和维护。此时,函数 便成为提升脚本质量的关键工具。
函数是一个命名的、可重复使用的代码块。通过将特定的功能逻辑封装在函数中,我们可以实现:
代码复用:避免重复编写相同的代码段。
提高可读性:主脚本逻辑更清晰,结构化更强。
简化维护:修改某个功能时,只需专注于对应的函数内部,降低了引入新错误的风险。
一、函数的定义与调用
1. 定义函数的语法
在 Shell 脚本中定义函数主要有两种等效方式:
方式一 (使用 function
关键字):
function function_name {
# 函数体内的命令
command1
command2
}
方式二 (更常用,推荐):
function_name() {
# 函数体内的命令
command1
command2
}
注意: 函数名后的圆括号 ()
和花括号 {}
都是必需的。函数在定义时并不会立即执行。
代码示例:定义一个简单的打印函数
#!/bin/bash# 定义一个打印系统信息的函数
show_system_info() {
echo "--- System Information ---"
echo "Hostname: $(hostname)"
echo "Date: $(date)"
echo "--------------------------"
}
2. 调用函数
定义函数后,要执行其内部代码,只需在脚本中像调用普通命令一样,直接书写函数名即可。
代码示例:调用上面定义的函数
#!/bin/bash# 先定义函数
show_system_info() {
echo "--- System Information ---"
echo "Hostname: $(hostname)"
echo "Date: $(date)"
echo "--------------------------"
}# 然后在需要的地方调用它
echo "Starting script..."
show_system_info
echo "Script finished."
二、函数参数与返回值
1. 函数参数的传递与使用
函数可以接收外部传入的数据,称为参数。在函数内部,获取参数的方式与脚本获取命令行参数的方式完全相同:
$1
,$2
, …: 分别代表第一个、第二个参数。
$#
: 传递给函数的参数总个数。
$*
: 将所有参数视为一个单一字符串。
$@
: 将每个参数视为独立的字符串 (推荐使用"$@"
来安全地处理所有参数)。
调用时传递参数:在函数名后用空格隔开各个参数值。
代码示例:定义并调用一个带参数的函数
#!/bin/bash# 定义一个接收用户名和日志级别的函数
log_message() {
local level=$1
local username=$2
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [${level}] User '${username}' performed an action."
}# 调用函数并传递参数
log_message "INFO" "alice"
log_message "WARNING" "bob"
2. 函数的返回值
函数可以通过两种主要方式向外部返回信息:
1. 退出状态码 - 用于表示成功/失败
- 作用: 主要用于传递函数的执行状态。
- 命令:
return N
,其中N
是一个0
到255
之间的整数。 - 约定:
return 0
表示成功,非零值表示失败或特定错误代码。 - 获取方式: 在函数调用之后,立即检查特殊变量
$?
的值。
代码示例:检查目录是否存在的函数
#!/bin/bash# 函数:检查目录是否存在
dir_exists() {
local dir_path=$1
if [ -d "$dir_path" ]; then
return 0 # 目录存在,成功
else
return 1 # 目录不存在,失败
fi
}# 调用函数并检查其退出状态
check_dir="/etc"
dir_exists "$check_dir"
if [ $? -eq 0 ]; then
echo "Directory '$check_dir' exists."
else
echo "Directory '$check_dir' does not exist."
fi
2. 标准输出 - 用于传递数据
- 作用: 这是最常用的传递数据 (如字符串、计算结果) 的方式。
- 方法: 在函数内部使用
echo
或其他会产生输出的命令。 - 获取方式: 在调用处使用命令替换
$(...)
来捕获函数的输出。
代码示例:一个返回格式化用户名的函数
#!/bin/bash# 函数:将用户名转换为大写
get_uppercase_user() {
local username=$1
# 使用 echo "返回" 结果
echo "$username" | tr 'a-z' 'A-Z'
}# 调用函数并捕获其输出
user="admin"
formatted_user=$(get_uppercase_user "$user")
echo "Original user: $user"
echo "Formatted user: $formatted_user"
总结 - 返回值 vs 输出:
- 表示状态 (成功/失败),使用
return N
,检查$?
。 - 传递数据 (字符串/数字),在函数内
echo
,在外部用variable=$(function_call)
捕获。
三、脚本结构与模块化
1. 将常用功能封装为函数
将重复出现或逻辑独立的功能封装成函数,可以使主程序逻辑更清晰,代码更易维护。
代码示例:使用函数打印带级别的日志
#!/bin/bash# 定义日志函数
log() {
local level=$1
local message=$2
echo "[$(date +'%F %T')] [$level] - $message"
}# --- 主程序逻辑 ---
log "INFO" "Starting the backup process..."
# ... 执行备份的代码 ...
if [ $? -eq 0 ]; then
log "SUCCESS" "Backup completed successfully."
else
log "ERROR" "Backup failed."
fi
2. 脚本的模块化与可重用性
当一些函数非常通用时,可以将它们放在一个独立的脚本文件 (如 utils.sh
) 中,然后在其他脚本中通过 source
命令加载并使用。
source
命令 (或.
): 在当前 Shell 环境中执行指定文件里的命令。这使得文件中的函数和变量在当前脚本中变得可用。
概念示例:
file: /opt/scripts/my_utils.sh
# my_utils.sh - A library of utility functions# 函数:检查用户是否为 root
is_root() {
if [ "$(id -u)" -eq 0 ]; then
return 0
else
return 1
fi
}
file: /opt/scripts/main_script.sh
#!/bin/bash# 使用 source 命令加载工具函数
source /opt/scripts/my_utils.sh# 现在可以直接使用 my_utils.sh 中定义的函数
if is_root; then
echo "Running with root privileges."
# ... 执行需要root权限的操作 ...
else
echo "Error: This script must be run as root." >&2
exit 1
fi
通过这种模块化的方式,通用功能可以被多个脚本共享,大大提高了代码的组织性和可维护性。
练习题
题目一:函数定义与调用
定义一个名为 print_hostname
的函数,该函数执行时打印出当前系统的 hostname
。然后在脚本中调用它。
题目二:函数参数
定义一个名为 sum
的函数,接收两个数字作为参数。函数内部计算这两个数字的和,并使用 echo
打印出类似 “The sum is: [结果]” 的字符串。然后调用该函数计算 15 和 27 的和。
题目三:函数返回值 (退出状态)
定义一个函数 is_even
,接收一个整数作为参数。如果该数字是偶数,函数 return 0
;如果是奇数,函数 return 1
。调用该函数并根据 $?
的值打印出 “Number is even” 或 “Number is odd”。
题目四:函数返回值 (捕获输出)
定义一个函数 get_kernel_version
,该函数使用 echo
输出命令 uname -r
的结果。在脚本中调用此函数,将输出捕获到一个名为 kernel_ver
的变量中,并打印 “Current kernel version: [捕获到的版本号]”。
题目五:参数处理
定义一个函数 file_info
,它接收一个文件路径作为参数。函数需要检查该文件是否存在,如果存在,则打印文件的类型(通过 file
命令);如果不存在,则打印错误信息。
题目六:局部变量
解释在Shell函数内部使用 local
关键字声明变量的两个主要好处是什么?
题目七:模块化加载
你有一个包含多个通用函数的脚本文件 /usr/local/lib/common_functions.sh
。在你的新脚本 /root/my_app.sh
中,你想使用这些函数,应该在新脚本的开头写什么命令?(请写出两种等效的命令)
题目八:函数与循环
定义一个函数 countdown
,接收一个正整数作为参数,然后从该数字倒数到1,每秒打印一个数字。例如,调用 countdown 3
会依次打印 3, 2, 1 (每行一个,间隔1秒)。
题目九:结合退出状态和标准输出
定义一个函数 get_user_home
,接收一个用户名作为参数。如果该用户存在于 /etc/passwd
中,函数 echo
出该用户的家目录路径并 return 0
。如果用户不存在,函数不产生任何标准输出,但 return 1
。
题目十:函数嵌套调用
定义两个函数:add
用于计算两个数的和并 echo
结果;calculate_and_log
接收两个数字,调用 add
函数计算它们的和,然后打印一条日志信息,如 “Calculation result: [和]”。
参考答案与解析
答案一:
#!/bin/bash
# 定义函数
print_hostname() {
echo "Current hostname is: $(hostname)"
}# 调用函数
print_hostname
答案二:
#!/bin/bash
# 定义函数
sum() {
local num1=$1
local num2=$2
local total=$((num1 + num2))
echo "The sum is: $total"
}# 调用函数
sum 15 27
答案三:
#!/bin/bash
# 定义函数
is_even() {
local number=$1
if [ $((number % 2)) -eq 0 ]; then
return 0 # 偶数
else
return 1 # 奇数
fi
}# 调用函数并检查退出状态
check_number=10
is_even $check_number
if [ $? -eq 0 ]; then
echo "Number $check_number is even."
else
echo "Number $check_number is odd."
fi
- 解析: 使用了数学求余运算符
%
来判断奇偶。return
用于返回状态,外部通过$?
来判断。
答案四:
#!/bin/bash
# 定义函数
get_kernel_version() {
echo "$(uname -r)"
}# 调用函数并捕获输出
kernel_ver=$(get_kernel_version)
echo "Current kernel version: $kernel_ver"
- 解析:
$(...)
结构用于执行命令并捕获其标准输出,这是将函数输出赋值给变量的标准方法。
答案五:
#!/bin/bash
# 定义函数
file_info() {
local file_path=$1
if [ -e "$file_path" ]; thenecho "Information for '$file_path':"file "$file_path"
elseecho "Error: File '$file_path' not found." >&2
fi
}# 调用函数
file_info "/etc/hosts"
file_info "/no/such/file"
- 解析: 使用
-e
检查文件是否存在。file
命令用于确定文件类型。错误信息通过>&2
输出到标准错误,这是一个好习惯。
答案六:
使用 local
关键字的好处:
- 避免命名冲突: 确保函数内部的变量不会意外地修改或覆盖函数外部的同名全局变量。
- 增强函数封装性: 使函数更加独立和自包含,因为它的变量作用域被限制在函数内部,这提高了代码的可读性和可维护性。
答案七:
命令一 (推荐):
source /usr/local/lib/common_functions.sh
命令二 (等效):
. /usr/local/lib/common_functions.sh
- 解析:
source
及其简写形式.
都能将指定脚本中的函数和变量加载到当前Shell环境中,使其可用。
答案八:
#!/bin/bash
# 定义函数
countdown() {
local start_num=$1
for (( i=start_num; i>=1; i-- )); doecho $isleep 1
done
}# 调用函数
countdown 3
- 解析: 使用了
for
循环来实现倒计时。sleep 1
命令会使脚本暂停1秒。
答案九:
#!/bin/bash
# 定义函数
get_user_home() {
local user=$1
local home_dir=$(grep "^${user}:" /etc/passwd | cut -d: -f6)if [ -n "$home_dir" ]; thenecho "$home_dir"return 0
elsereturn 1
fi
}# 调用并测试
username="root"
user_home=$(get_user_home "$username")
if [ $? -eq 0 ]; thenecho "Home directory for '$username' is: $user_home"
elseecho "User '$username' not found."
fi
- 解析:
grep
用于在/etc/passwd
中查找用户行,cut
用于提取第6个字段(家目录)。-n
用于检查提取出的字符串是否非空,从而判断用户是否存在。
答案十:
#!/bin/bash# 函数一:计算和
add() {
local result=$(($1 + $2))
echo $result
}# 函数二:调用add并记录日志
calculate_and_log() {
local num_a=$1
local num_b=$2
local sum_result=$(add $num_a $num_b)
echo "Calculation result: $sum_result"
}# 主程序调用
calculate_and_log 100 250
- 解析:
calculate_and_log
函数在其内部调用了add
函数。它通过命令替换$(add ...)
捕获了add
函数的计算结果,并将其用于自己的输出,展示了函数间的协作。