数组

数组概述

  • 数组 array
  • Shell 数组是一种用于存储一系列值的数据结构。
  • 在 Shell 中,数组可以包含整数、字符串和其他类型的值。
  • 数组可以使用数字索引或字符串索引来访问其元素。
  • 在 Shell 中,数组的索引从 0 开始,因此第一个元素的索引为 0,第二个元素的索引为 1,以此类推。

以下是一些使用数组的示例:

  1. 定义数组

可以使用以下语法定义一个数组:

bashCopy code
my_array=(value1 value2 value3)

这将创建一个包含三个元素的数组。

  1. 访问数组元素

可以使用以下语法访问数组元素:

bashCopy code
${my_array[index]}

其中 index 是要访问的元素的索引。例如,要访问数组 my_array 中的第二个元素,可以使用以下语法:

bashCopy code
${my_array[1]}
  1. 遍历数组

可以使用以下语法遍历数组中的所有元素:

bashCopy codefor element in "${my_array[@]}"
do
  # Do something with $element
done

此代码将遍历数组 my_array 中的所有元素,并将每个元素存储在变量 element 中。

  1. 获取数组长度

可以使用以下语法获取数组的长度:

bashCopy code
${#my_array[@]}

此代码将返回数组 my_array 中的元素数。

这些是一些 Shell 数组的基础知识,有助于在 Shell 脚本中使用数组来存储和处理数据。

数组名和索引

  • 索引的编号从0开始,属于数值索引;
  • 但也可以使用自定义的格式,这种称为关联索引。
  • PS:
    • bash 的数组支持稀疏格式(索引不连续)

在 Shell 数组中,索引指的是用于访问数组元素的值。在 Shell 中,有两种类型的索引:数值索引和关联索引。

  1. 数值索引

数值索引是使用整数作为索引的方式。在 Shell 中,数组的数值索引从 0 开始,因此第一个元素的索引为 0,第二个元素的索引为 1,以此类推。可以使用以下语法来访问数组中的元素:

bashCopy code
${array[index]}

其中 array 是数组的名称,index 是要访问的元素的索引。

  1. 关联索引

关联索引是使用字符串作为索引的方式。在关联数组中,每个元素都与一个唯一的字符串关联。可以使用以下语法来访问关联数组中的元素:

bashCopy code
${array[key]}

其中 array 是数组的名称,key 是要访问的元素的关联键值。

关联数组非常适合用于需要使用键-值对存储数据的场景,例如在脚本中存储配置设置或用户数据。

在 Bash shell 中,可以直接使用赋值语句来定义数组变量,这种数组被称为"普通数组"或"非声明数组"(unindexed array)。这种方式的数组不需要显式地使用 declare -a 命令进行声明,Bash 会根据赋值语句自动判断变量类型。

例如,在下面的示例中,我们定义了一个普通数组:

title=("ceo" "coo" "cto")

这将创建一个名为 title 的数组,并将 ceocoocto 三个元素添加到数组中,而无需使用 declare -a 命令显式地声明该数组。

那反之呢

在 Bash shell 中,除了使用赋值语句定义普通数组之外,还可以使用 declare 命令或 typeset 命令显式地声明数组变量,这种数组被称为"声明数组"或"索引数组"(indexed array)。

例如,在下面的示例中,我们使用 declare -a 命令声明了一个名为 title 的索引数组:

declare -a title=("ceo" "coo" "cto")

这将创建一个名为 title 的数组,并将 ceocoocto 三个元素添加到数组中。与普通数组不同,声明数组需要使用 declare -a 命令或 typeset -a 命令进行显式声明。

不是 -A 吗

您是对的,对于关联数组(也称为哈希表),在 Bash shell 中,可以使用 -A 选项来使用 declare 命令或 typeset 命令声明关联数组,例如:

declare -A my_array

这将创建一个名为 my_array 的关联数组。类似地,您也可以在赋值语句中直接定义一个关联数组,例如:

my_array=([key1]=value1 [key2]=value2 [key3]=value3)

这将创建一个名为 my_array 的关联数组,并将三个键值对 {key1: value1, key2: value2, key3: value3} 添加到数组中。与索引数组不同,关联数组可以使用任意字符串作为索引,而不仅仅是数字。

数组与变量的区别

  • 变量:存储单个元素的内存空间
  • 数组:存储多个元素的连续的内存空间,相当于多个变量的集合

数组和变量在 Shell 中都是用于存储值的数据结构,但它们有一些区别。

  1. 存储多个值

变量只能存储一个值,而数组可以存储多个值。这使得数组在处理需要存储多个值的数据时非常有用,例如存储一组文件名或一组数字。

  1. 索引

变量没有索引,而数组有索引。使用索引可以轻松地访问数组中的特定元素。在 Shell 中,数组的索引从 0 开始。

  1. 传递参数

在函数中传递参数时,可以将变量作为参数传递,但是如果需要传递多个值,则可以将数组作为参数传递。

  1. 变量名的命名

在 Shell 中,变量名通常是单个字母或单词的组合。而在数组中,通常需要使用一个具有描述性的名称来标识数组中存储的值。

总的来说,变量和数组在 Shell 中都是非常有用的数据结构。选择使用哪种数据结构取决于存储的数据类型和需要处理的数据数量。

定义数组

数组赋值

一次只赋值一个元素

  • ARRAY_NAME[INDEX]=VALUE

范例:

weekdays[0]="Sunday" # 创建一个名为weekdays的数组,并将该数组的第一个元素设置为字符串"Sunday"。在Bash中,数组索引从0开始。

weekdays[4]="Thursday" # 将weekdays数组的第五个元素(索引为4)设置为字符串"Thursday"。

## 这样,该数组的第一和第五个元素分别是"Sunday"和"Thursday",而其他元素尚未设置,因此其值为空。

一次只赋值多个元素

  • ARRAY_NAME=("VAL1" "VAL2" "VAL3" ...)

范例:

title=("ceo" "coo" "cto") # 创建一个名为title的数组,其中包含三个字符串元素:"ceo","coo"和"cto"。

num=({0..10}) # 创建一个名为num的数组,其中包含数字0到10之间的所有整数。

alpha=({a..g}) # 创建一个名为alpha的数组,其中包含小写字母a到g之间的所有字母。

file=( *.sh ) # 创建一个名为file的数组,其中包含当前目录中所有以.sh为后缀的文件的文件名。

只赋值特定元素

  • ARRAY_NAME=([0]="VAL1" [3]="VAL2" ...)

交互式数组

read -a ARRAY

这是一个 Bash Shell 命令,用于从标准输入中读取一行,并将其分割为单词,并将这些单词存储在名为 ARRAY 的数组中。如果输入行以换行符结尾,则将其从数组中删除。例如,以下命令将从标准输入读取一行,并将其分割为单词,并将这些单词存储在名为 ARRAY 的数组中:

read -a ARRAY

然后,您可以使用以下语法来访问数组中的元素:

${ARRAY[index]}

其中,index 是要访问的元素的索引。例如,${ARRAY[0]} 将返回数组中的第一个元素。

普通数组 与 关联数组

普通数组

  • 普通数组可以不事先声明,直接使用:declare -a ARRAY_NAME

普通数组是一系列按顺序排列的值的集合,这些值通过索引进行访问。

索引是整数值,从0开始连续编号。

在Bash中,可以使用declare -a命令声明一个普通数组,如下所示:

declare -a my_array=(1 2 3 4 5)

关联数组

  • 关联数组必须先声明,再使用:declare -A ARRAY_NAME

关联数组是一种使用键值对进行访问的数组。

在Bash中,可以使用declare -A命令声明一个关联数组,如下所示:

declare -A my_assoc_array=( [key1]="value1" [key2]="value2" )

范例:

  • 。。。

注意事项

注意:索引数组 与 关联数组 两者不可相互转化

普通数组和关联数组在Bash中是两种不同类型的数组,它们具有不同的结构和用途,因此不能相互转化。

因为普通数组和关联数组在结构和用途上不同,所以它们不能相互转化。试图将一个普通数组转换为关联数组或将关联数组转换为普通数组都会导致语法错误或逻辑错误。

## declare -A test_array
## declare -a test_array
-bash: declare: test_array: cannot convert associative to indexed array


## file=( *.sh )
## declare -A file
-bash: declare: file: cannot convert indexed to associative array

索引数组又称普通数组对吗

是的,索引数组通常也被称为普通数组。这是因为索引数组是最基本的数组类型之一,其中每个元素都通过一个整数索引来访问。这种类型的数组是大多数编程语言中的常见数据类型之一,包括Java、C++、Python等。在这种类型的数组中,每个元素都有一个唯一的整数索引,可以使用该索引来访问该元素,使得索引数组非常易于使用和操作。

引用数组

显示所有数组

## declare -a
declare -a BASH_ARGC=()
declare -a BASH_ARGV=()
declare -a BASH_COMPLETION_VERSINFO=([0]="2" [1]="7")
declare -a BASH_LINENO=()
declare -a BASH_SOURCE=()
declare -ar BASH_VERSINFO=([0]="4" [1]="4" [2]="19" [3]="1" [4]="release" [5]="x86_64-redhat-linux-gnu")
declare -a DIRSTACK=()
declare -a FUNCNAME
declare -a GROUPS=()
declare -a PIPESTATUS=([0]="0")

这些是 Bash shell 内置的特殊数组,其含义如下:

  • BASH_ARGC: 包含当前 shell 函数或脚本的参数个数。
  • BASH_ARGV: 包含当前 shell 函数或脚本的所有参数。${BASH_ARGV[0]} 是脚本名,${BASH_ARGV[1]}${BASH_ARGV[$((BASH_ARGC-1))]} 是脚本的参数。
  • BASH_COMPLETION_VERSINFO: 包含 Bash shell 自动补全功能的版本信息。${BASH_COMPLETION_VERSINFO[0]} 是主版本号,${BASH_COMPLETION_VERSINFO[1]} 是次版本号。
  • BASH_LINENO: 包含当前正在执行的 shell 函数或脚本的行号。
  • BASH_SOURCE: 包含当前正在执行的 shell 函数或脚本的文件名。
  • BASH_VERSINFO: 包含 Bash shell 的版本信息。${BASH_VERSINFO[0]} 是主版本号,${BASH_VERSINFO[1]} 是次版本号,${BASH_VERSINFO[2]} 是修订版本号,${BASH_VERSINFO[3]} 是版本发布号,${BASH_VERSINFO[4]} 是版本状态,${BASH_VERSINFO[5]} 是编译选项。
  • DIRSTACK: 包含当前 shell 会话中使用 pushd 命令保存的目录栈。
  • FUNCNAME: 包含当前正在执行的 shell 函数的名称,函数嵌套时会有多个元素。
  • GROUPS: 包含当前 shell 进程的组 ID 列表。
  • PIPESTATUS: 包含前一个管道中每个命令的退出状态。${PIPESTATUS[0]} 是第一个命令的退出状态,${PIPESTATUS[1]} 是第二个命令的退出状态,以此类推。

引用部分数组元素

  • ${ARRAY_NAME[INDEX]}(如果省略 [INDEX] 表示引用下标为0的元素)

范例:

## declare -a 可省略(在 Bash 中,如果您在变量名称前添加了一个括号,Bash 会自动将其解释为一个数组变量)
## declare -a title=("ceo" "coo" "cto")


## echo ${title[1]}
coo


## 如果省略 [INDEX] 表示引用下标为0的元素
## echo ${title}
ceo


## echo ${title[2]}
cto


## echo ${title[3]}


## declare -a | grep title
declare -a title=([0]="ceo" [1]="coo" [2]="cto")

引用所有数组元素

  • ${ARRAY_NAME[*]}
  • ${ARRAY_NAME[@]}

范例:

## title=("ceo" "coo" "cto")


## echo ${title[*]}
ceo coo cto


## echo ${title[@]}
ceo coo cto

${ARRAY_NAME[*]} 和 ${ARRAY_NAME[@]} 有什么区别

在 Bash shell 中,${ARRAY_NAME[*]}${ARRAY_NAME[@]} 都用于展开数组 ARRAY_NAME 中的所有元素。它们的主要区别在于,当数组中的元素被双引号引用时,${ARRAY_NAME[*]} 展开为一个单独的字符串,而${ARRAY_NAME[@]} 展开为多个独立的字符串。

具体来说,当数组元素被双引号引用时,${ARRAY_NAME[*]} 展开为一个由所有元素组成的单个字符串,元素之间用第一个字符 of IFS(内部字段分隔符,通常为空格字符)分隔。例如:

$ my_array=("apple" "banana" "cherry")
$ echo "${my_array[*]}"
apple banana cherry

在上面的例子中,${my_array[*]} 展开为一个由数组中所有元素组成的单个字符串,用空格分隔。

另一方面,${ARRAY_NAME[@]} 展开为多个独立的字符串,每个字符串包含一个数组元素。例如:

$ my_array=("apple" "banana" "cherry")
$ for fruit in "${my_array[@]}"; do echo "$fruit"; done
apple
banana
cherry

在上面的例子中,${my_array[@]} 展开为三个独立的字符串,每个字符串包含一个数组元素。

总之,${ARRAY_NAME[*]}${ARRAY_NAME[@]} 的区别在于,前者会将所有数组元素展开为一个字符串,后者会将数组元素展开为多个独立的字符串。

打印数组中元素的个数

  • ${#ARRAY_NAME[*]}
  • ${#ARRAY_NAME[@]}
## title=("ceo" "coo" "cto")


## echo ${#title[@]}
3


## echo ${#title[*]}
3

删除数组

  • unset ARRAY[INDEX]

删除数组中的部分元素

  • 删除数组中的某元素,会导致稀疏格式
## echo ${title[*]}
ceo coo cto

## unset title[1]

## echo ${title[*]}
ceo cto

删除整个数组

## echo ${title[*]}
ceo coo cto

## unset title

## echo ${title[*]}

## ```





## 数组数据处理

## 数组切片

- `${ARRAY[@]:offset:number}`
  - offset 要跳过的元素个数
  - number 要取出的元素个数

**范例:**

```sh
## num=({0..10})


## echo ${num[*]}
0 1 2 3 4 5 6 7 8 9 10


## echo ${num[*]:2:3}
2 3 4

取偏移量后的所有元素

  • {ARRAY[@]:offset}

范例:

## echo ${num[*]}
0 1 2 3 4 5 6 7 8 9 10

## echo ${num[*]:2}
2 3 4 5 6 7 8 9 10

向数组中追加元素

  • ARRAY[${#ARRAY_NAME[*]}]=value
  • ARRAY[${#ARRAY_NAME[@]}]=value

范例:

## echo ${num[*]}
0 1 2 3 4 5 6 7 8 9 10
## echo ${#num[*]}
11


## 追加元素
## num[${#num[*]}]=11


## echo ${num[*]}
0 1 2 3 4 5 6 7 8 9 10 11
## echo ${#num[*]}
12

数组使用范例

备份多个文件

  • 这个脚本定义了一个备份目录和要备份的文件列表。它检查备份目录是否存在,如果不存在则创建它。
  • 然后它循环遍历文件列表,并为每个文件创建一个备份文件,备份文件的名称以当前日期为后缀。
  • 备份文件名的格式为:/backup/filename-YYYYMMDD.bak
#!/bin/bash

## 定义备份目录和要备份的文件列表
BACKUP_DIR="/backup"
FILES_TO_BACKUP=(/etc/passwd /etc/group /etc/shadow)

## 检查备份目录是否存在
if [ ! -d $BACKUP_DIR ]; then
    echo "Backup directory doesn't exist. Creating it..."
    mkdir -p $BACKUP_DIR
fi

## 备份文件
for file in "${FILES_TO_BACKUP[@]}"; do
    filename=$(basename $file)
    backup_filename="${BACKUP_DIR}/${filename}-$(date +%Y%m%d).bak"
    echo "Backing up $file to $backup_filename..."
    cp $file $backup_filename
done

通过调用第三方API获取数据并解析

  • 这个脚本定义了一个 API URL 和一个存储数据的数组。
  • 它使用 curl 命令调用 API 获取数据,并将每行数据存储到数组中。
  • 然后,它循环遍历数组中的每个元素,并对每个元素进行解析。在这个例子中,解析代码被省略了。
#!/bin/bash

## 定义 API URL 和存储数据的数组
API_URL="https://api.example.com/data"
DATA_ARRAY=()

## 通过 API 获取数据,并将其存储到数组中
while read -r line; do
    DATA_ARRAY+=("$line")
done < <(curl -s $API_URL)

## 解析数据
for data in "${DATA_ARRAY[@]}"; do
    # 在此处添加解析代码
    echo $data
done

处理命令行参数

  • 这个脚本定义了一个数组,包含命令行选项。
  • 它使用 getopts 命令处理命令行参数,并将选择的选项存储到变量 selected_option 中。然后,
#!/bin/bash

## 定义一个数组,包含命令行选项
OPTIONS=("option1" "option2" "option3")

## 处理命令行参数
while getopts ":o:" opt; do
    case $opt in
        o)
            selected_option=$OPTARG
            ;;
        \?)
            echo "Invalid option: -$OPTARG" >&2
            exit 1
            ;;
        :)
            echo "Option -$OPTARG requires an argument." >&2
            exit 1
            ;;
    esac
done

## 检查选择的选项是否有效
if [[ ! " ${OPTIONS[@]} " =~ " ${selected_option} " ]]; then
    echo "Invalid option: $selected_option" >&2
    exit 1
fi

echo "Selected option: $selected_option"

在多个服务器上执行命令并收集输出

  • 这个脚本定义了一个服务器列表和要在每个服务器上执行的命令。
  • 它循环遍历服务器列表,并在每个服务器上执行命令。
  • 输出被收集到标准输出,并用一行分隔符分隔每个服务器的输出。
#!/bin/bash

## 定义服务器列表和要执行的命令
SERVERS=("server1" "server2" "server3")
COMMAND="uptime"

## 循环遍历服务器列表,执行命令并收集输出
for server in "${SERVERS[@]}"; do
    echo "Server: $server"
    ssh $server $COMMAND
    echo "=============================================="
done

统计文本文件中不同单词的数量

  • 这个脚本读取一个文本文件,并统计其中每个单词出现的次数。
  • 它使用一个关联数组来存储单词计数,关联数组的键是单词,值是单词出现的次数。
  • 然后,它循环遍历关联数组,打印每个单词出现的次数。
#!/bin/bash

## 定义文本文件名和单词计数数组
FILE_NAME="textfile.txt"
declare -A WORD_COUNTS=()

## 读取文本文件中的每个单词,并将其添加到计数数组中
while read -r line; do
    for word in $line; do
        if [[ ! -z "$word" ]]; then
            (( WORD_COUNTS["$word"]++ ))
        fi
    done
done < $FILE_NAME

## 打印每个单词出现的次数
for word in "${!WORD_COUNTS[@]}"; do
    echo "$word: ${WORD_COUNTS[$word]}"
done
  1. 定义变量 FILE_NAME,该变量存储要读取的文本文件的名称。还定义了一个关联数组 WORD_COUNTS,该数组用于存储单词计数。
  2. 使用 while 循环读取文件 $FILE_NAME 中的每一行。read -r line 语句将每一行读入变量 line,并使用 for 循环对该行中的每个单词进行迭代。
  3. 如果单词不为空,则将 WORD_COUNTS 数组中该单词的计数加 1。
  4. 循环完成后,使用 for 循环打印每个单词及其出现次数。${!WORD_COUNTS[@]} 表示遍历 WORD_COUNTS 数组中的所有单词。${WORD_COUNTS[$word]} 则表示获取关联数组 WORD_COUNTS$word 对应的值。

${!WORD_COUNTS[@]} 中的 ! 是什么意思

在 Bash 脚本中,${!WORD_COUNTS[@]} 表示获取关联数组 WORD_COUNTS 中的所有键(即单词)。! 是一个间接引用符,用于获取数组中的键列表。具体来说,${!WORD_COUNTS[@]} 表示展开为数组 WORD_COUNTS 中的所有键列表。这个语法通常用于循环遍历关联数组的所有键。

批量重命名多个文件

  • 这个脚本定义了一个文件名前缀和一个文件列表。
  • 它循环遍历文件列表,并为每个文件生成一个新的文件名,新的文件名以前缀开头,并保留原来的文件扩展名。然后,它使用 mv 命令重命名每个文件。
#!/bin/bash

## 定义文件名前缀和文件列表
PREFIX="file"
FILES=("file1.txt" "file2.txt" "file3.txt")

## 循环遍历文件列表,重命名每个文件
for file in "${FILES[@]}"; do
    extension="${file##*.}"
    new_filename="${PREFIX}-${file%.*}.${extension}"
    mv $file $new_filename
done

其他范例一

其他范例二