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/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 -ivar 是整数

脚本示例:

复制代码
#!/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 "所有先决条件满足,继续执行..."
相关推荐
草莓熊Lotso12 小时前
Linux 文件描述符与重定向实战:从原理到 minishell 实现
android·linux·运维·服务器·数据库·c++·人工智能
历程里程碑12 小时前
Linux22 文件系统
linux·运维·c语言·开发语言·数据结构·c++·算法
恋猫de小郭12 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅19 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606119 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了19 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅19 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
wdfk_prog20 小时前
[Linux]学习笔记系列 -- [drivers][input]input
linux·笔记·学习
崔庆才丨静觅20 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
七夜zippoe20 小时前
CANN Runtime任务描述序列化与持久化源码深度解码
大数据·运维·服务器·cann