十一、Linux Shell脚本:函数与模块化

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
云原生网关 MSE Higress,422元/月
简介: 随着脚本越来越复杂,把所有代码都堆在一起会很难维护。这时就该用函数了,你可以把它看作是创建属于你自己的“新命令”。把一段常用逻辑封装成函数,以后只用喊一声它的“名字”就能调用。给函数“递东西”(传参数)用$1, $2;而函数想“回话”有两种方式:用return返回一个0或非0的“状态码”(表示成功或失败),用echo则能“喊出”具体的数据,让你在外面用$()接住。学会这个,就能开始打造自己的通用工具库了。

思维导图

image.png
image.png
image.png

随着我们的脚本功能日益强大,代码量不断增加,将所有命令简单堆砌会使脚本变得难以阅读和维护。此时,函数 便成为提升脚本质量关键工具

函数是一个命名的可重复使用代码块。通过将特定的功能逻辑封装在函数中,我们可以实现:

代码复用:避免重复编写相同的代码段。
提高可读性:主脚本逻辑更清晰,结构化更强。
简化维护:修改某个功能时,只需专注于对应的函数内部,降低了引入新错误的风险

一、函数的定义与调用

1. 定义函数的语法

在 Shell 脚本中定义函数主要有两种等效方式:

方式一 (使用 function 关键字):

function function_name {
   
# 函数体内的命令
command1
command2
}

方式二 (更常用,推荐):

function_name() {
   
# 函数体内的命令
command1
command2
}
注意: 函数名后的 圆括号 ()花括号 {} 都是 必需的。函数在 定义时不会立即执行。

代码示例:定义一个简单的打印函数
bash #!/bin/bash # 定义一个打印系统信息的函数 show_system_info() { echo "--- System Information ---" echo "Hostname: $(hostname)" echo "Date: $(date)" echo "--------------------------" }

### 2. 调用函数
定义函数后,要执行其内部代码,只需在脚本中 像调用普通命令一样,直接 书写函数名即可。

代码示例:调用上面定义的函数
bash #!/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 是一个 0255 之间的整数
  • 约定: 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" ]; then
  echo "Information for '$file_path':"
  file "$file_path"
else
  echo "Error: File '$file_path' not found." >&2
fi
}

# 调用函数
file_info "/etc/hosts"
file_info "/no/such/file"
  • 解析: 使用 -e 检查文件是否存在。file 命令用于确定文件类型。错误信息通过 >&2 输出到标准错误,这是一个好习惯。

答案六:
使用 local 关键字的好处:

  1. 避免命名冲突: 确保函数内部的变量不会意外地修改或覆盖函数外部的同名全局变量。
  2. 增强函数封装性: 使函数更加独立和自包含,因为它的变量作用域被限制在函数内部,这提高了代码的可读性和可维护性。

答案七:
命令一 (推荐):

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-- )); do
  echo $i
  sleep 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" ]; then
  echo "$home_dir"
  return 0
else
  return 1
fi
}

# 调用并测试
username="root"
user_home=$(get_user_home "$username")
if [ $? -eq 0 ]; then
  echo "Home directory for '$username' is: $user_home"
else
  echo "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 函数的计算结果,并将其用于自己的输出,展示了函数间的协作。
