Shell 脚本:循环与函数

一、循环结构:重复执行命令的核心

循环是 Shell 脚本中用于重复执行一组命令的结构,根据场景可分为 for、while、until 三种,配合 break/continue 可灵活控制循环流程。

1. for 循环:遍历取值列表

for 循环适用于已知取值范围的场景(如遍历文件、列表、数字序列),支持 “基础列表遍历” 和 “C 风格循环”,也可嵌套实现复杂逻辑(双 for 循环)。

(1)基础语法

类型

语法格式

说明

列表遍历

for 变量名 in 取值列表; do 命令序列; done

变量依次取 “取值列表” 中的值,执行命令

C 风格循环

for ((初始值; 条件; 步长)); do 命令序列; done

类似 C 语言,适合数字范围循环(如 1~9)

(2)实战案例
案例 1:批量创建用户(列表遍历)

需求:从 users.txt 文件中读取用户名,批量创建用户并设置独立密码。

  1. 先创建用户列表文件 users.txt:
    # 编辑用户列表(每个用户名占一行)
    
    vim /root/users.txt
    
    jim
    
    bob
    
    marry
    
    rose
    
    mike

  2. 编写脚本 b.sh
#!/bin/bash

# 批量创建用户脚本

for username in $(cat /root/users.txt); do # 遍历文件中的用户名

read -p "请为用户 $username 设置密码:" pass # 交互式输入密码

useradd $username # 创建用户

echo "$pass" | passwd --stdin $username # 批量设置密码(避免交互)

echo "用户 $username 创建完成!"

done
  • 执行:sh b.sh,按提示输入每个用户的密码即可批量创建。
案例 2:九九乘法表(双 for 循环嵌套)

需求:用嵌套 for 循环输出标准九九乘法表,通过制表符 \t 对齐。

#!/bin/bash

# 九九乘法表脚本

for ((i=1; i<=9; i++)); do # 外层循环:控制行数(1~9)

for ((j=1; j<=i; j++)); do # 内层循环:控制每行的列数(1~当前行数i)

product=$((j * i)) # 计算乘积

echo -n -e "$j×$i=$product\t" # -n 不换行,-e 解析转义符(\t)

done

echo # 每行结束后换行

done
  • 执行效果:
1×1=1	
1×2=2	2×2=4	
1×3=3	2×3=6	3×3=9	
1×4=4	2×4=8	3×4=12	4×4=16	
1×5=5	2×5=10	3×5=15	4×5=20	5×5=25	
1×6=6	2×6=12	3×6=18	4×6=24	5×6=30	6×6=36	
1×7=7	2×7=14	3×7=21	4×7=28	5×7=35	6×7=42	7×7=49	
1×8=8	2×8=16	3×8=24	4×8=32	5×8=40	6×8=48	7×8=56	8×8=64	
1×9=9	2×9=18	3×9=27	4×9=36	5×9=45	6×9=54	7×9=63	8×9=72	9×9=81	

2. while 循环:条件成立则重复执行

while 循环适用于条件未知的场景(如等待用户输入、循环到某个条件满足),核心逻辑是 “条件为真则执行循环体”。

(1)基础语法
while 条件表达式; do

命令序列 # 条件为真时执行

done
(2)实战案例
案例 1:计算 1~100 的整数和

需求:从 1 累加至 100,用 while 循环控制变量递增。

#!/bin/bash

# 计算 1~100 的和

a=1 # 初始值(从 1 开始)

sum=0 # 累加结果(初始为 0)

while [ $a -le 100 ]; do # 条件:a ≤ 100 时执行循环

sum=$((sum + a)) # 累加:sum = 原 sum + 当前 a

a=$((a + 1)) # a 自增 1(步长为 1)

done

echo "1~100 的整数和为:$sum" 

# 输出结果:5050
案例 2:随机猜数字游戏

需求:生成 0~999 的随机数,让用户猜数字,提示 “大了”“小了”,直到猜对并统计次数。

#!/bin/bash

# 随机猜数字游戏(0~999)

num=$((RANDOM % 1000)) # 生成随机数(RANDOM 是 Bash 内置随机数,%1000 限定 0~999)

a=0 # 统计猜的次数

while true; do # 无限循环(直到猜对后 exit)

read -p "请猜一个 0~999 的整数:" n # 接收用户输入

let a++ # 次数自增

# 判断猜的结果

if [ $n -eq $num ]; then

echo "恭喜猜对了!正确数字是 $n"

echo "你总共猜了 $a 次"

exit # 猜对后退出循环

elif [ $n -gt $num ]; then

echo "猜大了,再试试!"

else

echo "猜小了,再试试!"

fi

done

3. until 循环:条件不成立则重复执行

