linux入门六:Linux Shell 编程

发布于:2025-04-11 ⋅ 阅读:(39) ⋅ 点赞:(0)

一、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
可能原因

  1. 脚本路径错误(如 ./demo.sh 写成 demo.sh)。
  2. 脚本文件格式问题(如 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[@]}; do
  useradd $user && echo "用户 $user 创建成功" || echo "用户 $user 创建失败"
done

执行步骤

  1. 保存脚本为 create_users.sh
  2. 赋予执行权限:chmod +x create_users.sh
  3. 执行脚本:./create_users.sh

7. 常见易错点总结

  1. 变量赋值空格a = 10 错误,必须为 a=10
  2. 中括号空格[条件] 需写成 [ 条件 ](如 [ $a -gt 5 ])。
  3. 路径问题:执行脚本需用 ./脚本名,直接输入 脚本名 会提示 “命令未找到”。
  4. 转义符遗漏:使用 expr 计算乘法时,* 需转义为 \*(如 expr 5 \* 3)。
  5. 文件格式错误:Windows 格式文件需转换为 Unix 格式(使用 dos2unix)。

8. 拓展知识

8.1 脚本可移植性
  • 推荐头部#!/usr/bin/env bash,使用 env 命令自动查找 Bash 路径,避免硬编码。
  • 兼容性检查:使用 sh 命令测试脚本在旧版 Shell 中的运行情况。
8.2 权限设置最佳实践
  • 最小权限原则:仅赋予脚本所有者执行权限(chmod u+x 脚本名)。
  • 特殊权限setuidchmod u+s 脚本名)允许普通用户以脚本所有者权限执行。
8.3 脚本性能优化
  • 使用内置命令:优先使用 Bash 内置命令(如 echocd),避免调用外部程序。
  • 减少 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,主目录:$HOME

msg="当前用户:$USER,主目录:$HOME"
echo $msg  # 输出:当前用户:root,主目录:/root

2. 数值计算:从基础到高级

2.1 算术运算符
运算符 示例(a=10b=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 永久设置环境变量
  1. 临时生效(当前终端有效):
    export MY_VAR="自定义变量"
    
  2. 永久生效(所有终端有效):
    # 编辑用户配置文件
    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; do
  new_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=10b=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
else
  echo "文件 $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 choice

case $choice in
  1)
    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[@]}; do
  echo "当前水果:$fruit"
done
# 输出:
# 当前水果:apple
# 当前水果:banana
# 当前水果:orange

格式 2:C 语言风格(指定次数)

for ((初始值; 条件; 增量)); do
  命令  # 按次数循环
done

示例:计算 1+2+…+10

sum=0
for ((i=1; i<=10; i++)); do
  sum=$((sum + i))
done
echo "总和:$sum"  # 输出:55
2.2 while 循环:条件驱动的重复

语法格式

while [ 条件 ]; do
  命令  # 条件为真时持续执行
done

示例:逐行读取文件

#!/bin/bash
file="users.txt"
while read line; do  # 每次读取文件一行到变量 line
  echo "用户:$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}; do
  for j in {1..9}; do
    echo -n "$i×$j=$((i*j)) "  # -n 不换行
  done
  echo  # 换行
done

4. 函数:代码复用的 “积木”

4.1 定义与调用函数

语法格式

# 格式 1(简洁写法)
函数名() {
  local 变量  # 声明局部变量(仅限函数内使用)
  命令       # 函数逻辑
  return 退出码  # 可选,0 表示成功,非 0 表示失败
}

# 格式 2(显式声明)
function 函数名() {
  命令
}

示例:计算两数之和

#!/bin/bash
# 定义函数:接收两个参数,返回和
add() {
  local a=$1  # 局部变量,避免污染全局作用域
  local b=$2
  echo $((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  # 检查参数个数是否为 2
        echo "错误:需要 2 个参数"
        return 1
      fi
      # 逻辑代码
    }
    

5. 常见易错点与解决方案

5.1 中括号空格缺失

错误示例

if [ $age>18 ]; then  # 错误(缺少空格)
if [ $age -gt 18 ]; then  # 正确

解决方案:始终在中括号内外留空格([ 条件 ])。

5.2 case 分支遗漏 ;;

错误示例

case $choice in
  1) 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" ]; then
  mkdir -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):" guess
  attempts=$((attempts + 1))

  if [ $guess -eq $num ]; then
    echo "恭喜!你在 $attempts 次内猜对了!"
    break  # 跳出循环
  elif [ $guess -gt $num ]; then
    echo "猜大了!再试一次。"
  else
    echo "猜小了!再试一次。"
  fi
done

7. 拓展知识:让流程控制更强大