相关文章
|
3月前
|
安全 Linux Shell
四、Linux核心工具:Vim, 文件链接与SSH
要想在Linux世界里游刃有余,光会“走路”还不够,还得配上几样“高级装备”。首先是Vim编辑器,它像一把瑞士军刀,让你能在命令行里高效地修改文件。然后要懂“软硬链接”,软链接像个快捷方式,硬链接则是给文件起了个别名。最后,SSH是你的“传送门”,不仅能让你安全地远程登录服务器,还能用scp轻松传输文件,设置好密钥更能实现免-密登录,极大提升效率。
|
3月前
|
监控 Shell Linux
十、Linux Shell脚本:流程控制语句
要让脚本从简单的“指令清单”升级为能干的“小助手”,就需要用if教它根据条件做判断,用for和while循环让它重复处理任务,再用case语句帮它在多个选项中做出清晰的选择
|
3月前
|
Linux 应用服务中间件 Shell
二、Linux文本处理与文件操作核心命令
熟悉了Linux的基本“行走”后,就该拿起真正的“工具”干活了。用grep这个“放大镜”在文件里搜索内容,用find这个“探测器”在系统中寻找文件,再用tar把东西打包带走。最关键的是要学会使用管道符|,它像一条流水线,能把这些命令串联起来,让简单工具组合出强大的功能,比如 ps -ef | grep 'nginx' 就能快速找出nginx进程。
二、Linux文本处理与文件操作核心命令
|
3月前
|
存储 安全 Linux
三、Linux用户与权限管理详解
管理Linux系统就像当一个大楼的管家。首先,你得用useradd和passwd给新员工发“钥匙”(创建用户并设密码),并用groupadd把他们分到不同“部门”(用户组)。然后,你要为每个“房间”(文件或目录)设定规矩,这就是文件权限:用chmod命令设置谁(所有者、同部门、其他人)可以“进入”(x)、“读取”(r)或“写入”(w)。最后,用chown还能把房间的归属权转让给别人。
|
3月前
|
存储 安全 Unix
七、Linux Shell 与脚本基础
别再一遍遍地敲重复的命令了,把它们写进Shell脚本,就能一键搞定。脚本本质上就是个存着一堆命令的文本文件,但要让它“活”起来,有几个关键点:文件开头最好用#!/usr/bin/env bash来指定解释器,并用chmod +x给它执行权限。执行时也有讲究:./script.sh是在一个新“房间”(子Shell)里跑,不影响你;而source script.sh是在当前“房间”里跑,适合用来加载环境变量和配置文件。
|
3月前
|
缓存 安全 Linux
六、Linux核心服务与包管理
在没有网络的情况下,使用系统安装光盘是获取RPM包的常用方法。场景二:配置本地文件镜像源 (使用系统安装光盘/ISO)(检查RPM包的GPG签名以保证安全) 或。YUM/DNF包管理工具 (yum/dnf)(此处可以放置您为本主题制作的思维导图)处理依赖问题的危险选项 (应极力避免)(覆盖文件、替换已安装包)。(list) 则是列出文件。(query file) 是。(假设系统安装光盘已挂载到。信息 (verbose)。(upgrade) 选项。(all) 已安装的包。(package) 选项
340 11
|
3月前
|
存储 Unix Linux
五、Linux进程与磁盘管理
管理Linux服务器就像身兼两职:既是“医生”,又是“城市规划师”。作为“医生”,你得用ps和top给系统“体检”,看看哪些进程在运行;必要时用kill命令做“外科手术”,终止掉问题进程。作为“规划师”,你得先用fdisk把“土地”(磁盘)划分成“地块”(分区),再用mkfs铺上“地基”(文件系统),最后用mount把地块“挂牌”使用。而df和du就是你随时查看“土地”使用情况的测量工具。
|
缓存 Linux
Centos7中搭建本地yum源
Centos7中搭建本地yum源
681 0
|
26天前
|
缓存 Java Maven
六、Docker 核心技术:Dockerfile 指令详解
想亲手给你的应用程序打造一个专属的“集装箱”吗?Dockerfile就是你的说明书!它其实就是一个简单的文本文件,你可以在里面像搭积木一样,用FROM、COPY、RUN这些指令,一步步告诉Docker如何打包你的应用。最后,通过多阶段构建的小技巧,还能给镜像“减肥”,让它变得轻巧又高效。快来学习用Dockerfile变身打包达人吧!
|
3月前
|
JavaScript Shell Linux
十二、Linux Shell脚本:正则表达式
正则表达式就是一套给文本“相亲”的规则,让你不再只能找“张三”,而是能找到所有“姓张、两个字、且名字里不带'伟'”的人。它通过一些“魔术符号”(元字符)来描述模式:比如 . 代表任意字符,* 代表重复任意次,^ 和 $ 则卡住行头行尾。把这些符号组合起来,你就能用grep或sed等工具,从海量文本里精确地捞出你想要的任何格式的数据。