until 循环与 while 逻辑完全相反:“条件为假时执行循环体,条件为真时退出循环”,适合 “直到某个条件满足才停止” 的场景。

(1)基础语法
until 条件表达式; do

命令序列 # 条件为假时执行

done
(2)实战案例:计算 1~100 的整数和(until 版)

需求:与 while 案例功能一致,仅替换循环类型。

#!/bin/bash

# 用 until 计算 1~100 的和

a=1

sum=0

# 条件:a > 100 时退出循环(即 a ≤ 100 时执行循环)

until [ $a -gt 100 ]; do

sum=$((sum + a))

a=$((a + 1))

done

echo "1~100 的整数和为:$sum" 

# 结果同样为 5050

4. 循环控制:break 与 continue

break 和 continue 用于在循环中主动控制流程,避免无效循环或跳过部分迭代。

命令

作用

示例场景

break

立即跳出当前所在的循环(若嵌套循环,仅跳出内层)

猜数字游戏猜对后跳出循环

continue

跳过当前迭代的剩余命令,直接进入下一次迭代

遍历 1~15 时,跳过 6~9

(1)break 案例:嵌套循环中跳出内层

需求:外层循环 1~5,内层循环 1~5,当内层变量 b=4 时跳出内层循环。

#!/bin/bash

for ((a=1; a<=5; a++)); do # 外层循环

echo "外层循环:a = $a"

for ((b=1; b<=5; b++)); do # 内层循环

if [ $b -eq 4 ]; then

break # 当 b=4 时,跳出内层循环(不执行 b=4、5)

fi

echo " 内层循环:b = $b"

done

done
  • 执行效果(内层仅输出 b=1~3):
外层循环:a = 1

内层循环:b = 1

内层循环:b = 2

内层循环:b = 3

外层循环:a = 2

内层循环:b = 1

...(后续省略)
(2)continue 案例:跳过指定范围的迭代

需求:遍历 1~15,跳过 6~9 的数字(不输出)。

#!/bin/bash

for ((a=1; a<=15; a++)); do

# 条件:a 在 6~9 之间时,跳过当前迭代

if [ $a -gt 5 ] && [ $a -lt 10 ]; then

continue

fi

echo "当前数字:$a"

done
  • 执行效果(跳过 6~9):
当前数字:1

当前数字:2

...

当前数字:5

当前数字:10

当前数字:11

...

二、函数:封装可复用的代码块

函数是 Shell 中用于封装重复逻辑的代码块,可接收参数、返回结果,支持局部变量和递归,大幅提升脚本的复用性和可读性。

1. 函数定义:两种合法格式

Shell 函数定义有两种格式,function 关键字可省略(推荐省略,更简洁)。

格式

语法

说明

带 function

function 函数名 { 命令序列; } 或 function 函数名() { 命令序列; }

兼容性好,支持 ksh/zsh 等 Shell

省略 function

函数名() { 命令序列; }

Bash 推荐格式,更简洁,符合 POSIX 标准

2. 函数返回值:return 与 echo

函数的 “返回值” 有两种实现方式,适用于不同场景:

(1)return:返回退出状态码(0~255)
  • return 数值:用于返回执行结果状态(0 表示成功,非 0 表示失败),或小范围整数(0~255)。
  • 脚本中通过 $? 获取返回值($? 仅保存上一条命令的退出码,需立即获取)。
  • 注意:若数值超出 0~255,会自动取模 256(如 return 257 实际返回 1)。
案例:返回输入整数的 2 倍
#!/bin/bash

# 函数:返回输入整数的 2 倍

num() {

read -p "请输入一个整数:" n

return $((n * 2)) # 返回 n*2 的结果(需在 0~255 内)

}

num # 调用函数

echo "函数返回值(n*2):$?" # 立即获取返回值
  • 执行示例:
请输入一个整数:20 → 输出:40(20*2=40)

请输入一个整数:130 → 输出:4(130*2=260,260%256=4)

请输入一个整数:256 → 输出:0
(2)echo:返回任意结果(推荐)
  • 若需返回超出 0~255 的值(如大整数、字符串),直接用 echo 输出结果,脚本中通过命令替换($(函数名))捕获。
案例:返回输入整数的 2 倍(无范围限制)
#!/bin/bash

# 函数:用 echo 返回输入整数的 2 倍

num() {

read -p "请输入一个整数:" n

echo $((n * 2)) # 直接输出结果

}

result=$(num) # 捕获 echo 的输出

echo "函数返回值(n*2):$result" # 输出结果(无范围限制)
  • 执行示例:
请输入一个整数:300 → 输出:600(无范围限制)

3. 函数传参:向函数传递参数