7.1 复合条件表达式
  • 逻辑与&&(如 command1 && command2,仅当 command1 成功时执行 command2)。
  • 逻辑或||(如 command1 || command2,仅当 command1 失败时执行 command2)。
7.2 函数递归

示例:计算阶乘(递归实现)

factorial() {
  local n=$1
  if [ $n -eq 0 ]; then
    echo 1
  else
    echo $((n * $(factorial $((n-1)))))
  fi
}

result=$(factorial 5)
echo "5 的阶乘:$result"  # 输出:120
7.3 循环性能优化
  • 减少 I/O:将多次 echo 合并为一次,或使用 printf 提升效率。
  • 避免全局变量:函数内使用 local 声明变量,提高代码可读性和安全性。

8. 总结:流程控制的 “黄金法则”

  1. 条件判断:善用 if 和 case,复杂逻辑用 case 提高可读性。
  2. 循环选择:列表遍历用 for,条件驱动用 while,反向条件用 until
  3. 函数设计:参数验证、局部变量、明确返回值,提升代码复用性。
  4. 调试技巧:用 set -x 开启调试模式,查看循环和条件的执行流程。

通过掌握流程控制,你将能编写具备 “智能” 的 Shell 脚本,实现从简单任务到复杂自动

七、函数:代码复用的核心

在 Shell 编程中,函数是实现代码复用和模块化的关键。通过将重复或通用的逻辑封装为函数,不仅能减少代码冗余,还能提高脚本的可读性和维护性。本节将从函数的基础语法出发,结合实战案例,逐步解析函数的核心知识点。

1. 函数基础:从定义到调用

1.1 函数定义的两种格式

格式 1:简洁写法(推荐新手)

函数名() {
  命令1
  命令2
  return 退出码  # 可选,默认返回最后一条命令的状态(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  # 返回错误码
  fi
  local 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" ]; then
    return 0  # 文件存在,返回成功码
  else
    return 1  # 文件不存在,返回错误码
  fi
}

check_file "test.sh"
if [ $? -eq 0 ]; then
  echo "文件存在"
else
  echo "文件不存在"
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=$1
  if [ $n -eq 0 ]; then
    echo 1  # 递归终止条件
  else
    echo $(( $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=$1
  if [ -d "$dir" ]; then
    echo "目录 $dir 已存在"
    return 1
  fi
  mkdir -p "$dir"
  if [ $? -eq 0 ]; then
    echo "目录 $dir 创建成功"
    return 0
  else
    echo "目录 $dir 创建失败"
    return 1
  fi
}

# 函数 2:复制文件到目录
copy_file() {
  local src=$1
  local dest_dir=$2
  if [ ! -f "$src" ]; then
    echo "源文件 $src 不存在"
    return 1
  fi
  if [ ! -d "$dest_dir" ]; then
    create_dir "$dest_dir"  # 调用其他函数
    if [ $? -ne 0 ]; then
      return 1
    fi
  fi
  cp "$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=$1
  case $choice in
    1)
      uname -a  # 显示系统信息
      ;;
    2)
      echo "退出程序"
      exit 0
      ;;
    *)
      echo "无效选择!"
      ;;
  esac
}

# 主程序
while true; do
  show_menu  # 调用菜单函数
  read -p "请选择:" choice
  handle_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 \) ]; then
  echo "$year 是闰年"
else
  echo "$year 是平年"
fi

关键点

  • \(` 和 `\) 用于转义括号,确保条件正确组合。
  • || 表示逻辑或,-a 表示逻辑与。

九、常见易错点总结

  1. 变量赋值空格a = 10 错误,必须为 a=10(等号前后不能有空格)。
  2. 中括号空格[条件] 需写成 [ 条件 ],否则报错(如 [a -gt 5] 错误,应为 [ $a -gt 5 ])。
  3. 文件路径错误:执行脚本时需用 ./脚本名,直接输入 脚本名 会提示 “命令未找到”。
  4. 转义符遗漏:使用 expr 计算乘法时,* 需转义为 \*,或改用 $(( )) 避免转义。
  5. 字符串比较误区:比较字符串是否相等时,= 前后需留空格(如 [ "$a" = "$b" ]),否则会被视为赋值。

十、总结

Shell 编程是 Linux 自动化的核心技能,从简单的脚本到复杂的流程控制,需要通过大量实践掌握。新手入门时,建议:

  1. 从单个知识点入手,如变量、循环、函数,逐个击破。
  2. 每学一个语法,编写小例子验证效果,理解背后逻辑。
  3. 遇到错误时,善用 echo 打印变量值,或用 bash -x 脚本名 调试(显示每行执行过程)。

记住:Shell 脚本的魅力在于 “用简单命令组合实现强大功能”,坚持练习,你会逐渐体会到它的高效与便捷!