一、Shell 概述
1. 什么是 Shell?
Shell 是 Linux 系统中用户与内核之间的桥梁,作为 命令解析器,它负责将用户输入的文本命令转换为计算机可执行的机器指令。
- 本质:Shell 是一个程序(如常见的 Bash、Zsh),而 Shell 脚本则是包含一系列命令的文本文件,通常以
.sh
结尾(非强制,仅为识别方便)。 - 作用:批量执行重复命令、实现自动化任务、编写复杂逻辑程序。
2. 为什么选择 Bash?
- 主流性:Bash(Bourne Again Shell)是 Linux 系统的默认 Shell,几乎所有发行版都内置,兼容性强。
- 功能强大:支持变量、数组、函数、流程控制等高级特性,满足从简单脚本到复杂程序的需求。
- 学习门槛低:语法简洁,对新手友好,且与早期 Shell(如 Bourne Shell)兼容。
二、Shell 脚本基础操作
1. 脚本文件的创建与编辑
1.1 选择文本编辑器
Shell 脚本本质是纯文本文件,推荐使用以下编辑器:
- nano(新手友好):命令行下的简单编辑器,通过
nano 文件名
启动,支持鼠标和快捷键操作。 - vim(功能强大):通过
vim 文件名
启动,需切换模式(i
进入插入模式,Esc
退出,:wq
保存并退出)。 - gedit(图形界面):适合桌面环境,直接右键文件选择「用文本编辑器打开」。
1.2 创建脚本文件
# 在当前目录创建名为 demo.sh 的脚本文件
touch demo.sh
1.3 编写脚本内容
# 用 nano 打开并编辑脚本
nano demo.sh
输入以下内容:
#!/bin/bash
# 这是一个简单的 Shell 脚本示例
echo "Hello, World!" # 输出文本
关键行解释:
#!/bin/bash
:指定脚本使用 Bash 解释器执行,必须位于文件第一行。#
开头的行是注释,用于解释代码逻辑,不参与执行。echo
命令用于输出文本,默认换行。若需不换行,使用echo -n "内容"
。
2. 脚本的执行方式
2.1 方式一:通过 bash
命令执行(无需权限)
bash demo.sh
原理:直接调用 Bash 解释器执行脚本,适用于快速测试,无需修改文件权限。
2.2 方式二:通过 sh
命令执行(兼容旧版)
sh demo.sh
注意:sh
通常指向 Bash,但某些系统中可能指向更古老的 Shell(如 BusyBox sh),可能导致兼容性问题。建议统一使用 #!/bin/bash
头部。
2.3 方式三:赋予执行权限后运行(推荐)
# 赋予文件执行权限
chmod +x demo.sh
# 通过相对路径执行
./demo.sh
关键点:
chmod +x
用于添加执行权限,否则会提示Permission denied
。./
表示当前目录,必须显式指定路径,因为当前目录默认不在系统PATH
中。
3. 脚本执行的常见问题与解决
3.1 权限不足
错误提示:Permission denied
解决方法:
chmod +x demo.sh # 赋予执行权限
3.2 路径错误
错误提示:No such file or directory
可能原因:
- 脚本路径错误(如
./demo.sh
写成demo.sh
)。 - 脚本文件格式问题(如 Windows 换行符导致的错误)。
解决方法:
# 检查路径是否正确
ls -l demo.sh # 确认文件存在且路径正确# 转换文件格式为 Unix 格式(若文件来自 Windows)
dos2unix demo.sh # 需先安装 dos2unix 工具
3.3 解释器路径错误
错误提示:Bad interpreter: No such file or directory
可能原因:脚本头部 #!/bin/bash
路径错误。
解决方法:
# 查看系统 Bash 路径
which bash # 通常输出 /bin/bash# 修改脚本头部为正确路径
vim demo.sh # 将第一行改为 #!/usr/bin/env bash(更具可移植性)
4. 脚本调试与验证
4.1 检查执行结果
# 执行脚本并查看输出
./demo.sh # 正常输出:Hello, World!# 检查命令执行状态(0 表示成功)
echo $? # 输出:0
4.2 调试模式
# 启用调试模式,显示每行执行的命令
bash -x demo.sh
4.3 错误处理
# 脚本遇到错误时立即退出
set -e# 捕获错误并输出信息
trap 'echo "错误发生在第 $LINENO 行"' ERR
5. 脚本优化与进阶
5.1 输出重定向
# 将输出保存到文件(覆盖原有内容)
./demo.sh > output.log# 追加输出到文件
./demo.sh >> output.log
5.2 输入重定向
# 从文件读取输入
cat input.txt | ./demo.sh
5.3 环境变量与脚本交互
# 在脚本中引用环境变量
echo "当前用户:$USER,主目录:$HOME"# 导出自定义变量到子进程
export MY_VAR="自定义变量"
6. 实战案例:批量创建用户
#!/bin/bash
# 批量创建用户脚本# 定义用户列表
users=("user1" "user2" "user3")# 遍历用户列表并创建用户
for user in ${users[@]}; douseradd $user && echo "用户 $user 创建成功" || echo "用户 $user 创建失败"
done
执行步骤:
- 保存脚本为
create_users.sh
。 - 赋予执行权限:
chmod +x create_users.sh
。 - 执行脚本:
./create_users.sh
。
7. 常见易错点总结
- 变量赋值空格:
a = 10
错误,必须为a=10
。 - 中括号空格:
[条件]
需写成[ 条件 ]
(如[ $a -gt 5 ]
)。 - 路径问题:执行脚本需用
./脚本名
,直接输入脚本名
会提示 “命令未找到”。 - 转义符遗漏:使用
expr
计算乘法时,*
需转义为\*
(如expr 5 \* 3
)。 - 文件格式错误:Windows 格式文件需转换为 Unix 格式(使用
dos2unix
)。
8. 拓展知识
8.1 脚本可移植性
- 推荐头部:
#!/usr/bin/env bash
,使用env
命令自动查找 Bash 路径,避免硬编码。 - 兼容性检查:使用
sh
命令测试脚本在旧版 Shell 中的运行情况。
8.2 权限设置最佳实践
- 最小权限原则:仅赋予脚本所有者执行权限(
chmod u+x 脚本名
)。 - 特殊权限:
setuid
(chmod u+s 脚本名
)允许普通用户以脚本所有者权限执行。
8.3 脚本性能优化
- 使用内置命令:优先使用 Bash 内置命令(如
echo
、cd
),避免调用外部程序。 - 减少 I/O 操作:将多次
echo
合并为一次输出,或使用printf
提升效率。
通过以上步骤,你可以全面掌握 Shell 脚本的基础操作,从创建、编辑到执行、调试,再到优化和实战应用。建议结合实际案例反复练习,加深对脚本执行原理的理解。
三、变量:脚本的 “数据细胞”
1. 变量基础:从存储到操作
1.1 变量定义与引用
定义变量:
name="Alice" # 字符串变量(值含空格需用引号包裹)
age=25 # 数值变量(本质为字符串,可参与计算)
file_path="/etc/passwd" # 路径变量
关键规则:
- 变量名必须以字母或下划线开头,区分大小写(如
Name
和name
是不同变量)。 - 等号两边不能有空格(
name = "Alice"
会报错)。
引用变量:
echo "姓名:$name,年龄:${age}岁"
# 输出:姓名:Alice,年龄:25岁
推荐写法:使用 ${变量名}
避免歧义,例如:
fruit="apple"
echo "${fruit}s" # 输出:apples(正确)
echo "$fruits" # 输出:(错误,变量名歧义)
1.2 单引号 vs 双引号
符号 | 特性 | 示例 |
---|---|---|
'' | 原样输出,不解析变量和转义符 | echo '$name' → $name |
"" | 解析变量和转义符(如 \n 换行) | echo "$name\n" → Alice 换行 |
实战场景:
msg='当前用户:$USER,主目录:$HOME'
echo $msg # 输出:当前用户:$USER,主目录:$HOMEmsg="当前用户:$USER,主目录:$HOME"
echo $msg # 输出:当前用户:root,主目录:/root
2. 数值计算:从基础到高级
2.1 算术运算符
运算符 | 示例(a=10 , b=3 ) | 结果 |
---|---|---|
+ | $((a + b)) | 13 |
- | $((a - b)) | 7 |
* | $((a * b)) | 30 |
/ | $((a / b)) | 3 |
% | $((a % b)) | 1 |
推荐方法:
# 方法 1:$(( ))(简洁高效)
sum=$((5 + 3)) # 8
product=$((5 * 3)) # 15# 方法 2:expr(需转义乘号)
sum=$(expr 5 + 3) # 8
product=$(expr 5 \* 3) # 15(注意 `\*`)
2.2 数值运算实战
案例:计算圆的面积
#!/bin/bash
radius=5
area=$((3.14 * radius * radius)) # 注意:整数运算会截断小数
echo "半径为 $radius 的圆面积:$area" # 输出:78(实际应为 78.5)
改进方案:
area=$(echo "3.14 * $radius * $radius" | bc) # 使用 bc 工具支持浮点运算
echo "半径为 $radius 的圆面积:$area" # 输出:78.5
3. 标准变量:系统级的数据仓库
3.1 常用环境变量
变量名 | 含义 | 示例(管理员用户) |
---|---|---|
$HOME | 用户主目录 | /root |
$PWD | 当前工作目录 | /home/user/scripts |
$USER | 当前用户名 | root |
$PATH | 命令搜索路径 | :/usr/bin:/bin |
$HOSTNAME | 主机名 | localhost.localdomain |
实战示例:
echo "当前用户:$USER,主目录:$HOME"
# 输出:当前用户:root,主目录:/root
3.2 永久设置环境变量
- 临时生效(当前终端有效):
export MY_VAR="自定义变量"
- 永久生效(所有终端有效):
# 编辑用户配置文件 nano ~/.bashrc # 在文件末尾添加 export MY_VAR="自定义变量" # 使配置生效 source ~/.bashrc
4. 特殊变量:脚本参数与状态
4.1 位置参数
变量 | 含义 | 示例(脚本 test.sh 1 2 "a b" ) |
---|---|---|
$0 | 脚本名称 | test.sh |
$1~$9 | 第 1 到第 9 个参数 | $1=1 , $2=2 , $3=a b |
${10} | 第 10 个参数(需用大括号) | $10=10 (若参数足够) |
示例脚本 args.sh
:
#!/bin/bash
echo "脚本名:$0"
echo "第一个参数:$1"
echo "第十个参数:${10}"
运行:
./args.sh 1 2 3 4 5 6 7 8 9 10
# 输出:
# 脚本名:./args.sh
# 第一个参数:1
# 第十个参数:10
4.2 其他特殊变量
变量 | 含义 | 示例(脚本 test.sh ) |
---|---|---|
$# | 参数个数 | 3 (若传递 3 个参数) |
$@ | 所有参数(独立字符串) | 1 2 "a b" |
$* | 所有参数(单个字符串) | 1 2 a b (空格丢失) |
$$ | 脚本进程号(PID) | 12345 (实际 PID) |
$? | 上条命令退出状态(0 = 成功) | 0 (若命令成功) |
实战案例:
#!/bin/bash
echo "参数个数:$#"
echo "所有参数(\$@):$@"
echo "所有参数(\$*):$*"
运行:
./test.sh hello "world!"
# 输出:
# 参数个数:2
# 所有参数($@):hello world!
# 所有参数($*):hello world!
5. 变量作用域:从全局到局部
5.1 全局变量
定义:在脚本任何位置定义的变量,默认在整个脚本有效。
#!/bin/bash
global_var="全局变量"function show_var() {echo "函数内访问全局变量:$global_var"
}show_var # 输出:函数内访问全局变量:全局变量
echo "函数外访问全局变量:$global_var" # 输出:函数外访问全局变量:全局变量
5.2 局部变量
定义:使用 local
关键字在函数内定义的变量,仅在函数内有效。
#!/bin/bash
function local_var_demo() {local local_var="局部变量" # 仅函数内有效echo "函数内访问局部变量:$local_var"
}local_var_demo # 输出:函数内访问局部变量:局部变量
echo "函数外访问局部变量:$local_var" # 输出:函数外访问局部变量:(空)
6. 高级变量操作:让脚本更灵活
6.1 变量替换
语法 | 作用 | 示例(str="hello world" ) |
---|---|---|
${str#h*o} | 从头部删除最短匹配 h*o | world |
${str##h*o} | 从头部删除最长匹配 h*o | rld |
${str%ld} | 从尾部删除最短匹配 ld | hello wor |
${str%%ld} | 从尾部删除最长匹配 ld | hello wor |
${str/world/Shell} | 替换第一个匹配项 | hello Shell |
${str//l/LL} | 替换所有匹配项 | heLLo worLLd |
实战案例:
path="/home/user/documents/report.txt"
# 提取文件名
filename=${path##*/} # 输出:report.txt
# 提取文件类型
extension=${filename##*.} # 输出:txt
6.2 命令替换
语法:
变量=$(命令) # 推荐写法
变量=`命令` # 反引号写法(易混淆)
示例:
# 获取当前日期
date=$(date +%Y-%m-%d)
echo "今天日期:$date" # 输出:今天日期:2023-10-01# 获取文件行数
line_count=$(wc -l < /etc/passwd)
echo "用户文件行数:$line_count" # 输出:用户文件行数:42
7. 常见易错点与解决方案
7.1 变量赋值空格错误
错误示例:
name = "Alice" # 报错:-bash: name: 未找到命令
解决方案:
name="Alice" # 正确写法
7.2 中括号条件判断空格缺失
错误示例:
if [ $age -gt 18 ]; then # 正确
if [ $age-gt 18 ]; then # 错误(缺少空格)
7.3 数组定义逗号分隔
错误示例:
names=("Kanye", "Edison", "Fish") # 错误(逗号分隔)
解决方案:
names=("Kanye" "Edison" "Fish") # 正确(空格分隔)
7.4 变量命名冲突
错误示例:
USER="自定义用户" # 覆盖系统变量 $USER
解决方案:
user="自定义用户" # 使用小写字母避免冲突
8. 拓展知识:让变量更强大
8.1 只读变量
readonly PI=3.14 # 定义只读变量
PI=3.1415 # 报错:PI: 只读变量
8.2 删除变量
name="Alice"
unset name # 删除变量
echo $name # 输出:(空)
8.3 变量类型转换
num="123"
echo $((num + 100)) # 输出:223(自动转换为整数)
9. 实战案例:变量综合应用
9.1 批量重命名文件
#!/bin/bash
# 将当前目录下所有 .txt 文件重命名为 .log
for file in *.txt; donew_name="${file%.txt}.log" # 替换扩展名mv "$file" "$new_name"echo "重命名:$file → $new_name"
done
9.2 动态获取系统信息
#!/bin/bash
# 获取系统负载、内存使用、用户数
load=$(uptime | awk -F 'load average:' '{print $2}' | cut -d ',' -f 1)
mem_used=$(free -h | awk '/Mem:/ {print $3}')
user_count=$(who | wc -l)echo "系统负载:$load"
echo "内存使用:$mem_used"
echo "在线用户:$user_count"
10. 总结:变量的 “生存法则”
- 命名规范:小写字母开头,避免与系统变量冲突。
- 引号使用:值含空格或特殊字符时,优先使用双引号。
- 作用域控制:函数内变量使用
local
声明,避免全局污染。 - 性能优化:算术运算用
$(( ))
,命令替换用$( )
。
通过以上内容,你将掌握 Shell 变量的核心操作,从基础定义到高级应用,再到实战案例,逐步提升脚本编写能力。变量是 Shell 编程的基石,熟练运用它们能让你的脚本更灵活、高效!
四、运算符与条件判断
1. 关系运算符(判断条件)
(1)数字比较
运算符 | 含义 | 示例(a=10 ,b=20 ) |
---|---|---|
-eq | 等于 | [ $a -eq $b ] → 假 |
-ne | 不等于 | [ $a -ne $b ] → 真 |
-gt | 大于 | [ $a -gt $b ] → 假 |
-lt | 小于 | [ $a -lt $b ] → 真 |
-ge | 大于等于 | [ $a -ge $b ] → 假 |
-le | 小于等于 | [ $a -le $b ] → 真 |
(2)字符串比较
运算符 | 含义 | 示例 |
---|---|---|
-z | 字符串为空 | [ -z "" ] → 真 |
-n | 字符串非空 | [ -n "abc" ] → 真 |
== | 字符串相等 | [ "a" == "a" ] → 真 |
!= | 字符串不等 | [ "a" != "b" ] → 真 |
\> | 字符串排序大于(需转义) | [ "b" \> "a" ] → 真 |
\< | 字符串排序小于(需转义) | [ "a" \< "b" ] → 真 |
(3)文件判断
运算符 | 含义 | 示例 |
---|---|---|
-e | 文件 / 目录存在 | [ -e /etc/passwd ] → 真 |
-f | 是普通文件 | [ -f first_script.sh ] → 真(若文件存在) |
-d | 是目录 | [ -d /home ] → 真 |
-r | 文件可读 | [ -r /etc/shadow ] → 假(普通用户不可读) |
-w | 文件可写 | [ -w first_script.sh ] → 真(若有写权限) |
-x | 文件可执行 | [ -x first_script.sh ] → 真(若有执行权限) |
2. 逻辑运算符(组合条件)
运算符 | 含义 | 示例 |
---|---|---|
-a | 逻辑与(AND) | [ $a -gt 5 -a $a -lt 15 ] → a 在 6-14 之间为真 |
-o | 逻辑或(OR) | [ -f file -o -d dir ] → 文件或目录存在为真 |
! | 逻辑非(NOT) | [ ! -e file ] → 文件不存在为真 |
注意:
- 条件判断需用中括号
[ ]
,且括号前后必须留空格(如[ $a -gt 5 ]
,否则报错)。 - 复杂条件可用
&&
和||
(适用于命令级逻辑,如command1 && command2
表示command1
成功后执行command2
)。
五、数组:批量数据处理
1. 定义数组
- 方式 1:直接赋值(下标从 0 开始)
fruits=("apple" "banana" "orange") # 定义包含三个元素的数组
- 方式 2:指定下标(支持稀疏数组)
numbers[0]=10 numbers[2]=30 # 下标 1 未定义,值为空
- 方式 3:省略下标(自动递增)
array=() array+=("one") # 追加元素 array+=("two")
2. 访问数组元素
- 获取单个元素:
${数组名[下标]}
echo ${fruits[1]} # 输出:banana
- 获取所有元素:
${数组名[@]}
或${数组名[*]}
echo ${fruits[@]} # 输出:apple banana orange
- 获取数组长度:
${#数组名[@]}
echo ${#fruits[@]} # 输出:3
- 切片操作(从下标 1 开始,取 2 个元素)
echo ${fruits[@]:1:2} # 输出:banana orange
3. 遍历数组示例
#!/bin/bash
nums=(1 3 5 7 9)
for num in ${nums[@]}; do # 遍历数组所有元素echo "当前数字:$num"
done
# 输出:
# 当前数字:1
# 当前数字:3
# 当前数字:5
# 当前数字:7
# 当前数字:9
六、流程控制:脚本的 “逻辑大脑”
在 Shell 编程中,流程控制是实现复杂逻辑的核心。通过条件判断和循环结构,脚本可以根据不同场景执行不同操作,实现自动化任务。本节将从基础语法到实战案例,逐步解析 Shell 流程控制的核心知识点。
1. 条件判断:让脚本 “会思考”
1.1 if 语句:最基础的条件分支
语法格式:
if [ 条件 ]; then命令1 # 条件为真时执行
elif [ 条件2 ]; then # 可选,多个条件分支命令2
else # 可选,所有条件不满足时执行命令3
fi # 必须以 fi 结束
关键细节:
- 条件表达式:需用中括号
[ ]
包裹,且括号前后必须留空格(如[ $a -gt 5 ]
,否则报错)。 - 文件判断参数:常用
-e
(存在)、-f
(普通文件)、-d
(目录)等(见下表)。
运算符 | 含义 | 示例 |
---|---|---|
-e | 文件 / 目录存在 | [ -e /etc/passwd ] → 真 |
-f | 是普通文件 | [ -f script.sh ] → 真(若文件存在) |
-d | 是目录 | [ -d /home ] → 真 |
示例:判断文件类型
#!/bin/bash
file="./test.txt"if [ -e "$file" ]; then # 文件存在if [ -f "$file" ]; then # 是普通文件echo "文件 $file 是普通文件"elif [ -d "$file" ]; then # 是目录echo "文件 $file 是目录"else # 其他类型(如链接、设备文件)echo "文件 $file 是特殊文件"fi
elseecho "文件 $file 不存在"
fi
1.2 case 语句:模式匹配的高效选择
语法格式:
case 变量 in模式1)命令1;; # 必须用双分号结束分支模式2)命令2;;*) # 通配符:匹配所有未定义的模式命令3;;
esac # 必须以 esac 结束
适用场景:
- 菜单驱动程序(如用户输入 1-5 选择操作)。
- 文件类型判断(如根据扩展名执行不同解压命令)。
示例:简易菜单程序
#!/bin/bash
echo "请选择操作(1-3):"
echo "1. 查看当前目录"
echo "2. 查看系统时间"
echo "3. 退出程序"
read choicecase $choice in1)ls -l # 列出当前目录文件;;2)date +"%Y-%m-%d %H:%M:%S" # 显示当前时间;;3)echo "退出程序"exit 0 # 退出脚本;;*)echo "无效选择!请输入 1-3";;
esac
2. 循环结构:让脚本 “重复执行”
2.1 for 循环:遍历列表或范围
格式 1:遍历列表(新手友好)
for 变量 in 元素1 元素2 元素3; do命令 # 对每个元素执行操作
done
示例:打印所有水果
fruits=("apple" "banana" "orange")
for fruit in ${fruits[@]}; doecho "当前水果:$fruit"
done
# 输出:
# 当前水果:apple
# 当前水果:banana
# 当前水果:orange
格式 2:C 语言风格(指定次数)
for ((初始值; 条件; 增量)); do命令 # 按次数循环
done
示例:计算 1+2+…+10
sum=0
for ((i=1; i<=10; i++)); dosum=$((sum + i))
done
echo "总和:$sum" # 输出:55
2.2 while 循环:条件驱动的重复
语法格式:
while [ 条件 ]; do命令 # 条件为真时持续执行
done
示例:逐行读取文件
#!/bin/bash
file="users.txt"
while read line; do # 每次读取文件一行到变量 lineecho "用户:$line"
done < "$file" # 从文件获取输入(重定向)
2.3 until 循环:反条件循环
语法格式:
until [ 条件 ]; do命令 # 条件为假时持续执行,直到条件为真
done
示例:等待文件生成
until [ -e "data.csv" ]; do # 直到文件存在echo "等待 data.csv 生成..."sleep 1 # 休眠 1 秒
done
echo "文件已生成!"
3. 循环控制:让流程更灵活
3.1 break 与 continue
关键字 | 作用 | 示例 |
---|---|---|
break | 跳出当前循环(类似 C 语言) | for i in 1 2 3; do if [ $i -eq 2 ]; then break; fi; done (仅打印 1) |
continue | 跳过当前循环迭代 | for i in 1 2 3; do if [ $i -eq 2 ]; then continue; fi; echo $i; done (打印 1, 3) |
3.2 嵌套循环:解决复杂逻辑
示例:打印乘法表
for i in {1..9}; dofor j in {1..9}; doecho -n "$i×$j=$((i*j)) " # -n 不换行doneecho # 换行
done
4. 函数:代码复用的 “积木”
4.1 定义与调用函数
语法格式:
# 格式 1(简洁写法)
函数名() {local 变量 # 声明局部变量(仅限函数内使用)命令 # 函数逻辑return 退出码 # 可选,0 表示成功,非 0 表示失败
}# 格式 2(显式声明)
function 函数名() {命令
}
示例:计算两数之和
#!/bin/bash
# 定义函数:接收两个参数,返回和
add() {local a=$1 # 局部变量,避免污染全局作用域local b=$2echo $((a + b)) # 通过 echo 输出结果(推荐)return 0 # 返回成功状态
}# 调用函数并获取结果
result=$(add 5 3)
echo "5 + 3 = $result" # 输出:8
echo "函数返回值:$?" # 输出:0(成功)
4.2 函数参数传递
- 位置参数:函数内通过
$1
、$2
等获取调用时传递的参数。 - 参数验证:调用前检查参数个数,避免空指针错误。
add() {if [ $# -ne 2 ]; then # 检查参数个数是否为 2echo "错误:需要 2 个参数"return 1fi# 逻辑代码 }
5. 常见易错点与解决方案
5.1 中括号空格缺失
错误示例:
if [ $age>18 ]; then # 错误(缺少空格)
if [ $age -gt 18 ]; then # 正确
解决方案:始终在中括号内外留空格([ 条件 ]
)。
5.2 case 分支遗漏 ;;
错误示例:
case $choice in1) echo "选项 1" # 缺少 ;;,导致语法错误
esac
解决方案:每个分支必须以 ;;
结束。
5.3 无限循环陷阱
错误示例:
while [ 1 -eq 1 ]; do # 条件永远为真,导致无限循环echo "陷阱!"
done
解决方案:确保循环条件最终会变为假,或用 break
强制退出。
6. 实战案例:流程控制综合应用
6.1 文件备份脚本
#!/bin/bash
# 功能:判断目录是否存在,存在则备份,否则创建并备份backup_dir="/backup"
source_dir="/data"# 判断备份目录是否存在
if [ ! -d "$backup_dir" ]; thenmkdir -p "$backup_dir" # 创建目录(-p 自动创建父目录)echo "创建备份目录:$backup_dir"
fi# 备份数据(使用时间戳命名备份文件)
timestamp=$(date +%Y%m%d%H%M%S)
tar -czf "$backup_dir/data_$timestamp.tar.gz" "$source_dir"echo "备份完成!文件路径:$backup_dir/data_$timestamp.tar.gz"
6.2 交互式猜数字游戏
#!/bin/bash
# 生成 1-100 随机数
num=$((RANDOM % 100 + 1))
attempts=0 # 记录尝试次数while true; do # 无限循环,直到猜对read -p "请输入一个数字(1-100):" guessattempts=$((attempts + 1))if [ $guess -eq $num ]; thenecho "恭喜!你在 $attempts 次内猜对了!"break # 跳出循环elif [ $guess -gt $num ]; thenecho "猜大了!再试一次。"elseecho "猜小了!再试一次。"fi
done
7. 拓展知识:让流程控制更强大
7.1 复合条件表达式
- 逻辑与:
&&
(如command1 && command2
,仅当 command1 成功时执行 command2)。 - 逻辑或:
||
(如command1 || command2
,仅当 command1 失败时执行 command2)。
7.2 函数递归
示例:计算阶乘(递归实现)
factorial() {local n=$1if [ $n -eq 0 ]; thenecho 1elseecho $((n * $(factorial $((n-1)))))fi
}result=$(factorial 5)
echo "5 的阶乘:$result" # 输出:120
7.3 循环性能优化
- 减少 I/O:将多次
echo
合并为一次,或使用printf
提升效率。 - 避免全局变量:函数内使用
local
声明变量,提高代码可读性和安全性。
8. 总结:流程控制的 “黄金法则”
- 条件判断:善用
if
和case
,复杂逻辑用case
提高可读性。 - 循环选择:列表遍历用
for
,条件驱动用while
,反向条件用until
。 - 函数设计:参数验证、局部变量、明确返回值,提升代码复用性。
- 调试技巧:用
set -x
开启调试模式,查看循环和条件的执行流程。
通过掌握流程控制,你将能编写具备 “智能” 的 Shell 脚本,实现从简单任务到复杂自动
七、函数:代码复用的核心
在 Shell 编程中,函数是实现代码复用和模块化的关键。通过将重复或通用的逻辑封装为函数,不仅能减少代码冗余,还能提高脚本的可读性和维护性。本节将从函数的基础语法出发,结合实战案例,逐步解析函数的核心知识点。
1. 函数基础:从定义到调用
1.1 函数定义的两种格式
格式 1:简洁写法(推荐新手)
函数名() {命令1命令2return 退出码 # 可选,默认返回最后一条命令的状态(0-255)
}
格式 2:显式声明(清晰易读)
function 函数名() {命令
}
关键说明:
function
关键字可选,但显式声明能提高代码可读性。return
用于指定退出码(0 表示成功,非 0 表示失败),省略时返回最后一条命令的状态。
示例:定义一个打招呼函数
greet() {echo "Hello, $1!" # $1 是函数的第一个参数return 10 # 手动设置返回码为 10
}
1.2 调用函数与参数传递
语法:
函数名 参数1 参数2 参数3 # 参数之间用空格分隔
示例:调用打招呼函数
greet "Alice" # 输出:Hello, Alice!
echo "函数返回码:$?" # 输出:10(通过 $? 获取返回码)
2. 参数处理:让函数更灵活
2.1 位置参数:函数的 “输入变量”
变量 | 含义 | 示例(函数调用 add 5 3 ) |
---|---|---|
$1 | 第一个参数 | 5 |
$2 | 第二个参数 | 3 |
$# | 参数个数 | 2 |
$@ | 所有参数(独立字符串) | 5 3 |
示例:计算两数之和的函数
add() {local sum=$(( $1 + $2 )) # local 声明局部变量,避免污染全局作用域echo "和为:$sum" # 输出结果(推荐通过 echo 返回数据)return 0 # 返回成功状态码
}# 调用函数并获取结果
result=$(add 5 3) # 将函数输出赋值给变量
echo "计算结果:$result" # 输出:计算结果:8
2.2 参数验证:避免无效输入
场景:当函数需要固定数量的参数时,先检查参数个数。
add() {if [ $# -ne 2 ]; then # 检查参数是否为 2 个echo "错误:需要 2 个参数,实际 $# 个"return 1 # 返回错误码filocal sum=$(( $1 + $2 ))echo $sum
}# 调用错误示例
add 5 # 输出:错误:需要 2 个参数,实际 1 个
echo "返回码:$?" # 输出:1
3. 变量作用域:避免 “变量污染”
3.1 全局变量:脚本内处处可见
特点:在函数外定义的变量,或函数内未用 local
声明的变量,均可在全局访问。
global_var="全局变量"show_global() {echo "函数内访问:$global_var" # 可直接访问全局变量
}show_global # 输出:函数内访问:全局变量
echo "函数外访问:$global_var" # 输出:函数外访问:全局变量
3.2 局部变量:函数内的 “私有数据”
语法:用 local
关键字声明,仅在函数内有效。
function local_demo() {local local_var="局部变量" # 局部变量echo "函数内:$local_var"
}local_demo # 输出:函数内:局部变量
echo "函数外:$local_var" # 输出:(空,外部无法访问)
4. 返回值:状态与数据的双重传递
4.1 返回码(状态值)
- 用途:通过
return
声明,用于表示函数执行是否成功(0 = 成功,非 0 = 失败)。 - 获取方式:调用后通过
$?
获取。
check_file() {if [ -e "$1" ]; thenreturn 0 # 文件存在,返回成功码elsereturn 1 # 文件不存在,返回错误码fi
}check_file "test.sh"
if [ $? -eq 0 ]; thenecho "文件存在"
elseecho "文件不存在"
fi
4.2 数据返回(推荐方式)
- 用途:通过
echo
或printf
输出数据,适用于返回字符串、数值等复杂结果。 - 获取方式:用命令替换
$(函数名)
接收输出。
get_current_time() {date +"%Y-%m-%d %H:%M:%S" # 直接输出时间
}time_now=$(get_current_time)
echo "当前时间:$time_now" # 输出:当前时间:2023-10-01 15:30:00
5. 高级技巧:让函数更强大
5.1 函数递归:用循环逻辑解决复杂问题
场景:计算阶乘、斐波那契数列等递归问题。
# 计算 n 的阶乘(递归实现)
factorial() {local n=$1if [ $n -eq 0 ]; thenecho 1 # 递归终止条件elseecho $(( $n * $(factorial $((n-1))) )) # 递归调用fi
}result=$(factorial 5)
echo "5 的阶乘:$result" # 输出:120
5.2 默认参数:让函数更 “智能”
语法:通过 ${参数:-默认值}
实现参数默认值。
greet() {local name=${1:-"Guest"} # 若未传参,默认值为 "Guest"echo "Hello, $name!"
}greet # 输出:Hello, Guest!(未传参时用默认值)
greet "Alice" # 输出:Hello, Alice!(传参时用实际值)
5.3 可变参数:处理不确定数量的输入
场景:函数需要接收任意数量的参数(如日志函数记录多个信息)。
log() {local timestamp=$(date +"%Y-%m-%d %H:%M:%S")echo "[${timestamp}] $*" # $* 表示所有参数(视为单个字符串)
}log "用户登录" "IP: 192.168.1.1" # 输出:[2023-10-01 15:30:00] 用户登录 IP: 192.168.1.1
6. 常见易错点与解决方案
6.1 忘记声明局部变量导致全局污染
错误示例:
count=0 # 全局变量
increment() {count=$((count + 1)) # 未用 local,修改全局变量
}increment
echo "全局 count:$count" # 输出:1(全局变量被修改)
解决方案:在函数内用 local
声明变量:
increment() {local count=$((count + 1)) # 局部变量,不影响全局
}
6.2 参数索引错误(如 $0 误用)
错误示例:
add() {echo $0 # 输出脚本名,而非第一个参数($0 是脚本名,参数从 $1 开始)
}
解决方案:牢记函数内参数从 $1
开始,$0
是脚本名。
6.3 返回码与数据返回混淆
错误做法:用 return
返回数据(仅支持 0-255 的整数)。
add() {return $((5 + 3)) # 错误,return 只能返回状态码
}
正确做法:用 echo
输出数据,return
仅用于状态码。
7. 实战案例:函数综合应用
7.1 文件操作函数库
需求:封装常用文件操作函数,如创建目录、复制文件。
#!/bin/bash# 函数 1:创建目录(带错误处理)
create_dir() {local dir=$1if [ -d "$dir" ]; thenecho "目录 $dir 已存在"return 1fimkdir -p "$dir"if [ $? -eq 0 ]; thenecho "目录 $dir 创建成功"return 0elseecho "目录 $dir 创建失败"return 1fi
}# 函数 2:复制文件到目录
copy_file() {local src=$1local dest_dir=$2if [ ! -f "$src" ]; thenecho "源文件 $src 不存在"return 1fiif [ ! -d "$dest_dir" ]; thencreate_dir "$dest_dir" # 调用其他函数if [ $? -ne 0 ]; thenreturn 1fificp "$src" "$dest_dir"echo "文件 $src 复制到 $dest_dir 成功"return 0
}# 调用函数
copy_file "data.txt" "backup"
7.2 交互式菜单函数
需求:通过函数实现菜单驱动的用户交互。
show_menu() {echo "===== 菜单 ====="echo "1. 查看系统信息"echo "2. 退出程序"echo "================"
}handle_choice() {local choice=$1case $choice in1)uname -a # 显示系统信息;;2)echo "退出程序"exit 0;;*)echo "无效选择!";;esac
}# 主程序
while true; doshow_menu # 调用菜单函数read -p "请选择:" choicehandle_choice "$choice" # 调用选择处理函数
done
8. 拓展知识:函数的进阶应用
8.1 函数库管理
- 创建函数文件:将常用函数保存到独立文件(如
utils.sh
)。# utils.sh 内容 function add() { ... } function greet() { ... }
- 引入函数库:通过
source
命令在脚本中引用。source utils.sh # 使 utils.sh 中的函数在当前脚本生效 add 5 3 # 直接调用
8.2 函数调试技巧
- 开启调试模式:用
set -x
跟踪函数执行步骤。set -x # 开启调试 add 5 3 # 显示每一步执行的命令 set +x # 关闭调试
- 打印参数信息:在函数开头输出参数,确认输入是否正确。
add() {echo "接收到的参数:$1, $2" # 调试用输出... }
9. 总结:函数的 “复用哲学”
- 代码复用:将重复逻辑封装为函数,避免 “重复造轮子”。
- 模块化设计:每个函数专注于一个独立功能(如文件操作、数据计算),提高可维护性。
- 错误处理:通过参数验证和返回码,让函数更健壮。
掌握函数后,你将从 “编写零散命令” 进阶到 “构建结构化脚本”。建议从简单函数开始,逐步积累常用工具函
八、实战案例:判断闰年
需求
输入年份,判断是否为闰年(闰年条件:能被 4 整除且不能被 100 整除,或能被 400 整除)。
脚本实现
#!/bin/bash
read -p "请输入年份:" year# 组合条件:(year%400==0) 或 (year%4==0 且 year%100!=0)
if [ $((year % 400)) -eq 0 ] || [ \( $((year % 4)) -eq 0 -a $((year % 100)) -ne 0 \) ]; thenecho "$year 是闰年"
elseecho "$year 是平年"
fi
关键点
\(` 和 `\)
用于转义括号,确保条件正确组合。||
表示逻辑或,-a
表示逻辑与。
九、常见易错点总结
- 变量赋值空格:
a = 10
错误,必须为a=10
(等号前后不能有空格)。 - 中括号空格:
[条件]
需写成[ 条件 ]
,否则报错(如[a -gt 5]
错误,应为[ $a -gt 5 ]
)。 - 文件路径错误:执行脚本时需用
./脚本名
,直接输入脚本名
会提示 “命令未找到”。 - 转义符遗漏:使用
expr
计算乘法时,*
需转义为\*
,或改用$(( ))
避免转义。 - 字符串比较误区:比较字符串是否相等时,
=
前后需留空格(如[ "$a" = "$b" ]
),否则会被视为赋值。
十、总结
Shell 编程是 Linux 自动化的核心技能,从简单的脚本到复杂的流程控制,需要通过大量实践掌握。新手入门时,建议:
- 从单个知识点入手,如变量、循环、函数,逐个击破。
- 每学一个语法,编写小例子验证效果,理解背后逻辑。
- 遇到错误时,善用
echo
打印变量值,或用bash -x 脚本名
调试(显示每行执行过程)。
记住:Shell 脚本的魅力在于 “用简单命令组合实现强大功能”,坚持练习,你会逐渐体会到它的高效与便捷!