函数可像脚本一样接收参数,通过 $1、$2、$*、$@ 等特殊变量获取参数($0 表示函数名,非脚本名)。

语法:调用函数时传递参数
# 定义函数(接收 2 个参数,计算和)

add() {

sum=$(( $1 + $2 )) # $1 是第一个参数,$2 是第二个参数

echo $sum

}

# 调用函数并传递参数(20 和 30 是参数)

add 20 30 

运行:sh a.sh 20 30

# 输出:50
实战案例:计算两个参数的和
#!/bin/bash

# 函数:计算两个参数的和

add() {

if [ $# -ne 2 ]; then # 判断参数数量是否为 2

echo "错误:需传入 2 个参数!"

return 1 # 返回非 0 表示失败

fi

echo $(( $1 + $2 ))

}

# 调用函数(传递脚本的参数 $1 和 $2)

add $1 $2
  • 执行:sh add.sh 20 30 → 输出:50;
  • 若执行 sh add.sh 10 → 输出:错误:需传入 2 个参数!

4. 函数变量作用域:局部变量与全局变量

Shell 变量默认是全局变量(整个脚本生效),若需限定变量仅在函数内生效,需用 local 关键字声明为局部变量

变量类型

声明方式

作用范围

示例

全局变量

直接赋值(如 a=10)

整个脚本(包括所有函数)

a=10 在函数内外都可访问

局部变量

local 变量名=值

仅当前函数内生效

函数内 local a=10,函数外无法访问

实战案例:区分局部与全局变量

需求:函数内用局部变量 m/n 计算和,函数外用全局变量 m/n 计算和,分别输出。

#!/bin/bash

# 函数:用局部变量计算和

sum1() {

sum=$((m + n))

local m n # 声明局部变量(仅函数内生效)

m=20

n=10

local sum=$((m + n)) # 局部变量 sum

echo "局部变量 m=$m, n=$n,和为:$sum"

}

# 全局变量(整个脚本生效)

m=20

n=20

# 调用函数并输出结果

sum1

echo "全局变量 m=$m, n=$n,和为:$sum"
  • 执行效果:
局部变量 m=20, n=10,和为:30

全局变量 m=20, n=20,和为:40

5. 递归函数:函数调用自身

递归是 “函数调用自身” 的特性,适用于分治问题(如阶乘、斐波那契数列),需注意设置 “递归终止条件”(避免无限递归)。

实战案例:计算一个数的阶乘

需求:输入一个正整数,用递归计算其阶乘(阶乘定义:n! = n × (n-1) × ... × 1,终止条件:1! = 1)。

#!/bin/bash

# 递归函数:计算阶乘

abc() {

local n=$1 # 接收参数(当前数字)

# 终止条件:n=1 时返回 1

if [ $n -eq 1 ]; then

echo 1

return

fi

# 递归调用:n! = n × (n-1)!

local result=$(abc $((n - 1))) # 调用自身,计算 (n-1)!

echo $((n * result)) # 返回 n × (n-1)! 的结果

}

# 接收用户输入并调用函数

read -p "请输入一个正整数:" num

result=$(abc $num)

echo "$num 的阶乘为:$result"
  • 执行示例:
请输入一个正整数:5 → 输出:5 的阶乘为:120(5×4×3×2×1=120)

请输入一个正整数:3 → 输出:3 的阶乘为:6(3×2×1=6)

6. 函数库:封装通用函数供复用

函数库是 “集中存放多个函数的脚本文件”,其他脚本可通过 “ source 命令” 加载函数库,直接调用函数,避免重复编写代码。

(1)函数库的核心逻辑

  • 独立文件:将通用函数(如加减乘除)集中写入一个脚本(如 func_lib.sh),作为 “函数库”;
  • 加载调用:其他脚本通过 source 函数库路径 或 . 函数库路径 加载函数,无需重新定义即可调用。

(2)实战案例:搭建 “加减乘除” 函数库

步骤 1:创建函数库文件 func_lib.sh

#!/bin/bash

# 函数库:包含加减乘除四个通用函数

# 1. 加法函数:返回 $1 + $2 的结果

add() {

local num1=$1

local num2=$2

echo $((num1 + num2))

}

# 2. 减法函数:返回 $1 - $2 的结果

subtract() {

local num1=$1

local num2=$2

echo $((num1 - num2))

}

# 3. 乘法函数:返回 $1 × $2 的结果

multiply() {

local num1=$1

local num2=$2

echo $((num1 * num2))

}

# 4. 除法函数:返回 $1 ÷ $2 的结果(处理分母为 0 的情况)

divide() {

local num1=$1

local num2=$2

if [ $num2 -eq 0 ]; then # 判断分母是否为 0

echo "错误:除法分母不能为 0"

return 1 # 返回非 0 表示执行失败

fi

echo $((num1 / num2)) # 整数除法(若需小数,需用 bc 工具)

}

步骤 2:创建调用脚本 calc.sh

通过 source ./func_lib.sh 加载函数库,调用其中的函数实现计算:

#!/bin/bash

# 调用函数库的计算器脚本

# 加载函数库(需指定正确路径,若在同一目录可写相对路径)

source ./func_lib.sh

# 交互式接收用户输入

read -p "请输入第一个整数:" n1

read -p "请输入第二个整数:" n2

# 调用函数库中的函数,获取计算结果

sum=$(add $n1 $n2)

diff=$(subtract $n1 $n2)

product=$(multiply $n1 $n2)

quotient=$(divide $n1 $n2)

# 输出结果

echo -e "\n===== 计算结果 ====="

echo "两数之和:$sum"

echo "两数之差:$diff"

echo "两数之积:$product"

echo "两数之商:$quotient"

步骤 3:执行脚本并验证

  1. 确保 func_lib.shcalc.sh 在同一目录;
  2. 赋予执行权限:chmod +x func_lib.sh calc.sh
  3. 执行脚本:./calc.sh,输出示例:
请输入第一个整数:10

请输入第二个整数:3

===== 计算结果 =====

两数之和:13

两数之差:7

两数之积:30

两数之商:3

    4.若输入分母为 0(如第二个数输入 0),会提示错误:

请输入第一个整数:10

请输入第二个整数:0

===== 计算结果 =====

两数之和:10

两数之差:10

两数之积:0

两数之商:错误:除法分母不能为 0

三、循环与函数的常见问题与注意事项

1. 循环相关注意事项

(1)while 循环的 “无限循环” 风险
  • 若条件表达式恒为真(如 while true),需确保循环体内有 exit 或 break 退出逻辑,避免脚本卡死:
# 安全的无限循环(有退出条件)

while true; do

read -p "输入 q 退出:" cmd

if [ "$cmd" = "q" ]; then

break # 输入 q 时退出循环

fi

done

2. 函数相关注意事项

(1)局部变量的 “声明顺序”
  • local 声明必须在变量使用前,否则变量会先被识别为全局变量,再转为局部变量,导致逻辑错误:
# 错误:先使用 m,再声明 local,此时 m 已被视为全局变量

sum() {

m=10 # 全局变量

local m # 后续修改 m 仅影响局部,但前一行已污染全局

m=20

echo $m

}

# 正确:先声明 local,再赋值使用

sum() {

local m

m=20

echo $m

}
(2)函数库的 “路径问题”
  • 加载函数库时,若使用相对路径(如 ./func_lib.sh),需确保执行脚本时的 “当前工作目录” 正确;若函数库路径固定,建议用绝对路径(如 /root/func_lib.sh):
# 推荐:用绝对路径加载,避免目录切换导致找不到函数库

. /root/func_lib.sh
(3)函数返回值的 “获取时机”
  • 用 $? 获取 return 的返回值时,需 “立即获取”,因为 $? 仅保存上一条命令的退出码,后续命令会覆盖它:
function sl {
        read -p "请输入一个整数:" n
        return $[ $n*2 ]
}

# 错误做法: echo 命令会覆盖 $? 的值

sl

echo "计算完成"

echo $? 

# 输出 echo 命令的退出码(0),而非 sl 函数的返回值

# 正确做法:立即获取返回值

sl

result=$?

echo "返回值:$result" # 输入5 输出 10(正确)

四、总结

Shell 脚本的循环与函数是实现 “自动化” 和 “复用性” 的核心,需掌握以下关键点:

1. 循环:选对场景,控制流程

  • for 循环:适合已知范围(如遍历列表、数字序列),嵌套可实现复杂逻辑(如九九乘法表);
  • while 循环:适合条件未知(如等待用户输入、猜数字游戏),需配合退出逻辑;
  • until 循环:与 while 相反,适合 “直到条件满足才停止” 的场景(如累加至目标值);
  • break/continue:灵活控制循环,避免无效迭代。

2. 函数:封装逻辑,提升效率

  • 定义格式:推荐省略 function,用 函数名() { ... },简洁且兼容标准;
  • 返回值:小整数用 return(0~255),大值 / 字符串用 echo + 命令替换;
  • 变量作用域:用 local 限定函数内变量,避免污染全局;
  • 函数库:集中管理通用函数,通过 source 加载,减少重复代码。

通过 “循环控制流程 + 函数封装逻辑” 的组合,可编写结构清晰、可维护、可复用的 Shell 脚本,满足日常运维、自动化任务等需求。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