shell编程从0基础--进阶 1
第一部分:Shell基础概念
什么是Shell?
Shell是操作系统的命令解释器,它连接用户和操作系统内核。简单说,就是你输入命令,它帮你执行。
常见的Shell类型
- Bash:最常用,Linux默认
- Zsh:功能更强大
- Sh:最基础的Shell
一、脚本基础结构命令
1. 脚本声明(Shebang)
命令格式:
#!/bin/bash
#!/bin/bash -x
# 上面这行同时启用了调试模式(-x选项)
echo "这行会显示执行过程"#!/usr/bin/env bash
# 这种写法更灵活,能适应不同系统的bash位置
echo "适用于多种Linux发行版"#!/bin/sh
# 使用POSIX兼容模式(更严格,确保跨平台兼容性)
# 注意:某些bash特性不可用
说明:
- 必须是脚本第一行
- 指定解释器路径,告诉系统用什么程序执行此脚本
- 常见解释器:
/bin/bash
,/bin/sh
,/usr/bin/env bash
2. 注释规范
完整脚本示例:
#!/bin/bash# 这是一个注释echo "Hello from my first script!"
# 单行注释: '
多行注释
可以包含任意内容
'<<COMMENT
另一种多行注释方式 (这种有时会报错)
COMMENT
#字符串赋值
name="zz-zjx"
greeting="Hello,$name"#数字赋值
<<222 count=10
max_rerties=3
dsd
#
ds
222
echo $greeting#222 可以换成任意配对字符
3. 严格模式设置
命令格式:
set -euo pipefail
说明:
-e
:命令失败时立即退出脚本-u
:引用未定义变量时报错-o pipefail
:管道中任一命令失败则整个管道失败
脚本示例:
#!/bin/bashset -euo pipefail# 如果前面的命令失败,脚本会在这里停止cp source.txt destination.txtecho "文件复制成功!"
二、变量操作命令
1. 变量定义与赋值
命令格式:
# 基本赋值
变量名=值# 命令替换赋值
变量名=$(命令)
变量名=`命令` # 旧式语法,不推荐嵌套使用# 算术赋值
let "变量名=表达式"
(( 变量名=表达式 ))# 声明特定类型变量
declare [-选项] 变量名=值
typeset [-选项] 变量名=值 # 与declare等效
关键规则:
- 等号两边不能有空格
- 值可以是字符串、数字或命令输出
- 变量名区分大小写
declare/typeset选项:
常用选项详解
选项 含义 示例 说明 -a
声明为普通数组(索引数组) declare -a fruits=("apple" "banana")
支持通过数字索引访问元素(如 ${fruits[0]}
)-A
声明为关联数组(键值对) declare -A user=( ["name"]="Alice" ["age"]=25 )
支持通过字符串键访问元素(如 ${user["name"]}
)-i
声明为整数型变量 declare -i count=10
强制变量只能存储整数,赋值时会自动转换(如 count="20abc"
会被转为20
)-r
声明为只读变量 declare -r PI=3.14
变量不可修改或删除(类似 readonly
)-x
声明为环境变量 declare -x PATH="/usr/local/bin:$PATH"
等效于 export
,变量对子进程可见-p
显示变量的属性和值 declare -p count
输出变量的类型、值和属性(如 declare -i count="10"
)-f
显示函数定义 declare -f my_function
列出已定义的函数及其代码 -F
仅显示函数名 declare -F
不显示函数体,仅列出函数名称 -g
在函数中声明全局变量 bar() { declare -g global_var=100; }
在函数内部声明的变量为全局作用域 +
取消属性 declare +i count
移除变量的 -i
属性(恢复为字符串类型)
declare
与直接赋值的区别
方式 | 示例 | 特点 |
---|---|---|
直接赋值 | var="hello" | 简单赋值,变量默认为字符串类型 |
declare | declare -i var=10 | 可设置变量类型(如整数)、只读属性、数组类型等 |
对比 | var=10 vs declare -i var=10 | 直接赋值的 var 是字符串,declare -i 的 var 是整数 |
脚本示例:
#!/bin/bash# 字符串赋值
name="zz-zjx"
age=33
is_student=false
greeting="Hello, $name"# 数字赋值
count=10
max_retries=3# 命令替换赋值
current_date=$(date +"%Y-%m-%d")
ip_address=$(hostname -I)# 算术赋值
let "x = 5 + 3 * 2"
(( y = x ** 2 )) # 幂运算
(( z = 10 % 3 )) # 取模# 声明特定类型变量
declare -i counter=0 # 整数,自动进行算术运算
counter="10 + 5" # 会计算为15,而不是字符串
echo "counter: $counter" # 输出15declare -r PI=3.14159 # 只读变量,不可修改
# PI=3.14 # 这行会报错declare -l lower_case="HELLO" # 自动转为小写
echo "小写: $lower_case" # 输出hellodeclare -u upper_case="world" # 自动转为大写
echo "大写: $upper_case" # 输出WORLD# 数组声明
declare -a fruits=("苹果" "香蕉" "橙子") # 索引数组
declare -A person=([name]="张三" [age]=30 [city]="北京") # 关联数组# 导出变量(成为环境变量)
declare -x API_KEY="secret_key_123"echo "$greeting"
echo "当前日期: $current_date"
echo "IP地址: $ip_address"
echo "数字相乘": $(($count*$max_retries))
2. 变量引用与参数扩展(高级技巧)
命令格式:
${变量名[修饰符]}
参数扩展修饰符:
| 基本引用 |
|
| 变量为空时使用默认值 |
|
| 变量未设置或为空时赋值并使用 |
|
| 变量为空时使用默认值(不赋值) |
|
| 变量设置且非空时使用value |
|
| 变量为空时显示错误并退出 |
|
| 从开头删除最短匹配 |
|
| 从开头删除最长匹配 |
|
| 从结尾删除最短匹配 |
|
| 从结尾删除最长匹配 |
|
| 从offset开始的子字符串 |
|
| 指定长度的子字符串 |
|
| 替换第一个匹配 |
|
| 替换所有匹配 |
|
| 变量长度 |
|
| 匹配前缀的所有变量名 |
|
| 数组所有索引 |
|
详细示例:
#!/bin/bash# 基本变量
filename="document.txt"
path="/home/user/documents/report.pdf"
text="Hello World, welcome to Shell scripting!"# 默认值
user="${USERNAME:-匿名用户}"
echo "用户: $user"# 必需变量检查
#: ${CONFIG_FILE?"错误:必须设置CONFIG_FILE环境变量"}# 字符串截取
echo "文件名: ${filename##*/}" # 输出: document.txt echo "扩展名: ${filename%.*}" # 输出: document echo "目录: ${path%/*}" # 输出: /home/user/documents echo "目录: ${path%%/*}" # 输出: echo "基本名: ${path##*/}" # 输出: report.pdf echo "基本名: ${path#*/}" # 输出: home/user/documents/report.pdf# 子字符串
echo "从第6个字符开始: ${text:6}" # 输出: World, welcome to Shell scripting!
echo "5个字符: ${text:6:5}" # 输出: World# 字符串替换
echo "替换第一个空格: ${text/ /_}" # 输出: Hello_World, welcome to Shell scripting!
echo "替换所有空格: ${text// /_}" # 输出: Hello_World,_welcome_to_Shell_scripting!# 大小写转换
echo "大写: ${text^^}" # 输出: HELLO WORLD, WELCOME TO SHELL SCRIPTING!
echo "小写: ${text,,}" # 输出: hello world, welcome to shell scripting!
echo "首字母大写: ${text^}" # 输出: Hello World, welcome to Shell scripting!
echo "首字母小写: ${text,}" # 输出: hello World, welcome to Shell scripting!# 数组索引
fruits=("苹果" "香蕉" "橙子")
echo "所有索引: ${!fruits[@]}" # 输出: 0 1 2
echo "数组长度: ${#fruits[@]}" # 输出: 3# 关联数组
declare -A person=([name]="zz-zjx" [age]=30)
echo "所有键: ${!person[@]}" # 输出: name age
echo "值的数量: ${#person[@]}" # 输出: 2
三、条件判断(深度详解)
1. test / [ ] 命令(全面参数)
命令格式:
test 表达式
# 或
[ 表达式 ]
文件测试操作符:
| 文件存在(已过时,用 |
| 文件存在且为块设备 |
| 文件存在且为字符设备 |
| 文件存在且为目录 |
| 文件存在 |
| 文件存在且为普通文件 |
| 文件存在且设置了组ID位 |
| 文件存在且为符号链接( |
| 文件存在且设置了"sticky bit" |
| 文件存在且为命名管道(FIFO) |
| 文件存在且可读 |
| 文件存在且大小不为零 |
| 文件描述符fd已打开并关联到终端 |
| 文件存在且设置了setuid位 |
| 文件存在且可写 |
| 文件存在且可执行 |
| 文件存在且属于当前用户 |
| 文件存在且属于当前用户组 |
| 文件存在且为符号链接 |
| 文件存在且为套接字 |
| 文件存在且自上次读取后已修改 |
字符串测试操作符:
| 字符串长度为零 |
| 字符串长度不为零 |
| 字符串相等 |
| 字符串相等(同上,部分shell扩展)(2个等号) |
| 字符串不相等 |
| 按字典顺序str1在str2前 |
| 按字典顺序str1在str2后 |
数值测试操作符:
| 等于 |
| 不等于 |
| 小于 |
| 小于等于 |
| 大于 |
| 大于等于 |
组合测试:
| 逻辑非 |
| 逻辑与(已过时,用 |
| 逻辑或(已过时,用 ||代替) |
| 将expr作为子表达式 |
详细脚本示例:
#!/bin/bash# 文件测试
file="/etc/passwd"
if [ -f "$file" ] && [ -r "$file" ]; thenecho "$file 是可读的普通文件"if [ -s "$file" ]; thenecho "$file 大小不为零"fiif [ -O "$file" ]; thenecho "$file 属于当前用户"fi
fi# 更复杂的文件测试
if [ -d "/var/log" ] && [ ! -w "/var/log" ]; thenecho "/var/log 是目录但不可写"
fi# 字符串测试
name="张三"
if [ -z "$name" ]; thenecho "名字为空"
elif [ "$name" = "张三" ]; thenecho "你好,张三!"# 字典顺序比较if [ "$name" \< "李四" ]; thenecho "张三在字典顺序上位于李四之前"fi
fi# 数值测试
age=25
if [ $age -ge 18 ] && [ $age -lt 65 ]; thenecho "你是工作年龄"
elif [ $age -ge 65 ]; thenecho "你是退休年龄"
elseecho "你是未成年人"
fi# 组合测试
if [ -f "config.txt" ] && { [ -r "config.txt" ] || [ -w "config.txt" ] ; }; thenecho "config.txt 是可读或可写的文件"
fi# 测试文件修改时间
if [ file1 -nt file2 ]; thenecho "file1 比 file2 新"
elif [ file1 -ot file2 ]; thenecho "file1 比 file2 旧"
fi
2. [[ ]] 增强型条件测试(Bash特有)
[[ 表达式 ]]
扩展特性:
| 模式匹配(支持通配符) |
|
| 正则表达式匹配 |
|
| 逻辑与 |
|
|| | 逻辑或 | [[ $a -gt 0 || $a -lt 10 ]] |
| 逻辑非 |
|
| 分组 |
|
Shell 中通过 [ ]
(即 test
命令)支持三种文件时间比较:
操作符 | 含义 | 英文全称 |
---|---|---|
-nt | newer than:比……更新(修改时间更晚) | newer than |
-ot | older than:比……更旧(修改时间更早) | older than |
-ef | equal file:两个文件指向同一个 inode(硬链接) | equal file |
详细脚本示例:
#!/bin/bash # 文件测试 file="/etc/passwd" if [ -f "$file" ] && [ -r "$file" ]; then echo "$file 是可读的普通文件" if [ -s "$file" ]; then echo "$file 大小不为零" fi if [ -O "$file" ]; then echo "$file 属于当前用户" fi fi # 更复杂的文件测试 if [ -d "/var/log" ] && [ ! -w "/var/log" ]; then echo "/var/log 是目录但不可写" fi# 字符串测试
name="张三"
if [ -z "$name" ]; thenecho "名字为空"
elif [ "$name" = "张三" ]; thenecho "你好,张三!"# 字典顺序比较 if [ "$name" \< "李四" ]; thenecho "张三在字典顺序上位于李四之前"fi
fi# 数值测试
age1=25if [ $age1 -ge 18 ] && [ $age1 -lt 65 ]; thenecho "你是工作年龄"
elif [ $age1 -ge 65 ]; thenecho "你是退休年龄"
elseecho "你是未成年人"
fi# 组合测试
if [[ -f "config.txt" && ( -r "config.txt" || -w "config.txt" ) ]]; thenecho "config.txt 是可读或可写的文件"
fi# 测试文件修改时间
if [ config.txt -nt config_new.txt ]; thenecho "file1 比 file2 新"
elif [ config.txt -ot config_new.txt ]; thenecho "file1 比 file2 旧"
fi
3. case 语句(高级用法)
命令格式:
case 变量 in模式1 | 模式2)# 匹配模式1或模式2时执行;;模式*)# 通配符匹配;;*)# 默认情况;;
esac
模式匹配规则:
|
:表示"或"关系*
:匹配任意字符(包括空)?
:匹配单个字符[...]
:匹配括号内的任意一个字符[a-z]
:匹配a到z之间的任意一个字符!(pattern)
:不匹配指定模式(需要开启extglob)@(pattern)
:匹配指定模式之一(需要开启extglob)*(pattern)
:匹配零个或多个指定模式(需要开启extglob)+(pattern)
:匹配一个或多个指定模式(需要开启extglob)?(pattern)
:匹配零个或一个指定模式(需要开启extglob)
#!/bin/bash # 基本case语句 read -p "请选择操作 (start/stop/restart/status): " action case $action in start | begin) echo "正在启动服务..." ;; stop | end) echo "正在停止服务..." ;; restart | reload) echo "正在重启服务..." ;; status | info) echo "正在检查服务状态..." ;; *) echo "错误:未知操作 '$action'" >&2 echo "可用操作: start, stop, restart, status" >&2 exit 1 ;; esac # 通配符匹配 read -p "请输入文件名: " filename case $filename in *.txt) echo "这是一个文本文件" ;; *.jpg | *.png | *.gif) echo "这是一个图片文件" ;; *.tar | *.tar.gz | *.tgz | *.zip) echo "这是一个压缩文件" ;; Makefile | makefile) echo "这是一个Makefile" ;;*)echo "未知文件类型";;
esac# 高级模式匹配(需要开启extglob)
shopt -s extglobread -p "请输入数字: " number
case $number in+([0-9])) # 匹配一个或多个数字echo "这是一个正整数: $number";;-+([0-9])) # 匹配负整数echo "这是一个负整数: $number";;+([0-9]).+([0-9])) # 匹配浮点数echo "这是一个浮点数: $number";;*)echo "这不是一个有效的数字";;
esac# 复杂模式匹配
read -p "请输入命令: " commandcase $command in"git commit*" | "git push*" | "git pull*")echo "这是一个git操作";;"docker run*" | "docker start*" | "docker stop*")echo "这是一个docker操作";;"[sS]udo *")echo "这是一个需要sudo权限的操作";;*)echo "普通命令";;
esac
对比:1>&2
vs 2>&1
对比项 | 1>&2 | 2>&1 |
---|---|---|
方向 | stdout → stderr | stderr → stdout |
目的 | 让“正常输出”变成“错误输出” | 让“错误输出”变成“正常输出” |
常见场景 | 脚本中输出错误提示 | 合并日志、管道处理 |
示例 | echo "error" 1>&2 | cmd > log 2>&1 |
口诀 | “1 进 2” | “2 进 1” |
其他常见重定向组合
写法 | 含义 | 示例 |
---|---|---|
> file | 1>file 的简写,stdout 写入文件 | ls > list.txt |
2> file | stderr 写入文件(覆盖) | cmd 2> error.log |
2>> file | stderr 写入文件(追加) | cmd 2>> error.log |
&> file | Bash 特有:等价于 >file 2>&1 ,合并 stdout 和 stderr | cmd &> log.txt |
>/dev/null | 丢弃 stdout | cmd > /dev/null |
2>/dev/null | 丢弃 stderr | cmd 2>/dev/null |
&>/dev/null | 丢弃所有输出(stdout + stderr) | cmd &>/dev/null |
写法 | 含义 | 使用场景 |
---|---|---|
1>&2 | stdout → stderr | 脚本中输出错误信息 |
2>&1 | stderr → stdout | 合并日志、管道处理 |
&>file | stdout + stderr → file | 简化合并重定向(Bash) |
>/dev/null | 丢弃 stdout | 静默执行 |
2>/dev/null | 丢弃 stderr | 忽略错误 |
case
的优势 vs if-elif
特性 | case | if-elif |
---|---|---|
可读性 | ✅ 高(清晰的分支) | ❌ 多个 elif 易混乱 |
模式匹配 | ✅ 支持 * 、? 、` | ` 等 |
性能 | ✅ 通常更快 | ❌ 多次调用 [ ] |
灵活性 | ❌ 仅字符串匹配 | ✅ 可做数值、文件判断等 |
💡 推荐:当你要对一个变量做多种字符串模式判断时,优先使用
case
。
四、数组(深度详解)
# 索引数组
declare -a array_name=(元素1 元素2 ...)
array_name[索引]=值# 关联数组(Bash 4.0+)
declare -A array_name=([键1]=值1 [键2]=值2 ...)
array_name[键]=值# 数组操作
${array[@]} # 所有元素
${!array[@]} # 所有索引(关联数组为键)
${#array[@]} # 数组长度
${array[索引]} # 特定元素
${array[@]:offset} # 从offset开始的子数组
${array[@]:offset:length} # 指定长度的子数组
详细脚本示例:
#!/bin/bash# 索引数组定义
fruits=("苹果" "香蕉" "橙子" "葡萄")
declare -a vegetables=("胡萝卜" "西兰花" "土豆")
#大型脚本一般用下面这种 数组定义,写法不同而已# 关联数组定义(Bash 4.0+)
declare -A capitals=([China]="北京" [USA]="华盛顿" [Japan]="东京")
declare -A user_info=([name]="张三" [age]=30 [email]="zhangsan@example.com")
# 或者分开写 declare -A capitals capitals["China"]="Beijing"
# 显示数组信息
echo "水果数组:"
echo " 全部元素: ${fruits[@]}" # 输出 全部元素: 苹果 香蕉 橙子 葡萄
echo " 元素数量/数组长度: ${#fruits[@]}"
echo " 索引: ${!fruits[@]}" # ${!arr[@]}:获取所有索引(编号) 普通数组:0 1 2 3 关联数组 :China USA Japan
echo " 第二个元素: ${fruits[1]}" # 索引从0开始
echo " 最后一个元素: ${fruits[-1]}"echo -e "\n首都关联数组:"
echo " 全部键: ${!capitals[@]}" # 输出(key) China USA Japan
echo " 键的数量: ${#capitals[@]}" #输出3
echo " 中国的首都: ${capitals[China]}" # 输出 北京
echo " 所有值: ${capitals[@]}" #输出 (value) 北京 华盛顿 东京# 数组操作
echo -e "\n数组操作:"
# 修改元素
fruits[2]="橘子"
echo "修改后的水果: ${fruits[@]}" # 数组是 0开始 0 1 2 所以对应实际第3个# 添加元素
fruits+=("西瓜") #注意千万不要写成 declare -a fruits+=("西瓜") 这表示重新赋值而不是追加
vegetables+=("番茄" "黄瓜")
echo "添加后的水果: ${fruits[@]}"
echo "添加后的蔬菜: ${vegetables[@]}"# 删除元素
unset fruits[1] # 删除香蕉
echo "删除后的水果: ${fruits[@]}"
echo "删除后的索引: ${!fruits[@]}"# 子数组
echo "子数组 (索引1-2): ${fruits[@]:1:2}" # 如果没有删除和改写就是香蕉橙子 ,加上以上就是 橘子 葡萄# 数组遍历
echo -e "\n遍历水果数组:"
for fruit in "${fruits[@]}"; doecho " - $fruit"
doneecho -e "\n带索引遍历水果数组:"
for i in "${!fruits[@]}"; doecho " [$i] ${fruits[$i]}"
doneecho -e "\n遍历首都关联数组:"
for country in "${!capitals[@]}"; doecho " $country: ${capitals[$country]}"
done
: ' 遍历水果数组:- 苹果- 橘子- 葡萄- 西瓜带索引遍历水果数组:[0] 苹果[2] 橘子[3] 葡萄[4] 西瓜遍历首都关联数组:Japan: 东京China: 北京USA: 华盛顿
'# 数组排序
echo -e "\n排序数组:"
sorted_fruits=($(printf '%s\n' "${fruits[@]}" | sort))
echo " 按字母排序: ${sorted_fruits[@]}" #输出 按字母排序: 橘子 苹果 葡萄 西瓜# 数组去重
echo -e "\n数组去重:"
duplicates=("a" "b" "a" "c" "b")
declare -A temp
for item in "${duplicates[@]}"; dotemp["$item"]=1
done
unique=("${!temp[@]}") #普通数组定义
echo " 原始: ${duplicates[@]}"
echo " 去重: ${unique[@]}"
: ' temp["$item"]=1
将 item 的值作为 键(key) 存入 temp 数组,值设为 1(任意值都行,这里只是占位)。
因为关联数组的键是唯一的,所以重复的值不会被重复添加。
🔍 举个例子:第一次 item="a" → temp["a"]=1
第二次 item="b" → temp["b"]=1
第三次 item="a" → temp["a"]=1(已存在,覆盖,但不影响“唯一性”)
第四次 item="c" → temp["c"]=1
第五次 item="b" → temp["b"]=1(已存在)
最终,temp 数组的键就是:a, b, c —— 自动去重!'# 数组合并
echo -e "\n数组合并:"
combined=("${fruits[@]}" "${vegetables[@]}")
echo " 合并结果: ${combined[@]}"
# 输出 苹果 橘子 葡萄 西瓜 胡萝卜 西兰花 土豆 番茄 黄瓜# 数组转字符串
echo -e "\n数组转字符串:"
joined=$(IFS=,; echo "${fruits[*]}")
echo " 逗号分隔: $joined"
: 'IFS=,
IFS:Internal Field Separator(内部字段分隔符),Bash 用来决定如何“连接”或“分割”字符串。
默认 IFS 包含空格、制表符、换行符。
这里临时设置 IFS=,,表示“用逗号作为分隔符”。
⚠️ IFS=, 只在当前命令中生效(因为写在 ; 前面),不会影响后续代码。✅ ;
分号,表示命令分隔。
✅ echo "${fruits[*]}"
"${fruits[*]}":把数组所有元素合并成 一个字符串。
Bash 会自动使用当前 IFS 的值作为分隔符来连接元素。
📌 关键区别:"${fruits[@]}":保持元素分离(用于遍历)
"${fruits[*]}":合并成一个字符串(用于连接)
✅ joined=$( ... )
使用 $() 捕获命令输出,把结果赋值给变量 joined。
# 字符串转数组
echo -e "\n字符串转数组:"
IFS=, read -r -a split_array <<< "red,green,blue"
echo " 分割结果: ${split_array[@]}"
输出 逗号分隔: 苹果,橘子,葡萄,西瓜
'# 多维数组模拟
echo -e "\n模拟多维数组:"
declare -A matrix
matrix["0,0"]=1
matrix["0,1"]=2
matrix["1,0"]=3
matrix["1,1"]=4echo " 矩阵元素:"
echo " [0,0]: ${matrix["0,0"]}"
echo " [0,1]: ${matrix["0,1"]}"
echo " [1,0]: ${matrix["1,0"]}"
echo " [1,1]: ${matrix["1,1"]}"
: ' 输出 模拟多维数组:矩阵元素:[0,0]: 1[0,1]: 2[1,0]: 3[1,1]: 4
或者
# 声明关联数组
declare -A config# 赋值
config["prod,db"]="192.168.1.100"
config["dev,web"]="localhost"# 输出
echo "配置信息:"
echo " 生产数据库: ${config["prod,db"]}"
echo " 开发Web服务: ${config["dev,web"]}"echo "全部配置:"
for key in "${!config[@]}"; doecho " $key = ${config[$key]}"
done输出:配置信息:生产数据库: 192.168.1.100开发Web服务: localhost
全部配置:prod,db = 192.168.1.100dev,web = localhost
'
# 数组作为函数参数
process_array() {local -n arr_ref=$1 # 使用nameref(Bash 4.3+)echo " 处理数组: ${arr_ref[@]}"# 修改原始数组arr_ref[0]="修改后的值"
}echo -e "\n数组作为函数参数:"
echo " 原始数组: ${fruits[@]}"
process_array fruits
echo " 修改后: ${fruits[@]}"
: '原始数组: 苹果 橘子 葡萄 西瓜处理数组: 苹果 橘子 葡萄 西瓜修改后: 修改后的值 橘子 葡萄 西瓜
'
# 旧版Bash的数组传递方法
process_array_old() {# 通过eval处理eval "local temp=(\"\${$1[@]}\")"echo " 处理数组: ${temp[@]}"# 无法直接修改原始数组
}# 数组序列化与反序列化
serialize_array() {local -n arr=$1local IFS="|"echo "${arr[*]}"
}deserialize_array() {local serialized=$1IFS="|" read -r -a "$2" <<< "$serialized"
}echo -e "\n数组序列化:"
serialized=$(serialize_array fruits)
echo " 序列化: $serialized"
declare -a deserialized
deserialize_array "$serialized" deserialized
echo " 反序列化: ${deserialized[@]}"
: '
数组序列化:序列化: 修改后的值|橘子|葡萄|西瓜反序列化: 修改后的值 橘子 葡萄 西瓜1. local serialized=$1
$1 是传进来的序列化字符串,比如 "苹果|香蕉|橙子"
2. IFS="|" read -r -a "$2" <<< "$serialized"
这是一行非常关键的命令,拆解:✅ IFS="|"
临时设置分隔符为 |,用于分割字符串。
✅ read
Bash 内置命令,用于读取输入。
✅ -r
禁用反斜杠转义(安全选项,建议总是加)。
✅ -a "$2"
-a:表示读入数组
"$2":第二个参数,是要存入的数组名(比如 deserialized)
注意:是 "$2"(带引号),因为它是变量名
✅ <<< "$serialized"
Here String:把 $serialized 字符串作为 read 的输入
'
五、循环控制(深度详解)
1. for 循环(全面用法)
命令格式:
# 列表形式
for 变量 in 列表; do# 循环体
done# C语言风格
for ((初始化; 条件; 步进)); do# 循环体
done# 读取命令输出
for 变量 in $(命令); do# 循环体
done# 读取管道输出
命令 | while IFS= read -r 变量; do# 循环体
done
详细脚本示例:
#!/bin/bash# 基本列表循环
echo "基本列表循环:"
for color in 红色 绿色 蓝色
doecho " - $color" done # 文件通配循环 echo -e "\n处理所有txt文件:" for file in *.txt do if [ -f "$file" ]; then echo " $file (大小: $(wc -c < "$file") 字节)" fi done # 范围循环 echo -e "\n数字范围循环:" for i in {1..5} do echo " $i" done # 带步长的范围 echo -e "\n带步长的范围:" for i in {1..10..2} do # 从1到10,步长2 echo " $i" done # C语言风格循环 echo -e "\nC语言风格循环:" for ((i=0, j=10; i<10; i++, j--)) do echo " i=$i, j=$j" done # 处理命令输出 echo -e "\n处理命令输出:"
for user in $(cut -d: -f1 /etc/passwd | head -n 5)
doecho " 用户: $user"
done# 读取文件行(正确处理空格和特殊字符)
echo -e "\n安全读取文件行:"
while IFS= read -r line
do echo " $line"
done < <(head -n 3 /etc/passwd) # 处理数组 fruits=("苹果" "香蕉" "橙子" "葡萄") echo -e "\n处理数组:" for ((i=0; i<${#fruits[@]}; i++))
doecho " 索引 $i: ${fruits[$i]}"
done# 处理关联数组
declare -A capitals=([China]="北京" [USA]="华盛顿" [Japan]="东京")
echo -e "\n处理关联数组:"
for country in "${!capitals[@]}"
doecho " $country 的首都是 ${capitals[$country]}"
done# 多变量循环
<<注释 echo -e "\n多变量循环:"
for i in {1..3}; j in {a..c}; doecho " $i - $j"
done 2>/dev/null || echo " 注意:Bash不支持多变量列表循环,上面的示例会出错"
注释# 正确的多变量处理方法
echo -e "\n正确的多变量处理:"
countries=("China" "USA" "Japan")
capitals=("北京" "华盛顿" "东京")
for ((i=0; i<${#countries[@]}; i++))
doecho " ${countries[$i]} - ${capitals[$i]}"
done
2. while / until 循环(高级用法)
命令格式:
# while循环
while [ 条件 ]; do# 条件为真时执行
done# until循环
until [ 条件 ]; do# 条件为假时执行
done# 读取文件的标准方式
while IFS= read -r line; do# 处理每一行
done < 文件# 从命令输出读取
命令 | while IFS= read -r line; do# 处理每一行
done# 处理多个文件描述符
exec 3< file1 4< file2
while IFS= read -r -u 3 line1 && IFS= read -r -u 4 line2; do# 同时处理两个文件
done
详细脚本示例:
#!/bin/bash# 简单while循环
echo "简单while循环:"
count=1
while [ $count -le 5 ]; doecho " $count"((count++))
done# 简单until循环
echo -e "\n简单until循环:"
count=1
until [ $count -gt 5 ]; doecho " $count"((count++))
done# 读取文件(安全方式,保留空格和特殊字符)
echo -e "\n安全读取文件(保留空格):"
while IFS= read -r line; doecho " $line"
done < <(echo -e "第一行\n 第二行 \n第三行")# 读取文件(带行号)
echo -e "\n带行号读取文件:"
line_num=1
while IFS= read -r line; doprintf " %3d: %s\n" $line_num "$line"((line_num++))
done < <(head -n 5 /etc/passwd)# 从命令输出读取
echo -e "\n从命令输出读取:"
ps aux | while IFS= read -r -a fields; doif [ "${fields[0]}" = "$(whoami)" ]; thenecho " $(printf "%-10s %6s %s" "${fields[0]}" "${fields[2]}" "${fields[10]}")"fi
done# 处理多个文件描述符
echo -e "\n同时处理两个文件:"
exec 3< <(echo -e "A\nB\nC") 4< <(echo -e "1\n2\n3")
while IFS= read -r -u 3 line1 && IFS= read -r -u 4 line2; doecho " $line1 - $line2"
done
exec 3<&- 4<&- # 关闭文件描述符# 无限循环与用户交互
echo -e "\n用户交互循环 (输入'exit'退出):"
while true; doread -rp "> " inputcase $input inexit|quit)break;;help)echo " 可用命令: help, echo [文本], exit";;echo*)# 提取echo后的文本text="${input#echo }"echo " $text";;*)echo " 未知命令: $input";;esac
done# 处理超时
echo -e "\n带超时的循环:"
start_time=$(date +%s)
timeout=5 # 5秒超时while true; docurrent_time=$(date +%s)elapsed=$((current_time - start_time))if [ $elapsed -ge $timeout ]; thenecho " 超时 ($timeout秒)"breakfiecho " 运行中... ($elapsed/$timeout秒)"sleep 1
done# 从here文档读取
echo -e "\n从here文档读取:"
while IFS= read -r line; doecho " $line"
done <<EOF
这是here文档的第一行
这是第二行
包含特殊字符: \$ & * |
EOF
3. 循环控制命令(高级技巧)
命令格式:
break [n] # 退出循环(n表示退出n层循环)
continue [n] # 跳过当前迭代,继续下一次循环
return [n] # 从函数返回(n为返回状态)
exit [n] # 退出脚本(n为退出状态)
#!/bin/bash# 多层循环中的break
echo "多层循环中的break:"
for i in {1..3}; doecho "外层循环 $i:"for j in {A..C}; dofor k in {x,y,z}; doif [ "$k" = "y" ]; thenecho " 跳出两层循环 (i=$i, j=$j, k=$k)"break 2 # 跳出两层循环fiecho " ($i, $j, $k)"donedone
done# 多层循环中的continue
echo -e "\n多层循环中的continue:"
for i in {1..3}; doecho "外层循环 $i:"for j in {A..C}; dofor k in {x,y,z,p}; doif [ "$k" = "y" ]; thenecho " 跳过内层当前迭代 (i=$i, j=$j, k=$k)"continue 2 # 跳过两层循环的当前迭代fiecho " ($i, $j, $k)"donedone
done
: '
continue 2 不是“跳过 y 继续 z 和 p”,而是“看到 y 就把整个 j=A 这一轮直接作废”,所以 z 和 p 还没来得及出场,舞台就被关灯了!
continue 3 的意思是:“看到 k=y,就立刻放弃当前 i 的所有工作,直接进入下一个 i”。所以每个 i 只能完成 j=A, k=x,然后就被 k=y 触发跳过,z, p, j=B 全部不会执行。多层循环中的continue:
外层循环 1:(1, A, x)跳过内层当前迭代 (i=1, j=A, k=y)(1, B, x)跳过内层当前迭代 (i=1, j=B, k=y)(1, C, x)跳过内层当前迭代 (i=1, j=C, k=y)
外层循环 2:(2, A, x)跳过内层当前迭代 (i=2, j=A, k=y)(2, B, x)跳过内层当前迭代 (i=2, j=B, k=y)(2, C, x)跳过内层当前迭代 (i=2, j=C, k=y)
外层循环 3:(3, A, x)跳过内层当前迭代 (i=3, j=A, k=y)(3, B, x)跳过内层当前迭代 (i=3, j=B, k=y)(3, C, x)跳过内层当前迭代 (i=3, j=C, k=y)带状态返回的循环:
处理 item1...成功: item1
处理 item2...成功: item2
处理 item3...成功: item3
处理 item4...成功: item4
所有项目处理成功
'
# 带状态返回的循环
echo -e "\n带状态返回的循环:"
process_items() {local success_count=0local error_count=0for item in "$@"; doecho "处理 $item..."if (( RANDOM % 2 == 0 )); thenecho " 成功: $item"((success_count++))elseecho " 失败: $item" >&2((error_count++))fidoneif [ $error_count -eq 0 ]; thenreturn 0 # 全部成功elif [ $error_count -lt $success_count ]; thenreturn 1 # 部分成功elsereturn 2 # 大部分失败fi
}# 调用并检查状态process_items item1 item2 item3 item4
result=$?if [ $result -eq 0 ]; thenecho "所有项目处理成功"
elif [ $result -eq 1 ]; thenecho "部分项目处理成功"
elseecho "大部分项目处理失败"
fi# 退出脚本的不同状态
echo -e "\n脚本退出状态:"
check_prerequisites() {# 检查必要条件if ! command -v curl &> /dev/null; thenecho "错误:缺少curl命令" >&2exit 127 # 命令未找到的标准退出码fiif [ ! -w /tmp ]; thenecho "错误:/tmp目录不可写" >&2exit 2 # 权限错误fi
}check_prerequisites
echo "所有先决条件满足,继续执行..."