Shell 数组详解

Shell数组简介

数组是Shell脚本中非常重要的数据结构,它允许我们在单个变量中存储多个值。通过使用数组,我们可以:

存储多个值

在单个变量中存储多个相关的值,如文件名列表、用户名列表等。

批量处理

通过循环遍历数组,批量处理数据,提高脚本效率。

快速访问

通过索引快速访问特定位置的元素,实现高效数据检索。

动态操作

支持动态添加、删除和修改元素,适应不同的数据处理需求。

数组类型

Shell支持两种类型的数组:索引数组(使用数字作为索引)和关联数组(使用字符串作为键名)。Bash 4.0及以上版本支持关联数组。

数组定义和初始化

学习如何定义和初始化Shell数组。

索引数组

使用数字作为索引的数组,索引从0开始。

indexed_array.sh
#!/bin/bash

# 方法1: 直接赋值
fruits=("apple" "banana" "orange")

# 方法2: 逐个元素赋值
colors[0]="red"
colors[1]="green"
colors[2]="blue"

# 方法3: 使用declare命令
declare -a numbers=(1 2 3 4 5)

# 方法4: 从命令输出创建数组
files=($(ls *.txt))

# 方法5: 使用序列
letters=({a..e})

echo "水果数组: ${fruits[@]}"
echo "颜色数组: ${colors[@]}"
echo "数字数组: ${numbers[@]}"
echo "字母数组: ${letters[@]}"

关联数组

使用字符串作为键名的数组,需要Bash 4.0+。

associative_array.sh
#!/bin/bash

# 声明关联数组
declare -A user

# 赋值方式1: 逐个赋值
user[name]="Alice"
user[age]="25"
user[city]="Beijing"

# 赋值方式2: 一次性赋值
declare -A country_codes=([CN]="China" [US]="USA" [JP]="Japan")

# 赋值方式3: 使用键值对列表
declare -A colors=(
["red"]="#FF0000"
["green"]="#00FF00"
["blue"]="#0000FF"
)

echo "用户名: ${user[name]}"
echo "国家代码: ${country_codes[CN]}"
echo "红色代码: ${colors[red]}"
注意:

关联数组需要Bash 4.0或更高版本。可以使用bash --version检查Bash版本。

数组访问和操作

学习如何访问数组元素和获取数组信息。

访问数组元素

array_access.sh
#!/bin/bash

# 定义数组
fruits=("apple" "banana" "orange" "grape" "mango")

# 访问单个元素
echo "第一个水果: ${fruits[0]}"      # apple
echo "第二个水果: ${fruits[1]}"      # banana
echo "最后一个水果: ${fruits[-1]}"   # mango (Bash 4.3+)

# 访问所有元素
echo "所有水果: ${fruits[@]}"        # apple banana orange grape mango
echo "所有水果: ${fruits[*]}"        # apple banana orange grape mango

# 数组长度
echo "水果数量: ${#fruits[@]}"       # 5

# 获取所有索引
echo "数组索引: ${!fruits[@]}"       # 0 1 2 3 4

# 数组切片
echo "前三个水果: ${fruits[@]:0:3}"  # apple banana orange
echo "从第二个开始: ${fruits[@]:2}"   # orange grape mango

