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 "所有先决条件满足,继续执行..."
相关推荐
濮水大叔5 小时前
能够动态推断与生成DTO是Node生态的一个重要里程碑
前端·typescript·node.js
咕噜签名分发冰淇淋5 小时前
搭建APP应用程序如何选择服务器
运维·服务器
吗喽对你问好5 小时前
场景题:如果一个大型项目,某一个时间所有的CPU的已经被占用了,导致服务不可用,我们开发人员应该如何使服务器尽快恢复正常
java·linux·运维·服务器
飞阿飞飞5 小时前
手把手教你用React做一个Excel导入功能,看完就能用!
前端·react.js
艾小码5 小时前
JSON数据处理太头疼?这4个技巧让你秒变高手!
前端·javascript·json
庚云5 小时前
前端项目中 .env 文件的原理和实现
前端·面试
10share5 小时前
【vite-vue】demo项目创建
前端
Mintopia5 小时前
用 Next.js 打造全栈文件上传(S3 / Cloudinary)——从字节到云端的奇妙旅程
前端·javascript·next.js