第一部分: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/bash
set -euo pipefail
# 如果前面的命令失败,脚本会在这里停止
cp source.txt destination.txt
echo "文件复制成功!"
二、变量操作命令
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" # 输出15
declare -r PI=3.14159 # 只读变量,不可修改
# PI=3.14 # 这行会报错
declare -l lower_case="HELLO" # 自动转为小写
echo "小写: $lower_case" # 输出hello
declare -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. 变量引用与参数扩展(高级技巧)
命令格式:
${变量名[修饰符]}
参数扩展修饰符:
${var} |
基本引用 | echo ${name} |
${var=default} |
变量为空时使用默认值 | echo ${user=root} |
${var:=default} |
变量未设置或为空时赋值并使用 | echo ${path:=/usr/bin} |
${var:-default} |
变量为空时使用默认值(不赋值) | echo ${name:-匿名} |
${var:+value} |
变量设置且非空时使用value | echo ${name:+已设置} |
${var:?message} |
变量为空时显示错误并退出 | echo ${file:?必须指定文件} |
${var#pattern} |
从开头删除最短匹配 | echo ${path#*/} |
${var##pattern} |
从开头删除最长匹配 | echo ${path##*/} |
${var%pattern} |
从结尾删除最短匹配 | echo ${file%.txt} |
${var%%pattern} |
从结尾删除最长匹配 | echo ${file%%.*} |
${var:offset} |
从offset开始的子字符串 | echo ${text:5} |
${var:offset:length} |
指定长度的子字符串 | echo ${text:5:10} |
${var/pattern/replacement} |
替换第一个匹配 | echo ${text/hello/hi} |
${var//pattern/replacement} |
替换所有匹配 | echo ${text// /_} |
${#var} |
变量长度 | echo ${#name} |
${!prefix*} |
匹配前缀的所有变量名 | echo ${!USER*} |
${!name[@]} |
数组所有索引 | echo ${!fruits[@]} |
详细示例:
#!/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 表达式
# 或
[ 表达式 ]
文件测试操作符:
-a file |
文件存在(已过时,用-e 代替) |
-b file |
文件存在且为块设备 |
-c file |
文件存在且为字符设备 |
-d file |
文件存在且为目录 |
-e file |
文件存在 |
-f file |
文件存在且为普通文件 |
-g file |
文件存在且设置了组ID位 |
-h file |
文件存在且为符号链接(-L 更标准) |
-k file |
文件存在且设置了"sticky bit" |
-p file |
文件存在且为命名管道(FIFO) |
-r file |
文件存在且可读 |
-s file |
文件存在且大小不为零 |
-t fd |
文件描述符fd已打开并关联到终端 |
-u file |
文件存在且设置了setuid位 |
-w file |
文件存在且可写 |
-x file |
文件存在且可执行 |
-O file |
文件存在且属于当前用户 |
-G file |
文件存在且属于当前用户组 |
-L file |
文件存在且为符号链接 |
-S file |
文件存在且为套接字 |
-N file |
文件存在且自上次读取后已修改 |
字符串测试操作符:
-z str |
字符串长度为零 |
-n str |
字符串长度不为零 |
str1 = str2 |
字符串相等 |
str1 == str2 |
字符串相等(同上,部分shell扩展)(2个等号) |
str1 != str2 |
字符串不相等 |
str1 < str2 |
按字典顺序str1在str2前 |
str1 > str2 |
按字典顺序str1在str2后 |
数值测试操作符:
arg1 -eq arg2 |
等于 |
arg1 -ne arg2 |
不等于 |
arg1 -lt arg2 |
小于 |
arg1 -le arg2 |
小于等于 |
arg1 -gt arg2 |
大于 |
arg1 -ge arg2 |
大于等于 |
组合测试:
! expr |
逻辑非 |
expr1 -a expr2 |
逻辑与(已过时,用&& 代替) |
expr1 -o expr2 |
逻辑或(已过时,用 ||代替) |
( expr ) |
将expr作为子表达式 |
详细脚本示例:
#!/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" ]; then
echo "名字为空"
elif [ "$name" = "张三" ]; then
echo "你好,张三!"
# 字典顺序比较
if [ "$name" \< "李四" ]; then
echo "张三在字典顺序上位于李四之前"
fi
fi
# 数值测试
age=25
if [ $age -ge 18 ] && [ $age -lt 65 ]; then
echo "你是工作年龄"
elif [ $age -ge 65 ]; then
echo "你是退休年龄"
else
echo "你是未成年人"
fi
# 组合测试
if [ -f "config.txt" ] && { [ -r "config.txt" ] || [ -w "config.txt" ] ; }; then
echo "config.txt 是可读或可写的文件"
fi
# 测试文件修改时间
if [ file1 -nt file2 ]; then
echo "file1 比 file2 新"
elif [ file1 -ot file2 ]; then
echo "file1 比 file2 旧"
fi
2. [[ ]] 增强型条件测试(Bash特有)
[[ 表达式 ]]
扩展特性:
== |
模式匹配(支持通配符) | [[ $name == J* ]] |
=~ |
正则表达式匹配 | [[ $email =~ ^[a-z]+@[a-z]+\.[a-z]+$ ]] |
&& |
逻辑与 | [[ $a -gt 0 && $a -lt 10 ]] |
|| | 逻辑或 | `[[ $a -gt 0 |
! |
逻辑非 | [[ ! -f "$file" ]] |
(...) |
分组 | [[ ( $a -gt 0 ) && ( $b -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" ]; then
echo "名字为空"
elif [ "$name" = "张三" ]; then
echo "你好,张三!"
# 字典顺序比较
if [ "$name" \< "李四" ]; then
echo "张三在字典顺序上位于李四之前"
fi
fi
# 数值测试
age1=25
if [ $age1 -ge 18 ] && [ $age1 -lt 65 ]; then
echo "你是工作年龄"
elif [ $age1 -ge 65 ]; then
echo "你是退休年龄"
else
echo "你是未成年人"
fi
# 组合测试
if [[ -f "config.txt" && ( -r "config.txt" || -w "config.txt" ) ]]; then
echo "config.txt 是可读或可写的文件"
fi
# 测试文件修改时间
if [ config.txt -nt config_new.txt ]; then
echo "file1 比 file2 新"
elif [ config.txt -ot config_new.txt ]; then
echo "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 extglob
read -p "请输入数字: " number
case number in +([0-9])) # 匹配一个或多个数字 echo "这是一个正整数: number"
;;
-+([0-9])) # 匹配负整数
echo "这是一个负整数: number" ;; +([0-9]).+([0-9])) # 匹配浮点数 echo "这是一个浮点数: number"
;;
*)
echo "这不是一个有效的数字"
;;
esac复杂模式匹配
read -p "请输入命令: " command
case $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[@]}"; do
echo " - $fruit"
done
echo -e "\n带索引遍历水果数组:"
for i in "${!fruits[@]}"; do
echo " [$i] ${fruits[$i]}"
done
echo -e "\n遍历首都关联数组:"
for country in "${!capitals[@]}"; do
echo " $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[@]}"; do
temp["$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"]=4
echo " 矩阵元素:"
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[@]}"; do
echo " $key = ${config[$key]}"
done
输出:
配置信息:
生产数据库: 192.168.1.100
开发Web服务: localhost
全部配置:
prod,db = 192.168.1.100
dev,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=$1
local IFS="|"
echo "${arr[*]}"
}
deserialize_array() {
local serialized=$1
IFS="|" 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 红色 绿色 蓝色
do
echo " - $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)
do
echo " 用户: $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++))
do
echo " 索引 $i: ${fruits[$i]}"
done
# 处理关联数组
declare -A capitals=([China]="北京" [USA]="华盛顿" [Japan]="东京")
echo -e "\n处理关联数组:"
for country in "${!capitals[@]}"
do
echo " $country 的首都是 ${capitals[$country]}"
done
# 多变量循环
<<注释 echo -e "\n多变量循环:"
for i in {1..3}; j in {a..c}; do
echo " $i - $j"
done 2>/dev/null || echo " 注意:Bash不支持多变量列表循环,上面的示例会出错"
注释
# 正确的多变量处理方法
echo -e "\n正确的多变量处理:"
countries=("China" "USA" "Japan")
capitals=("北京" "华盛顿" "东京")
for ((i=0; i<${#countries[@]}; i++))
do
echo " ${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 ]; do
echo " $count"
((count++))
done
# 简单until循环
echo -e "\n简单until循环:"
count=1
until [ $count -gt 5 ]; do
echo " $count"
((count++))
done
# 读取文件(安全方式,保留空格和特殊字符)
echo -e "\n安全读取文件(保留空格):"
while IFS= read -r line; do
echo " $line"
done < <(echo -e "第一行\n 第二行 \n第三行")
# 读取文件(带行号)
echo -e "\n带行号读取文件:"
line_num=1
while IFS= read -r line; do
printf " %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; do
if [ "${fields[0]}" = "$(whoami)" ]; then
echo " $(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; do
echo " $line1 - $line2"
done
exec 3<&- 4<&- # 关闭文件描述符
# 无限循环与用户交互
echo -e "\n用户交互循环 (输入'exit'退出):"
while true; do
read -rp "> " input
case $input in
exit|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; do
current_time=$(date +%s)
elapsed=$((current_time - start_time))
if [ $elapsed -ge $timeout ]; then
echo " 超时 ($timeout秒)"
break
fi
echo " 运行中... ($elapsed/$timeout秒)"
sleep 1
done
# 从here文档读取
echo -e "\n从here文档读取:"
while IFS= read -r line; do
echo " $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}; do
echo "外层循环 $i:"
for j in {A..C}; do
for k in {x,y,z}; do
if [ "$k" = "y" ]; then
echo " 跳出两层循环 (i=$i, j=$j, k=$k)"
break 2 # 跳出两层循环
fi
echo " ($i, $j, $k)"
done
done
done
# 多层循环中的continue
echo -e "\n多层循环中的continue:"
for i in {1..3}; do
echo "外层循环 $i:"
for j in {A..C}; do
for k in {x,y,z,p}; do
if [ "$k" = "y" ]; then
echo " 跳过内层当前迭代 (i=$i, j=$j, k=$k)"
continue 2 # 跳过两层循环的当前迭代
fi
echo " ($i, $j, $k)"
done
done
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=0
local error_count=0
for item in "$@"; do
echo "处理 $item..."
if (( RANDOM % 2 == 0 )); then
echo " 成功: $item"
((success_count++))
else
echo " 失败: $item" >&2
((error_count++))
fi
done
if [ $error_count -eq 0 ]; then
return 0 # 全部成功
elif [ $error_count -lt $success_count ]; then
return 1 # 部分成功
else
return 2 # 大部分失败
fi
}
# 调用并检查状态
process_items item1 item2 item3 item4
result=$?
if [ $result -eq 0 ]; then
echo "所有项目处理成功"
elif [ $result -eq 1 ]; then
echo "部分项目处理成功"
else
echo "大部分项目处理失败"
fi
# 退出脚本的不同状态
echo -e "\n脚本退出状态:"
check_prerequisites() {
# 检查必要条件
if ! command -v curl &> /dev/null; then
echo "错误:缺少curl命令" >&2
exit 127 # 命令未找到的标准退出码
fi
if [ ! -w /tmp ]; then
echo "错误:/tmp目录不可写" >&2
exit 2 # 权限错误
fi
}
check_prerequisites
echo "所有先决条件满足,继续执行..."