# 关联数组示例
declare -A user=([name]="Alice" [age]=25 [city]="Beijing")
echo "用户名: ${user[name]}"
echo "用户年龄: ${user[age]}"
echo "所有键: ${!user[@]}"
echo "所有值: ${user[@]}"
访问技巧:
  • 使用${array[index]}访问特定元素
  • 使用${array[@]}${array[*]}访问所有元素
  • 使用${#array[@]}获取数组长度
  • 使用${!array[@]}获取所有索引/键名
  • 使用${array[@]:start:length}进行数组切片

数组遍历

学习如何遍历数组的所有元素。

遍历数组的方法

array_iteration.sh
#!/bin/bash

# 定义数组
fruits=("apple" "banana" "orange" "grape")

echo "=== 方法1: for循环遍历值 ==="
for fruit in "${fruits[@]}"; do
echo "水果: $fruit"
done

echo -e "\n=== 方法2: for循环遍历索引 ==="
for i in "${!fruits[@]}"; do
echo "索引 $i: ${fruits[$i]}"
done

echo -e "\n=== 方法3: C风格for循环 ==="
for ((i=0; i<${#fruits[@]}; i++)); do
echo "元素 $i: ${fruits[$i]}"
done

echo -e "\n=== 方法4: while循环 ==="
i=0
while [ $i -lt ${#fruits[@]} ]; do
echo "元素 $i: ${fruits[$i]}"
((i++))
done

echo -e "\n=== 关联数组遍历 ==="
declare -A user=([name]="Alice" [age]=25 [city]="Beijing")
for key in "${!user[@]}"; do
echo "$key: ${user[$key]}"
done

echo -e "\n=== 使用while和read遍历(多行数据)==="
data=("line1" "line2" "line3")
printf "%s\n" "${data[@]}" | while read line; do
echo "处理: $line"
done
遍历建议:
  • 使用"${array[@]}"而不是"${array[*]}"来保留元素中的空格
  • 对于关联数组,使用"${!array[@]}"获取所有键名
  • 在需要索引时使用"${!array[@]}"方式遍历

数组操作

学习如何对数组进行添加、删除、修改等操作。

数组基本操作

array_operations.sh
#!/bin/bash

# 初始化数组
fruits=("apple" "banana" "orange")

echo "原始数组: ${fruits[@]}"

# 添加元素到末尾
fruits+=("grape")
echo "添加grape后: ${fruits[@]}"

# 添加多个元素
fruits+=("mango" "pear")
echo "添加多个后: ${fruits[@]}"

# 在特定位置插入元素(需要重新创建数组)
fruits=("${fruits[@]:0:2}" "peach" "${fruits[@]:2}")
echo "在位置2插入peach后: ${fruits[@]}"

# 修改元素
fruits[1]="blueberry"
echo "修改第二个元素后: ${fruits[@]}"

# 删除元素
unset fruits[2]
echo "删除第三个元素后: ${fruits[@]}"
echo "当前索引: ${!fruits[@]}"

# 重新索引数组
fruits=("${fruits[@]}")
echo "重新索引后: ${fruits[@]}"
echo "重新索引后的索引: ${!fruits[@]}"

# 数组合并
arr1=("A" "B" "C")
arr2=("D" "E" "F")
combined=("${arr1[@]}" "${arr2[@]}")
echo "合并数组: ${combined[@]}"

# 数组拷贝
copy=("${fruits[@]}")
echo "数组拷贝: ${copy[@]}"

# 清空数组
unset fruits
echo "清空数组后长度: ${#fruits[@]}"

数组搜索和过滤

array_search.sh
#!/bin/bash

# 定义数组
numbers=(10 25 30 45 50 65 70 85 90)

echo "原始数组: ${numbers[@]}"

# 搜索元素
search_value=50
for i in "${!numbers[@]}"; do
if [ "${numbers[$i]}" -eq "$search_value" ]; then
    echo "找到 $search_value 在索引 $i"
    break
fi
done

# 过滤数组(大于50的值)
filtered=()
for num in "${numbers[@]}"; do
if [ "$num" -gt 50 ]; then
    filtered+=("$num")
fi
done
echo "大于50的值: ${filtered[@]}"

# 数组排序
sorted=($(printf "%s\n" "${numbers[@]}" | sort -n))
echo "排序后数组: ${sorted[@]}"

# 反转数组
reversed=()
for ((i=${#numbers[@]}-1; i>=0; i--)); do
reversed+=("${numbers[$i]}")
done
echo "反转数组: ${reversed[@]}"

# 数组去重
duplicates=(1 2 2 3 4 4 5 5 5)
unique=($(printf "%s\n" "${duplicates[@]}" | sort -u))
echo "去重后数组: ${unique[@]}"

# 检查元素是否存在
check_exists() {
local array=("$@")
local seeking=$2
local in=1
for element in "${array[@]}"; do
    if [[ "$element" == "$seeking" ]]; then
        in=0
        break
    fi
done
return $in
}

if check_exists "${numbers[@]}" 30; then
echo "30 存在于数组中"
else
echo "30 不存在于数组中"
fi

综合示例

通过实际脚本示例展示Shell数组的综合应用。

文件管理系统

使用数组管理文件列表和执行批量操作。

file_manager.sh
#!/bin/bash

# 文件管理系统 - 数组应用示例

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# 全局数组变量
declare -a files
declare -a backup_files
declare -A file_info

# 初始化文件列表
init_files() {
echo -e "${BLUE}扫描当前目录文件...${NC}"
files=($(ls -p | grep -v /))  # 只获取文件,排除目录
echo -e "找到 ${#files[@]} 个文件"
}

# 显示文件列表
show_files() {
echo -e "\n${BLUE}=== 文件列表 ===${NC}"
if [ ${#files[@]} -eq 0 ]; then
    echo -e "${YELLOW}没有找到文件${NC}"
    return
fi

for i in "${!files[@]}"; do
    local file="${files[$i]}"
    local size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
    local date=$(date -r "$file" "+%Y-%m-%d %H:%M:%S")

    # 存储文件信息到关联数组
    file_info["${file}_size"]=$size
    file_info["${file}_date"]=$date

    printf "%-3s %-20s %-10s %s\n" "$i" "$file" "$size" "$date"
done
}

# 备份文件
backup_files() {
local backup_dir="backup_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$backup_dir"

backup_files=()
for file in "${files[@]}"; do
    if cp "$file" "$backup_dir/"; then
        backup_files+=("$file")
        echo -e "${GREEN}备份成功: $file${NC}"
    else
        echo -e "${RED}备份失败: $file${NC}"
    fi
done

echo -e "\n${GREEN}备份完成! 共备份 ${#backup_files[@]} 个文件到 $backup_dir${NC}"
}

# 按扩展名筛选文件
filter_by_extension() {
local extension="$1"
local filtered=()

for file in "${files[@]}"; do
    if [[ "$file" == *".$extension" ]]; then
        filtered+=("$file")
    fi
done

if [ ${#filtered[@]} -gt 0 ]; then
    echo -e "\n${BLUE}=== .$extension 文件 ===${NC}"
    printf "%s\n" "${filtered[@]}"
else
    echo -e "${YELLOW}没有找到 .$extension 文件${NC}"
fi
}

# 文件统计
file_statistics() {
echo -e "\n${BLUE}=== 文件统计 ===${NC}"

# 按扩展名统计
declare -A ext_count
declare -A ext_size

for file in "${files[@]}"; do
    local size="${file_info[${file}_size]}"
    local extension="${file##*.}"

    # 如果没有扩展名,使用"无"
    if [[ "$file" == "$extension" ]]; then
        extension="无"
    fi

    ((ext_count[$extension]++))
    ((ext_size[$extension]+=size))
done

echo -e "${GREEN}按扩展名统计:${NC}"
for ext in "${!ext_count[@]}"; do
    local count=${ext_count[$ext]}
    local total_size=${ext_size[$ext]}
    local percentage=$((count * 100 / ${#files[@]}))
    echo -e "  .$ext: $count 个文件 ($total_size 字节) $percentage%"
done

# 文件大小统计
local total_size=0
for file in "${files[@]}"; do
    ((total_size+=${file_info[${file}_size]}))
done
echo -e "\n${GREEN}总大小: $total_size 字节${NC}"
}

# 主菜单
main_menu() {
while true; do
    echo -e "\n${BLUE}=== 文件管理系统 ===${NC}"
    echo "1. 显示文件列表"
    echo "2. 备份所有文件"
    echo "3. 筛选文本文件"
    echo "4. 筛选图片文件"
    echo "5. 文件统计"
    echo "6. 重新扫描文件"
    echo "0. 退出"

    read -p "请选择操作 [0-6]: " choice

    case $choice in
        1) show_files ;;
        2) backup_files ;;
        3) filter_by_extension "txt" ;;
        4) filter_by_extension "jpg" ;;
        5) file_statistics ;;
        6) init_files ;;
        0)
            echo -e "${GREEN}再见!${NC}"
            break
            ;;
        *)
            echo -e "${RED}无效选择!${NC}"
            ;;
    esac
done
}

# 脚本入口
main() {
echo -e "${GREEN}文件管理系统启动...${NC}"
init_files
main_menu
}

# 执行主函数
main "$@"
使用示例:
./file_manager.sh
然后按照菜单提示选择操作。

数组使用最佳实践

推荐做法
  • 使用有意义的数组名
  • 在循环中使用引号保护元素:"${array[@]}"
  • 使用${!array[@]}获取索引进行遍历
  • 为关联数组使用描述性的键名
  • 在修改数组前先备份重要数据
  • 使用函数封装数组操作
避免的做法
  • 不要假设数组索引是连续的
  • 避免在数组元素中包含特殊字符
  • 不要忘记检查数组是否为空
  • 避免过度复杂的嵌套数组结构
  • 不要混用索引数组和关联数组
  • 避免在循环中修改正在遍历的数组
调试技巧

使用 declare -p array_name 查看数组的完整内容。在脚本中添加 set -x 开启调试模式,观察数组操作过程。