五、shell脚本--函数与脚本结构:搭积木,让脚本更有条理
随着我们的脚本越来越长、越来越复杂,直接把所有命令堆在一起会变得难以阅读和维护。这时候,函数 (Function) 就派上大用场了!
函数就像一个可以重复使用的代码块,你可以给它起个名字,然后在脚本的任何地方通过名字来调用它执行。这有点像我们生活中的“工具”或者“食谱”:
- 避免重复: 如果有一段代码需要反复使用,把它写成函数,就不用每次都复制粘贴了,干净利落✨。
- 提高可读性: 把特定的功能封装在函数里,主脚本的逻辑就会更清晰,一眼就能看出它在干什么。
- 方便维护: 如果某个功能需要修改,只需要改动对应的函数内部代码就行了,牵一发而动全身的风险大大降低🔧。
一、 函数的定义与调用:创建和使用你的“工具”
1.定义函数的语法
在 Shell 脚本里定义函数主要有两种方式,它们基本是等价的:
方式一 (带 function
关键字):
function 函数名 {# 函数内部的命令...命令1命令2# ...
}
方式二 (更常用,类似 C 语言):
函数名() {# 函数内部的命令...命令1命令2# ...
}
注意: 函数名后面的圆括号 ()
和花括号 {}
是必须的。花括号 {}
里面就是函数要执行的代码体。函数体里的命令并不会在定义时立刻执行。
示例:定义一个简单的问候函数
#!/bin/bash# 使用方式二定义函数
print_hello() {echo "你好,欢迎使用 Shell 函数!"
}
2.调用函数
定义好函数之后,想要执行它里面的代码,只需要在脚本中像调用普通命令一样,直接写函数名就行了。
示例:调用上面定义的函数
#!/bin/bash# 先定义函数
print_hello() {echo "你好,欢迎使用 Shell 函数!"
}# 然后调用它
echo "准备调用函数..."
print_hello # 直接写函数名来调用
echo "函数调用完毕。"
运行这个脚本,你就会看到 你好,欢迎使用 Shell 函数!
这句话被打印出来。
3.函数参数的传递与使用:给“工具”加点料 🔧🔩
函数不仅能执行固定操作,还能接收外部传来的数据,这些数据就叫做参数 (Parameters)。函数内部获取参数的方式和脚本获取命令行参数完全一样:
$1
: 函数接收到的第一个参数。$2
: 函数接收到的第二个参数。- … 以此类推 …
$#
: 传递给函数的参数总个数。$*
: 所有参数组成的单一字符串。$@
: 所有参数组成的独立字符串列表 (推荐用"$@"
来处理)。
调用函数时传递参数: 直接在函数名后面跟上用空格隔开的参数值就行了。
示例:定义并调用一个带参数的问候函数
#!/bin/bash# 定义一个接收名字的函数
greet_user() {# $1 代表传进来的第一个参数 (名字)local user_name=$1 # 把参数存到局部变量里是个好习惯echo "你好, $user_name!很高兴见到你。👋"echo "这次调用一共传入了 $# 个参数。"
}# 调用函数,并传递参数
greet_user "张三" # 把 "张三" 作为 $1 传给函数
greet_user "李四" "王五" # 把 "李四" 作为 $1, "王五" 作为 $2 传进去
4.函数的返回值:告诉外面“我干得怎么样” 🤔✅❌
函数执行完后,怎么告诉调用它的地方“任务完成得如何”或者“结果是什么”呢?Shell 函数主要通过两种方式返回信息:
4.1.退出状态码 (Exit Status): 使用 return
命令 (0-255)
-
作用: 主要用来表示函数执行的成功或失败状态,就像普通命令的退出码一样。
-
命令:
return N
,其中N
是一个0 到 255 之间的整数。 -
约定:
return 0
通常表示成功 ✅,非零值 (1-255) 表示失败或某种错误状态 ❌。 -
如何获取: 在函数调用之后,立刻检查特殊变量
$?
,它的值就是return
命令指定的那个数字 N。如果不使用return
,$?
的值是函数体中最后一条命令的退出状态码。 -
注意:
return
主要用于传递状态信息,不适合用来传递复杂的计算结果或长字符串。示例:检查文件是否存在的函数
#!/bin/bash
# 定义函数,检查文件是否存在
check_file_exists() {local filename=$1if [ -f "$filename" ]; thenecho "调试信息:文件 '$filename' 存在。"return 0 # 文件存在,返回 0 (成功)elseecho "调试信息:文件 '$filename' 不存在。"return 1 # 文件不存在,返回 1 (失败)fi
}# 调用函数并检查返回值 $?
target_file="/etc/passwd"
check_file_exists "$target_file"
status=$? # 立刻保存 $? 的值!echo "检查 $target_file 的退出状态码是: $status"
if [ $status -eq 0 ]; thenecho "检查结果:文件确实存在。"
elseecho "检查结果:文件不存在或不是普通文件。"
fi# 再试一个不存在的文件
check_file_exists "/no/such/file"
status=$?
echo "检查 /no/such/file 的退出状态码是: $status"
# ... 后续判断同上 ...
4.2.标准输出 (stdout): 使用 echo
或其他打印命令 (传递数据)
- 作用: 这是函数向外部传递计算结果、字符串或其他数据的最常用方式。
- 方法: 在函数内部,使用
echo
(或其他会输出到屏幕的命令) 打印你想“返回”的数据。 - 如何获取: 在调用函数的地方,使用命令替换
$(...)
(或者老式的反引号`...`
) 来捕获函数的标准输出,并将其赋值给一个变量。
示例:一个返回问候语字符串的函数
#!/bin/bash
# 定义函数,它会"返回"一个拼接好的字符串
generate_greeting() {local name=$1local time_period="早上" # 简化处理,总是早上好# 注意:这里用 echo 输出结果echo "$time_period 好, $name!"
}# 调用函数并捕获其输出
user="王女士"
# 使用 $(...) 来获取函数的输出
greeting_message=$(generate_greeting "$user")# 现在 $greeting_message 变量里就存着函数的输出了
echo "从函数获取到的问候语是: $greeting_message"
总结 返回值 vs 输出:
- 想表示成功/失败状态?用
return N
,然后检查$?
。 - 想传递数据/结果 (字符串、数字等)?在函数里用
echo
输出,在外面用result=$(函数名 参数)
捕获。 - 别混淆:
return 100
主要是设$?=100
表示一种状态,而echo 100
是把字符串 “100” 输出到屏幕(可以被捕获)。
二、脚本结构与模块化:搭积木,让代码更整洁
把代码写进函数是提高脚本质量的第一步。更进一步,我们可以思考如何组织这些函数,让整个脚本结构更清晰,甚至让一些通用的函数能在多个脚本之间共享。
1.将常用功能封装为函数
想象一下,你在写一个比较大的脚本,里面可能反复需要打印分隔线、记录日志、检查用户输入等。把这些重复出现或逻辑独立的功能各自封装成一个函数,好处多多:
- 代码更简洁: 主流程代码会变短,只剩下调用函数的名字,逻辑一目了然。
- 复用更容易: 定义一次,到处调用。
- 维护更方便: 如果分隔线样式要改,只需要改
print_separator
函数就行了。
示例:使用函数打印分隔线
#!/bin/bash# 定义一个打印分隔线的函数
print_separator() {echo "----------------------------------------"
}# --- 主程序逻辑开始 ---
echo "任务一:处理用户数据"
# ... 执行任务一的代码 ...
echo "用户数据处理完毕。"# 调用函数打印分隔线
print_separatorecho "任务二:生成报告"
# ... 执行任务二的代码 ...
echo "报告生成完成。"# 再次调用函数打印分隔线
print_separatorecho "所有任务结束。"
看,通过调用 print_separator
,我们避免了重复写 echo "------..."
,代码是不是清爽多了?
2.脚本的模块化与可重用性 🧩➡️📦
当你的函数库越来越丰富,有些函数(比如日志记录、通用的数学计算、字符串处理等)可能在很多不同的脚本里都会用到。这时,我们可以把这些通用的函数单独存放在一个或多个文件里(比如叫做 my_utils.sh
),然后在需要它们的脚本中使用 source
命令(或者一个点 .
)把这些函数加载进来。
source
命令 (或.
): 它的作用是在当前 Shell 环境中执行指定文件里的命令。如果那个文件里定义了函数,那么执行source
之后,这些函数在当前脚本里就可用了!
概念示例 (不实际运行,仅作演示):
假设你有一个 utils.sh
文件,内容是:
# 文件名: utils.sh
# 一些通用的工具函数log_message() {local level=$1local message=$2echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $message" >> /var/log/my_app.log
}add_numbers() {echo $(($1 + $2))
}
然后,在你的主脚本 main.sh
里,你可以这样做:
#!/bin/bash
# 文件名: main.sh# 使用 source 命令加载 utils.sh 里的函数定义
source ./utils.sh
# 或者用点 . (注意点和文件名之间有空格)
# . ./utils.sh# 现在可以直接使用 utils.sh 里定义的函数了
log_message "INFO" "主脚本开始执行..."num1=10
num2=20
sum=$(add_numbers $num1 $num2) # 调用来自 utils.sh 的函数
echo "$num1 + $num2 = $sum"log_message "INFO" "主脚本执行完毕。"
通过这种方式,log_message
和 add_numbers
这些通用功能就实现了模块化,可以被多个脚本重用,大大提高了代码的组织性和可维护性。这就是脚本结构优化的重要一步!
三、练习题 🧠✍️
题目一:函数定义与调用
❓ 定义一个名为 show_date
的函数,它不需要参数,执行时打印当前的日期和时间 (可以使用 date
命令)。然后在脚本中调用这个函数。
题目二:函数参数
❓ 定义一个名为 multiply
的函数,接收两个数字作为参数 ($1
和 $2
)。函数内部计算这两个数字的乘积,并使用 echo
打印结果,例如 “Result: N”。然后调用这个函数传递参数 5 和 6。
题目三:函数返回值 (退出状态)
❓ 定义一个函数 is_positive
,接收一个数字作为参数。如果数字大于 0,函数使用 return 0
;否则,使用 return 1
。调用该函数传递 -3
,并根据 $?
的值打印 “数字是正数” 或 “数字不是正数”。
题目四:函数返回值 (捕获输出)
❓ 定义一个函数 get_system_info
,该函数使用 echo
输出字符串 “系统类型: $(uname -s)”。在脚本中调用该函数,并将函数的输出捕获到一个名为 sys_info
的变量中,最后打印这个变量。
题目五:函数的好处
❓ 简单说出使用函数来组织 Shell 脚本代码的主要好处有哪些?(至少说两点)
题目六:模块化概念
❓ 如果你想把一些常用的函数(比如日志函数、检查函数)放到一个单独的文件 common_funcs.sh
里,然后在你的主脚本 my_script.sh
中使用这些函数,你应该在 my_script.sh
的开头使用什么命令来加载 common_funcs.sh
?
四、参考答案 ✅💡
答案一:
#!/bin/bash
# 定义函数
show_date() {echo "当前日期和时间是: $(date)"
}# 调用函数
echo "准备显示日期..."
show_date
echo "日期显示完毕。"
答案二:
#!/bin/bash
# 定义函数
multiply() {local num1=$1local num2=$2local result=$((num1 * num2))echo "计算结果: $result" # 使用 echo 输出结果
}# 调用函数并传递参数
echo "计算 5 * 6 ..."
multiply 5 6 # 输出 "计算结果: 30"
答案三:
#!/bin/bash
# 定义函数
is_positive() {local number=$1if [ $number -gt 0 ]; thenreturn 0 # 正数,返回 0 (成功)elsereturn 1 # 非正数,返回 1 (失败)fi
}# 调用函数并检查退出状态 $?
check_num=-3
echo "检查数字 $check_num 是否为正数..."
is_positive $check_num
status=$? # 立刻获取返回值if [ $status -eq 0 ]; thenecho "结果: 数字是正数。"
elseecho "结果: 数字不是正数。" # 因为传入 -3,应该输出这个
fi
答案四:
#!/bin/bash
# 定义函数
get_system_info() {# 使用 echo 输出信息echo "系统类型: $(uname -s)"
}# 调用函数并捕获输出到变量 sys_info
echo "正在获取系统信息..."
sys_info=$(get_system_info) # 使用命令替换 $(...)# 打印捕获到的信息
echo "获取到的信息是: $sys_info"
答案五:
使用函数的主要好处包括:
- 代码复用 (Reusability): 避免重复编写相同的代码块。
- 提高可读性 (Readability): 主逻辑更清晰,代码结构化。
- 简化维护 (Maintainability): 修改功能只需改动函数内部。
- 模块化 (Modularity): 可以将相关功能组织在一起,甚至放到单独文件。
答案六:
应该在 my_script.sh
的开头使用 source common_funcs.sh
命令,或者用它的简写形式 . common_funcs.sh
(注意点.
和文件名之间有空格)。