shell教程

第26章. 数组

较新的Bash版本支持一维数组.数组元素可以用符号variable[xx]来初始化.另外,脚本可以用declare-avariable语句来清楚地指定一个数组.要访问一个数组元素,可以使用花括号来访问,即${variable[xx]}.例子26-1.简单的数组用法#!/bin/basharea[11]=23area[13]=37area[51]=UFOs#数组成员不必一定要连贯或连续的.#数组的一

较新的Bash版本支持一维数组. 数组元素可以用符号variable[xx]来初始化. 另外,脚本可以用declare -a variable语句来清楚地指定一个数组. 要访问一个数组元素,可以使用花括号来访问,即${variable[xx]}.


例子 26-1. 简单的数组用法

#!/bin/bash


area[11]=23
area[13]=37
area[51]=UFOs

#  数组成员不必一定要连贯或连续的.

#  数组的一部分成员允许不被初始化.
#  数组中空缺元素是允许的.
#  实际上,保存着稀疏数据的数组(“稀疏数组”)在电子表格处理软件中非常有用.
#


echo -n "area[11] = "
echo ${area[11]}    #  {大括号}是需要的.

echo -n "area[13] = "
echo ${area[13]}

echo "Contents of area[51] are ${area[51]}."

# 没有初始化内容的数组元素打印空值(NULL值).
echo -n "area[43] = "
echo ${area[43]}
echo "(area[43] unassigned)"

echo

# 两个数组元素的和被赋值给另一个数组元素
area[5]=`expr ${area[11]} + ${area[13]}`
echo "area[5] = area[11] + area[13]"
echo -n "area[5] = "
echo ${area[5]}

area[6]=`expr ${area[11]} + ${area[51]}`
echo "area[6] = area[11] + area[51]"
echo -n "area[6] = "
echo ${area[6]}
# 这里会失败是因为整数和字符串相加是不允许的.

echo; echo; echo

# -----------------------------------------------------------------
# 另一个数组, "area2".
# 另一种指定数组元素的值的办法...
# array_name=( XXX YYY ZZZ ... )

area2=( zero one two three four )

echo -n "area2[0] = "
echo ${area2[0]}
# 啊哈, 从0开始计数(即数组的第一个元素是[0], 而不是 [1]).

echo -n "area2[1] = "
echo ${area2[1]}    # [1] 是数组的第二个元素.
# -----------------------------------------------------------------

echo; echo; echo

# -----------------------------------------------
# 第三种数组, "area3".
# 第三种指定数组元素值的办法...
# array_name=([xx]=XXX [yy]=YYY ...)

area3=([17]=seventeen [24]=twenty-four)

echo -n "area3[17] = "
echo ${area3[17]}

echo -n "area3[24] = "
echo ${area3[24]}
# -----------------------------------------------

exit 0

Bash 允许把变量当成数组来操作,即使这个变量没有明确地被声明为数组.

string=abcABC123ABCabc
echo ${string[@]}               # abcABC123ABCabc
echo ${string[*]}               # abcABC123ABCabc
echo ${string[0]}               # abcABC123ABCabc
echo ${string[1]}               # 没有输出!
                                # 为什么?
echo ${#string[@]}              # 1
                                # 数组中只有一个元素.
                                # 且是这个字符串本身.

# Thank you, Michael Zick, for pointing this out.
类似的示范请参考Bash variables are untyped.


例子 26-2. 格式化一首诗

#!/bin/bash
# poem.sh: 排印出作者喜欢的一首诗.

# 诗的行数 (一小节诗).
Line[1]="I do not know which to prefer,"
Line[2]="The beauty of inflections"
Line[3]="Or the beauty of innuendoes,"
Line[4]="The blackbird whistling"
Line[5]="Or just after."

# 出处.
Attrib[1]=" Wallace Stevens"
Attrib[2]="\"Thirteen Ways of Looking at a Blackbird\""
# 此诗是公众的 (版权期已经到期了).

echo

for index in 1 2 3 4 5    # 5行.
do
  printf "     %s\n" "${Line[index]}"
done

for index in 1 2          # 打印两行出处行.
do
  printf "          %s\n" "${Attrib[index]}"
done

echo

exit 0

# 练习:
# --------
# 修改这个脚本使其从一个文本文件中提取内容打印一首行.

数组元素有它们独有的语法, 并且甚至Bash命令和操作符有特殊的选项可以支持数组使用.


例子 26-3. 多种数组操作

#!/bin/bash
# array-ops.sh: 数组更多有趣的用法.


array=( zero one two three four five )
# 元素    0   1   2    3     4    5

echo ${array[0]}       #  zero
echo ${array:0}        #  zero
                       #  第一个元素的参数扩展,
                       #+ 从位置0开始 (即第一个字符).
echo ${array:1}        #  ero
                       #  第一个元素的参数扩展,
                       #+ 从位置1开始 (即第二个字符).

echo "--------------"

echo ${#array[0]}      #  4
                       #  数组第一个元素的长度.
echo ${#array}         #  4
                       #  数组第一个元素的长度.
                       #  (另一种写法)

echo ${#array[1]}      #  3
                       #  数组第二个元素的长度.
                       #  Bash的数组是0开始索引的.

echo ${#array[*]}      #  6
                       #  数组中元素的个数.
echo ${#array[@]}      #  6
                       #  数组中元素的个数.

echo "--------------"

array2=( [0]="first element" [1]="second element" [3]="fourth element" )

echo ${array2[0]}      # 第一个元素
echo ${array2[1]}      # 第二个元素
echo ${array2[2]}      #
                       # 因为初始化时没有指定,因此值为空(null).
echo ${array2[3]}      # 第四个元素


exit 0

大部分标准的字符串操作符 可以用于数组操作.


例子 26-4. 用于数组的字符串操作符

#!/bin/bash
# array-strops.sh: 用于数组的字符串操作符.
# 由Michael Zick编码.
# 已征得作者的同意.

#  一般来说,任何类似 ${name ... } 写法的字符串操作符
#+ 都能在一个数组的所有字符串元素中使用
#+ 像${name[@] ... } 或 ${name[*] ...} 的写法.


arrayZ=( one two three four five five )

echo

# 提取尾部的子串
echo ${arrayZ[@]:0}     # one two three four five five
                        # 所有的元素.

echo ${arrayZ[@]:1}     # two three four five five
                        # 在第一个元素 element[0]后面的所有元素.

echo ${arrayZ[@]:1:2}   # two three
                        # 只提取在元素 element[0]后面的两个元素.

echo "-----------------------"

#  子串删除
#  从字符串的前部删除最短的匹配,
#+ 匹配字串是一个正则表达式.

echo ${arrayZ[@]#f*r}   # one two three five five
                        # 匹配表达式作用于数组所有元素.
                        # 匹配了"four"并把它删除.

# 字符串前部最长的匹配
echo ${arrayZ[@]##t*e}  # one two four five five
                        # 匹配表达式作用于数组所有元素.
                        # 匹配"three"并把它删除.

# 字符串尾部的最短匹配
echo ${arrayZ[@]%h*e}   # one two t four five five
                        # 匹配表达式作用于数组所有元素.
                        # 匹配"hree"并把它删除.

# 字符串尾部的最长匹配
echo ${arrayZ[@]%%t*e}  # one two four five five
                        # 匹配表达式作用于数组所有元素.
                        # 匹配"three"并把它删除.

echo "-----------------------"

# 子串替换

# 第一个匹配的子串会被替换
echo ${arrayZ[@]/fiv/XYZ}   # one two three four XYZe XYZe
                            # 匹配表达式作用于数组所有元素.

# 所有匹配的子串会被替换
echo ${arrayZ[@]//iv/YY}    # one two three four fYYe fYYe
                            # 匹配表达式作用于数组所有元素.

# 删除所有的匹配子串
# 没有指定代替字串意味着删除
echo ${arrayZ[@]//fi/}      # one two three four ve ve
                            # 匹配表达式作用于数组所有元素.

# 替换最前部出现的字串
echo ${arrayZ[@]/#fi/XY}    # one two three four XYve XYve
                            # 匹配表达式作用于数组所有元素.

# 替换最后部出现的字串
echo ${arrayZ[@]/%ve/ZZ}    # one two three four fiZZ fiZZ
                            # 匹配表达式作用于数组所有元素.

echo ${arrayZ[@]/%o/XX}     # one twXX three four five five
                            # 为什么?

echo "-----------------------"


# 在从awk(或其他的工具)取得数据之前 --
# 记得:
#   $( ... ) 是命令替换.
#   函数以子进程运行.
#   函数将输出打印到标准输出.
#   用read来读取函数的标准输出.
#   name[@]的写法指定了一个"for-each"的操作.

newstr() {
    echo -n "!!!"
}

echo ${arrayZ[@]/%e/$(newstr)}
# on!!! two thre!!! four fiv!!! fiv!!!
# Q.E.D: 替换部分的动作实际上是一个'赋值'.

#  使用"For-Each"型的
echo ${arrayZ[@]//*/$(newstr optional_arguments)}
#  现在Now, 如果if Bash只传递匹配$0的字符串给要调用的函数. . .
#

echo

exit 0

命令替换 能创建数组的新的单个元素.


例子 26-5. 将脚本的内容传给数组

#!/bin/bash
# script-array.sh: 把此脚本的内容传进数组.
# 从Chris Martin的e-mail中得到灵感 (多谢!).

script_contents=( $(cat "$0") )  #  把这个脚本($0)的内容存进数组.
                                 #

for element in $(seq 0 $((${#script_contents[@]} - 1)))
  do                #  ${#script_contents[@]}
                    #+ 表示数组中元素的个数.
                    #
                    #  问题:
                    #  为什么需要  seq 0  ?
                    #  试试更改成 seq 1.
  echo -n "${script_contents[$element]}"
                    # 将脚本的每行列成一个域.
  echo -n " -- "    # 使用" -- "作为域分隔符.
done

echo

exit 0

# 练习:
# --------
#  修改这个脚本使它能按照它原本的格式输出,
#+ 连同空白符,换行,等等.
#

在数组的环境里, 一些 Bash 内建的命令 含义有一些轻微的改变. 例如, unset 会删除数组元素, 或甚至删除整个数组.


例子 26-6. 一些数组专用的工具

#!/bin/bash

declare -a colors
#  所有脚本后面的命令都会把
#+ 变量"colors"作为数组对待.

echo "Enter your favorite colors (separated from each other by a space)."

read -a colors    # 键入至少3种颜色以用于下面的示例.
#  指定'read'命令的选项,
#+ 允许指定数组元素.

echo

element_count=${#colors[@]}
# 专用语法来提取数组元素的个数.
#     element_count=${#colors[*]} 也可以.
#
#  "@"变量允许分割引号内的单词
#+ (依靠空白字符来分隔变量).
#
#  这就像"$@" 和"$*"在位置参数中表现出来的一样.
#

index=0

while [ "$index" -lt "$element_count" ]
do    # List all the elements in the array.
  echo ${colors[$index]}
  let "index = $index + 1"
done
# 每个数组元素被列为单独的一行.
# 如果这个没有要求, 可以用  echo -n "${colors[$index]} "
#
# 可以用一个"for"循环来做:
#   for i in "${colors[@]}"
#   do
#     echo "$i"
#   done
# (Thanks, S.C.)

echo

# 再次列出数组中所有的元素, 但使用更优雅的做法.
  echo ${colors[@]}          # echo ${colors[*]} 也可以.

echo

# "unset"命令删除一个数组元素或是整个数组.
unset colors[1]              # 删除数组的第二个元素.
                             # 作用等同于   colors[1]=
echo  ${colors[@]}           # 再列出数组,第二个元素没有了.

unset colors                 # 删除整个数组.
                             #  unset colors[*] 或
                             #+ unset colors[@] 都可以.
echo; echo -n "Colors gone."
echo ${colors[@]}            # 再列出数组, 则为空了.

exit 0

正如在前面的例子中看到的, ${array_name[@]}${array_name[*]} 都与数组的所有元素相关. 同样地, 为了计算数组的元素个数, 可以用${#array_name[@]}${#array_name[*]}. ${#array_name} 是数组第一个元素${array_name[0]}的长度(字符数) .


例子 26-7. 关于空数组和空数组元素

#!/bin/bash
# empty-array.sh

#  多谢 Stephane Chazelas 制作这个例子最初的版本,
#+ 并由 Michael Zick 扩展了.


# 空数组不同与含有空值元素的数组.

array0=( first second third )
array1=( '' )   # "array1" 由一个空元素组成.
array2=( )      # 没有元素 . . . "array2" 是空的.

echo
ListArray()
{
echo
echo "Elements in array0:  ${array0[@]}"
echo "Elements in array1:  ${array1[@]}"
echo "Elements in array2:  ${array2[@]}"
echo
echo "Length of first element in array0 = ${#array0}"
echo "Length of first element in array1 = ${#array1}"
echo "Length of first element in array2 = ${#array2}"
echo
echo "Number of elements in array0 = ${#array0[*]}"  # 3
echo "Number of elements in array1 = ${#array1[*]}"  # 1  (惊奇!)
echo "Number of elements in array2 = ${#array2[*]}"  # 0
}

# ===================================================================

ListArray

# 尝试扩展这些数组.

# 增加一个元素到数组.
array0=( "${array0[@]}" "new1" )
array1=( "${array1[@]}" "new1" )
array2=( "${array2[@]}" "new1" )

ListArray

# 或
array0[${#array0[*]}]="new2"
array1[${#array1[*]}]="new2"
array2[${#array2[*]}]="new2"

ListArray

# 当像上面的做法增加数组时,数组像 '栈'
# 上面的做法是 'push(压栈)'
# 栈高是:
height=${#array2[@]}
echo
echo "Stack height for array2 = $height"

# 'pop(出栈)' 是:
unset array2[${#array2[@]}-1]   # 数组是以0开始索引的,
height=${#array2[@]}            #+ 这就意味着第一个元素下标是 0.
echo
echo "POP"
echo "New stack height for array2 = $height"

ListArray

# 只列出数组array0的第二和第三个元素.
from=1		 #是以0开始的数字
to=2		#
array3=( ${array0[@]:1:2} )
echo
echo "Elements in array3:  ${array3[@]}"

# 像一个字符串一样处理(字符的数组).
# 试试其他的字符串格式.

# 替换:
array4=( ${array0[@]/second/2nd} )
echo
echo "Elements in array4:  ${array4[@]}"

# 替换所有匹配通配符的字符串.
array5=( ${array0[@]//new?/old} )
echo
echo "Elements in array5:  ${array5[@]}"

# 当你开始觉得对此有把握的时候 . . .
array6=( ${array0[@]#*new} )
echo # 这个可能会使你感到惊奇.
echo "Elements in array6:  ${array6[@]}"

array7=( ${array0[@]#new1} )
echo # 数组array6之后就没有惊奇了.
echo "Elements in array7:  ${array7[@]}"

# 这看起来非常像 . . .
array8=( ${array0[@]/new1/} )
echo
echo "Elements in array8:  ${array8[@]}"

#  那么我们怎么总结它呢So what can one say about this?

#  字符串操作在数组var[@]的每一个元素中执行.
#
#  因此Therefore : 如果结果是一个零长度的字符串,
#+ Bash支持字符串向量操作,
#+ 元素会在结果赋值中消失不见.

#  提问, 这些字符串是强还是弱引用?

zap='new*'
array9=( ${array0[@]/$zap/} )
echo
echo "Elements in array9:  ${array9[@]}"

# 当你还在想你在Kansas州的何处时 . . .
array10=( ${array0[@]#$zap} )
echo
echo "Elements in array10:  ${array10[@]}"

# 把 array7 和 array10比较.
# 把 array8 和 array9比较.

# 答案: 必须用弱引用.

exit 0

${array_name[@]}${array_name[*]} 的关系类似于$@ and $*. 这种数组用法非常有用.

# 复制一个数组.
array2=( "${array1[@]}" )
# 或
array2="${array1[@]}"

# 给数组增加一个元素.
array=( "${array[@]}" "new element" )
# 或
array[${#array[*]}]="new element"

# Thanks, S.C.

array=( element1 element2 ... elementN ) 初始化操作, 依赖于命令替换(command substitution)使将一个文本内容加载进数组成为可能.

#!/bin/bash

filename=sample_file

#            cat sample_file
#
#            1 a b c
#            2 d e fg


declare -a array1

array1=( `cat "$filename"`)                #  加载$filename文件的内容进数组array1.
#         打印文件到标准输出               #
#
#  array1=( `cat "$filename" | tr '\n' ' '`)
#                            把文件里的换行变为空格.
#  这是没必要的,因为Bash做单词分割时会把换行变为空格.
#

echo ${array1[@]}            # 打印数组.
#                              1 a b c 2 d e fg
#
#  文件中每个由空白符分隔开的“词”都被存在数组的一个元素里
#

element_count=${#array1[*]}
echo $element_count          # 8

出色的技巧使数组的操作技术又多了一种.


例子 26-8. 初始化数组

#! /bin/bash
# array-assign.bash

#  数组操作是Bash特有的,
#+ 因此脚本名用".bash"结尾.

# Copyright (c) Michael S. Zick, 2003, All rights reserved.
# 许可证: 没有任何限制,可以用于任何目的的反复使用.
# Version: $ID$
#
# 由William Park添加注释.

#  基于Stephane Chazelas提供在本书中的一个例子
#

# 'times' 命令的输出格式:
# User CPU <空格> System CPU
# User CPU of dead children <空格> System CPU of dead children

#  Bash赋一个数组的所有元素给新的数组变量有两种办法.
#
#  在Bash版本2.04, 2.05a 和 2.05b,
#+ 这两种办法都对NULL的值的元素全部丢弃.
#  另一种数组赋值办法是维护[下标]=值之间的关系将会在新版本的Bash支持.
#

#  可以用外部命令来构造一个大数组,
#+ 但几千个元素的数组如下就可以构造了.
#

declare -a bigOne=( /dev/* )
echo
echo 'Conditions: Unquoted, default IFS, All-Elements-Of'
echo "Number of elements in array is ${#bigOne[@]}"

# set -vx



echo
echo '- - testing: =( ${array[@]} ) - -'
times
declare -a bigTwo=( ${bigOne[@]} )
#                 ^              ^
times

echo
echo '- - testing: =${array[@]} - -'
times
declare -a bigThree=${bigOne[@]}
# 这次没有用括号.
times

#  正如Stephane Chazelas指出的那样比较输出的数组可以了解第二种格式的赋值比第三和第四的times的更快
#
#
#  William Park 解释explains:
#+ bigTwo 数组是被赋值了一个单字符串,
#+ bigThree 则赋值时一个一个元素的赋值.
#  所以, 实际上的情况是:
#                   bigTwo=( [0]="... ... ..." )
#                   bigThree=( [0]="..." [1]="..." [2]="..." ... )


#  我在本书的例子中仍然会继续用第一种格式,
#+ 因为我认为这会对说明清楚更有帮助.

#  我的例子中的可复用的部分实际上还是会使用第二种格式,
#+ 因为这种格式更快一些.

# MSZ: 很抱歉早先的失误(应是指本书的先前版本).


#  注:
#  ----
#  在31和43行的"declare -a"语句不是必须的,
#+ 因为会在使用Array=( ... )赋值格式时暗示它是数组.
#
#  但是, 省略这些声明会导致后面脚本的相关操作更慢一些.
#
#  试一下, 看有什么变化.

exit 0

对变量增加 declare -a 语句声明可以加速后面的数组操作速度.


例子 26-9. 复制和连接数组

#! /bin/bash
# CopyArray.sh
#
# 由 Michael Zick编写.
# 在本书中使用已得到许可.

#  怎么传递变量名和值处理,返回就用使用该变量,
#+ 或说"创建你自己的赋值语句".


CpArray_Mac() {

# 创建赋值命令语句

    echo -n 'eval '
    echo -n "$2"                    # 目的变量名
    echo -n '=( ${'
    echo -n "$1"                    # 源名字
    echo -n '[@]} )'

# 上面的全部会合成单个命令.
# 这就是函数所有的功能.
}

declare -f CopyArray                # 函数"指针"
CopyArray=CpArray_Mac               # 建立命令

Hype()
{

# 要复制的数组名为 $1.
# (接合数组,并包含尾部的字符串"Really Rocks".)
# 返回结果的数组名为 $2.

    local -a TMP
    local -a hype=( Really Rocks )

    $($CopyArray $1 TMP)
    TMP=( ${TMP[@]} ${hype[@]} )
    $($CopyArray TMP $2)
}

declare -a before=( Advanced Bash Scripting )
declare -a after

echo "Array Before = ${before[@]}"

Hype before after

echo "Array After = ${after[@]}"

# 有多余的字符串?

echo "What ${after[@]:3:2}?"

declare -a modest=( ${after[@]:2:1} ${after[@]:3:2} )
#                    ----     子串提取       ----

echo "Array Modest = ${modest[@]}"

# 'before'变量变成什么了 ?

echo "Array Before = ${before[@]}"

exit 0


例子 26-10. 关于连接数组的更多信息

#! /bin/bash
# array-append.bash

# Copyright (c) Michael S. Zick, 2003, All rights reserved.
# 许可: 可以无限制的以任何目的任何格式重复使用.
# 版本: $ID$
#
# 格式上由M.C做了轻微的修改.


# 数组操作是Bash特有的属性.
# 原来的 UNIX /bin/sh 没有类似的功能.


#  把此脚本的输出管道输送给 'more'
#+ 以便输出不会滚过终端屏幕.


# 下标依次使用.
declare -a array1=( zero1 one1 two1 )
# 下标有未使用的 ([1] 没有被定义).
declare -a array2=( [0]=zero2 [2]=two2 [3]=three2 )

echo
echo '- Confirm that the array is really subscript sparse. -'
echo "Number of elements: 4"        # 这儿是举例子就用硬编码.
for (( i = 0 ; i < 4 ; i++ ))
do
    echo "Element [$i]: ${array2[$i]}"
done
# 也可以参考basics-reviewed.bash更多的常见代码.


declare -a dest

# 组合 (添加) 两个数组到第三个数组.
echo
echo 'Conditions: Unquoted, default IFS, All-Elements-Of operator'
echo '- Undefined elements not present, subscripts not maintained. -'
# # 那些未定义的元素不存在; 组合时会丢弃这些元素.

dest=( ${array1[@]} ${array2[@]} )
# dest=${array1[@]}${array2[@]}     # 奇怪的结果, 或者叫臭虫.

# 现在, 打印出结果.
echo
echo '- - Testing Array Append - -'
cnt=${#dest[@]}

echo "Number of elements: $cnt"
for (( i = 0 ; i < cnt ; i++ ))
do
    echo "Element [$i]: ${dest[$i]}"
done

# 把一个数组赋值给另一个数组的单个元素 (两次).
dest[0]=${array1[@]}
dest[1]=${array2[@]}

# 列出结果.
echo
echo '- - Testing modified array - -'
cnt=${#dest[@]}

echo "Number of elements: $cnt"
for (( i = 0 ; i < cnt ; i++ ))
do
    echo "Element [$i]: ${dest[$i]}"
done

# 检测第二个元素的改变.
echo
echo '- - Reassign and list second element - -'

declare -a subArray=${dest[1]}
cnt=${#subArray[@]}

echo "Number of elements: $cnt"
for (( i = 0 ; i < cnt ; i++ ))
do
    echo "Element [$i]: ${subArray[$i]}"
done

#  用 '=${ ... }' 把整个数组的值赋给另一个数组的单个元素
#+ 使数组所有元素值被转换成了一个字符串,各元素的值由一个空格分开(其实是IFS的第一个字符).
#
#

# 如果原先的元素没有包含空白符 . . .
# 如果原先的数组下标都是连续的 . . .
# 我们就能取回最初的数组结构.

# 恢复第二个元素的修改回元素.
echo
echo '- - Listing restored element - -'

declare -a subArray=( ${dest[1]} )
cnt=${#subArray[@]}

echo "Number of elements: $cnt"
for (( i = 0 ; i < cnt ; i++ ))
do
    echo "Element [$i]: ${subArray[$i]}"
done
echo '- - Do not depend on this behavior. - -'
echo '- - This behavior is subject to change - -'
echo '- - in versions of Bash newer than version 2.05b - -'

# MSZ: 很抱歉早先时混淆的几个要点(译者注:应该是指本书早先的版本).

exit 0

--

数组允许在脚本中实现一些常见的熟悉算法.这是否是必要的好想法在此不讨论,留给读者自行判断.


例子 26-11. 一位老朋友: 冒泡排序

#!/bin/bash
# bubble.sh: 排序法之冒泡排序.

# 回忆冒泡排序法. 在这个版本中要实现它...

#  靠连续地多次比较数组元素来排序,
#+ 比较两个相邻的元素,如果排序顺序不对,则交换两者的顺序.
#  当第一轮比较结束后,最"重"的元素就被排到了最底部.
#  当第二轮比较结束后,第二"重"的元素就被排到了次底部的位置.
#  以此类推.
#  这意味着每轮的比较不需要比较先前已"沉淀"好的数据.
#  因此你会注意到后面数据的打印会比较快一些.


exchange()
{
  # 交换数组的两个元素.
  local temp=${Countries[$1]} #  临时保存要交换的一个元素.
                              #
  Countries[$1]=${Countries[$2]}
  Countries[$2]=$temp

  return
}

declare -a Countries  #  声明数组,
                      #+ 在此是可选的,因为下面它会被按数组来初始化.

#  是否允许用转义符(\)将数组的各变量值放到几行上?
#
#  是的.

Countries=(Netherlands Ukraine Zaire Turkey Russia Yemen Syria \
Brazil Argentina Nicaragua Japan Mexico Venezuela Greece England \
Israel Peru Canada Oman Denmark Wales France Kenya \
Xanadu Qatar Liechtenstein Hungary)

# "Xanadu" 是个虚拟的充满美好的神话之地.
#


clear                      # 开始之前清除屏幕.

echo "0: ${Countries[*]}"  # 从0索引的元素开始列出整个数组.

number_of_elements=${#Countries[@]}
let "comparisons = $number_of_elements - 1"

count=1 # 传递数字.

while [ "$comparisons" -gt 0 ]          # 开始外部的循环
do

  index=0  # 每轮开始前重设索引值为0.

  while [ "$index" -lt "$comparisons" ] # 开始内部循环
  do
    if [ ${Countries[$index]} \> ${Countries[`expr $index + 1`]} ]
    #  如果原来的排序次序不对...
    #  回想一下 \> 在单方括号里是is ASCII 码的比较操作符.
    #

    #  if [[ ${Countries[$index]} > ${Countries[`expr $index + 1`]} ]]
    #+ 也可以.
    then
      exchange $index `expr $index + 1`  # 交换.
    fi
    let "index += 1"
  done # 内部循环结束

# ----------------------------------------------------------------------
# Paulo Marcel Coelho Aragao 建议使用更简单的for-loops.
#
# for (( last = $number_of_elements - 1 ; last > 1 ; last-- ))
# do
#     for (( i = 0 ; i < last ; i++ ))
#     do
#         [[ "${Countries[$i]}" > "${Countries[$((i+1))]}" ]] \
#             && exchange $i $((i+1))
#     done
# done
# ----------------------------------------------------------------------


let "comparisons -= 1" #  因为最"重"的元素冒到了最底部,
                       #+ 我们可以每轮少做一些比较.

echo
echo "$count: ${Countries[@]}"  # 每轮结束后,打印一次数组.
echo
let "count += 1"                # 增加传递计数.

done                            # 外部循环结束
                                # 完成.

exit 0

--

在数组内嵌一个数组有可能做到吗?

#!/bin/bash
# "内嵌" 数组.

#  Michael Zick 提供这个例子,
#+ 由William Park作了些纠正和解释.

AnArray=( $(ls --inode --ignore-backups --almost-all \
	--directory --full-time --color=none --time=status \
	--sort=time -l ${PWD} ) )  # 命令及选项.

# 空格是有意义的 . . . 不要在上面引号引用任何东西.

SubArray=( ${AnArray[@]:11:1}  ${AnArray[@]:6:5} )
#  这个数组有6个元素:
#+     SubArray=( [0]=${AnArray[11]} [1]=${AnArray[6]} [2]=${AnArray[7]}
#      [3]=${AnArray[8]} [4]=${AnArray[9]} [5]=${AnArray[10]} )
#
#  Bash中的数组像是字符串(char *)型的(循环)链表.
#
#  因此, 这实际上不是内嵌的数组,
#+ 但它的功能是相似的.

echo "Current directory and date of last status change:"
echo "${SubArray[@]}"

exit 0

--

内嵌数组和间接引用(indirect references) 的组合使用产生了一些有趣的用法.


例子 26-12. 内嵌数组和间接引用

#!/bin/bash
# embedded-arrays.sh
# 内嵌数组和间接引用.

# 由Dennis Leeuw编写.
# 已获使用许可.
# 由本文作者修改.


ARRAY1=(
        VAR1_1=value11
        VAR1_2=value12
        VAR1_3=value13
)

ARRAY2=(
        VARIABLE="test"
        STRING="VAR1=value1 VAR2=value2 VAR3=value3"
        ARRAY21=${ARRAY1[*]}
)       # 把ARRAY1数组嵌到这个数组里.

function print () {
        OLD_IFS="$IFS"
        IFS=$'\n'       #  这是为了在每个行打印一个数组元素.
                        #
        TEST1="ARRAY2[*]"
        local ${!TEST1} # 试下删除这行会发生什么.
        #  间接引用.
	#  这使 $TEST1只在函数内存取。
	#


        #  我们看看还能干点什么.
        echo
        echo "\$TEST1 = $TEST1"       #  变量的名称.
        echo; echo
        echo "{\$TEST1} = ${!TEST1}"  #  变量的内容.
                                      #  这就是间接引用的作用.
                                      #
        echo
        echo "-------------------------------------------"; echo
        echo


        # 打印变量
        echo "Variable VARIABLE: $VARIABLE"

        # 打印一个字符串元素
        IFS="$OLD_IFS"
        TEST2="STRING[*]"
        local ${!TEST2}      # 间接引用 (像上面一样).
        echo "String element VAR2: $VAR2 from STRING"

        # 打印一个字符串元素
        TEST2="ARRAY21[*]"
        local ${!TEST2}      # 间接引用 (像上面一样).
        echo "Array element VAR1_1: $VAR1_1 from ARRAY21"
}

print
echo

exit 0

#   脚本作者注,
#+ "你可以很容易地将其扩展成Bash的一个能创建hash的脚本."
#   (难) 留给读者的练习: 实现它.

--

数组使埃拉托色尼素数筛子有了shell脚本的实现. 当然, 如果是追求效率的应用自然应该用一种编译型的语言,例如用C. 这种脚本运行实在是太慢.


例子 26-13. 复杂数组应用: 埃拉托色尼素数筛子

#!/bin/bash
# sieve.sh (ex68.sh)

# 埃拉托色尼素数筛子
# 找素数的经典算法.

#  在同等数量的数值内这个脚本比用C写的版本慢很多.
#

LOWER_LIMIT=1       # 从1开始.
UPPER_LIMIT=1000    # 到 1000.
# (如果你很有时间的话,你可以把它设得更高 . . . )

PRIME=1
NON_PRIME=0

let SPLIT=UPPER_LIMIT/2
# 优化:
# 只需要测试中间到最大之间的值 (为什么?).


declare -a Primes
# Primes[] 是一个数组.


initialize ()
{
# 初始化数组.

i=$LOWER_LIMIT
until [ "$i" -gt "$UPPER_LIMIT" ]
do
  Primes[i]=$PRIME
  let "i += 1"
done
#  假定所有的数组成员都是需要检查的 (素数)
#+ 一直到检查完成前.
}

print_primes ()
{
# 打印出所有Primes[]数组中被标记为素数的元素.

i=$LOWER_LIMIT

until [ "$i" -gt "$UPPER_LIMIT" ]
do

  if [ "${Primes[i]}" -eq "$PRIME" ]
  then
    printf "%8d" $i
    # 每个数字打印前先打印8个空格, 数字是在偶数列打印的.
  fi

  let "i += 1"

done

}

sift () # 查出非素数.
{

let i=$LOWER_LIMIT+1
# 我们都知道1是素数, 所以我们从2开始.

until [ "$i" -gt "$UPPER_LIMIT" ]
do

if [ "${Primes[i]}" -eq "$PRIME" ]
# 不要处理已经过滤过的数字 (被标识为非素数).
then

  t=$i

  while [ "$t" -le "$UPPER_LIMIT" ]
  do
    let "t += $i "
    Primes[t]=$NON_PRIME
    # 标识为非素数.
  done

fi

  let "i += 1"
done


}


# ==============================================
# main ()
# 继续调用函数.
initialize
sift
print_primes
# 这就是被称为结构化编程的东西了.
# ==============================================

echo

exit 0



# -------------------------------------------------------- #
# 因为前面的一个'exit',所以下面的代码不会被执行.

#  下面是Stephane Chazelas写的一个埃拉托色尼素数筛子的改进版本,
#+ 运行会稍微快一点.

# 必须在命令行上指定参数(寻找素数的限制范围).

UPPER_LIMIT=$1                  # 值来自命令行.
let SPLIT=UPPER_LIMIT/2         # 从中间值到最大值.

Primes=( '' $(seq $UPPER_LIMIT) )

i=1
until (( ( i += 1 ) > SPLIT ))  # 仅需要从中间值检查.
do
  if [[ -n $Primes[i] ]]
  then
    t=$i
    until (( ( t += i ) > UPPER_LIMIT ))
    do
      Primes[t]=
    done
  fi
done
echo ${Primes[*]}

exit 0

比较这个用数组的素数产生器和另一种不用数组的例子 A-16.

--

数组可以做一定程度的扩展,以模拟支持Bash原本不支持的数据结构.


例子 26-14. 模拟下推的堆栈

#!/bin/bash
# stack.sh: 下推的堆栈模拟

#  类似于CPU栈, 下推的堆栈依次保存数据项,
#+ 但取出时则反序进行, 后进先出.

BP=100            #  栈数组的基点指针.
                  #  从元素100开始.

SP=$BP            #  栈指针.
                  #  初始化栈底.

Data=             #  当前栈的内容.
                  #  必须定义成全局变量,
                  #+ 因为函数的返回整数有范围限制.

declare -a stack


push()            # 把一个数据项压入栈.
{
if [ -z "$1" ]    # 没有可压入的?
then
  return
fi

let "SP -= 1"     # 更新堆栈指针.
stack[$SP]=$1

return
}

pop()                    # 从栈中弹出一个数据项.
{
Data=                    # 清空保存数据项中间变量.

if [ "$SP" -eq "$BP" ]   # 已经没有数据可弹出?
then
  return
fi                       #  这使SP不会超过100,
                         #+ 例如, 这可保护一个失控的堆栈.

Data=${stack[$SP]}
let "SP += 1"            # 更新堆栈指针.
return
}

status_report()          # 打印堆栈的当前状态.
{
echo "-------------------------------------"
echo "REPORT"
echo "Stack Pointer = $SP"
echo "Just popped \""$Data"\" off the stack."
echo "-------------------------------------"
echo
}


# =======================================================
# 现在,来点乐子.

echo

# 看你是否能从空栈里弹出数据项来.
pop
status_report

echo

push garbage
pop
status_report     # 压入garbage, 弹出garbage.

value1=23; push $value1
value2=skidoo; push $value2
value3=FINAL; push $value3

pop              # FINAL
status_report
pop              # skidoo
status_report
pop              # 23
status_report    # 后进, 先出!

#  注意堆栈指针每次压栈时减,
#+ 每次弹出时加一.

echo

exit 0

# =======================================================


# 练习:
# ---------

# 1)  修改"push()"函数,使其调用一次就能够压入多个数据项.
#

# 2)  修改"pop()"函数,使其调用一次就能弹出多个数据项.
#

# 3)  给那些有临界操作的函数增加出错检查.
#     即是指是否一次完成操作或没有完成操作返回相应的代码,
#   + 没有完成要启动合适的处理动作.
#

# 4)  这个脚本为基础,
#   + 写一个栈实现的四则运算计算器.

--

要想操作数组的下标需要中间变量. 如果确实要这么做, 可以考虑使用一种更强功能的编程语言, 例如 Perl 或 C.


例子 26-15. 复杂的数组应用: 列出一种怪异的数学序列

#!/bin/bash

# Douglas Hofstadter的有名的"Q-series":

# Q(1) = Q(2) = 1
# Q(n) = Q(n - Q(n-1)) + Q(n - Q(n-2)), 当 n>2 时

# 这是令人感到陌生的也是没有规律的"乱序"整数序列.
# 序列的头20个如下所示:
# 1 1 2 3 3 4 5 5 6 6 6 8 8 8 10 9 10 11 11 12

#  参考Hofstadter的书, "Goedel, Escher, Bach: An Eternal Golden Braid",
#+ 页码 137.


LIMIT=100     # 计算数的个数.
LINEWIDTH=20  # 很行要打印的数的个数.

Q[1]=1        # 序列的头2个是 1.
Q[2]=1

echo
echo "Q-series [$LIMIT terms]:"
echo -n "${Q[1]} "             # 打印头2个数.
echo -n "${Q[2]} "

for ((n=3; n <= $LIMIT; n++))  # C风格的循环条件.
do   # Q[n] = Q[n - Q[n-1]] + Q[n - Q[n-2]]  当 n>2 时
#  需要将表达式分步计算,
#+ 因为Bash不擅长处理此类复杂计算.

  let "n1 = $n - 1"        # n-1
  let "n2 = $n - 2"        # n-2

  t0=`expr $n - ${Q[n1]}`  # n - Q[n-1]
  t1=`expr $n - ${Q[n2]}`  # n - Q[n-2]

  T0=${Q[t0]}              # Q[n - Q[n-1]]
  T1=${Q[t1]}              # Q[n - Q[n-2]]

Q[n]=`expr $T0 + $T1`      # Q[n - Q[n-1]] + Q[n - Q[n-2]]
echo -n "${Q[n]} "

if [ `expr $n % $LINEWIDTH` -eq 0 ]    # 格式化输出.
then   #      ^ 取模操作
  echo # 把行分成内部的块.
fi

done

echo

exit 0

# 这是Q-series问题的迭代实现.
# 更直接明了的递归实现留给读者完成.
# 警告: 递归地计算这个序列会花很长的时间.

--

Bash 只支持一维数组,但有一些技巧可用来模拟多维数组.


例子 26-16. 模拟二维数组,并使它倾斜

#!/bin/bash
# twodim.sh: 模拟二维数组.

# 一维数组由单行组成.
# 二维数组由连续的行组成.

Rows=5
Columns=5
# 5 X 5 的数组Array.

declare -a alpha     # char alpha [Rows] [Columns];
                     # 不必要的声明. 为什么?

load_alpha ()
{
local rc=0
local index

for i in A B C D E F G H I J K L M N O P Q R S T U V W X Y
do     # 如果你高兴,可以使用不同的符号.
  local row=`expr $rc / $Columns`
  local column=`expr $rc % $Rows`
  let "index = $row * $Rows + $column"
  alpha[$index]=$i
# alpha[$row][$column]
  let "rc += 1"
done

#  更简单的办法
#+   declare -a alpha=( A B C D E F G H I J K L M N O P Q R S T U V W X Y )
#+ 但这就缺少了二维数组的感觉了.
}

print_alpha ()
{
local row=0
local index

echo

while [ "$row" -lt "$Rows" ]   #  以行顺序为索引打印行的各元素:
do                             #+ 即数组列值变化快,
                               #+ 行值变化慢.
  local column=0

  echo -n "       "            #  依行倾斜打印正方形的数组.

  while [ "$column" -lt "$Columns" ]
  do
    let "index = $row * $Rows + $column"
    echo -n "${alpha[index]} "  # alpha[$row][$column]
    let "column += 1"
  done

  let "row += 1"
  echo

done

# 等同于
#     echo ${alpha[*]} | xargs -n $Columns

echo
}

filter ()     # 过滤出负数的数组索引.
{

echo -n "  "  # 产生倾斜角度.
              # 解释怎么办到的.

if [[ "$1" -ge 0 &&  "$1" -lt "$Rows" && "$2" -ge 0 && "$2" -lt "$Columns" ]]
then
    let "index = $1 * $Rows + $2"
    # Now, print it rotated现在,打印旋转角度.
    echo -n " ${alpha[index]}"
    #           alpha[$row][$column]
fi

}




rotate ()  #  旋转数组 45 度 --
{          #+ 在左下角"平衡"图形.
local row
local column

for (( row = Rows; row > -Rows; row-- ))
  do       # 从后面步进数组. 为什么?

  for (( column = 0; column < Columns; column++ ))
  do

    if [ "$row" -ge 0 ]
    then
      let "t1 = $column - $row"
      let "t2 = $column"
    else
      let "t1 = $column"
      let "t2 = $column + $row"
    fi

    filter $t1 $t2   # 过滤出负数数组索引.
                     # 如果你不这样做会怎么样?
  done

  echo; echo

done

#  数组旋转灵感源于Herbert Mayer写的
#+ "Advanced C Programming on the IBM PC," 的例子 (页码. 143-146)
#+ (看参考书目附录).
#  这也能看出C能做的事情有多少能用shell脚本做到.
#

}


#---------------   现在, 可以开始了.     ------------#
load_alpha     # 加载数组.
print_alpha    # 打印数组.
rotate         # 反时钟旋转数组45度.
#-----------------------------------------------------#

exit 0

# 这是有点做作,不太优雅.

# 练习:
# ---------
# 1)  重写数组加载和打印函数,
#     使其更直观和容易了解.
#
# 2)  指出数组旋转函数是什么原理.
#     Hint索引: 思考数组从尾向前索引的实现.
#
# 3)  重写脚本使其可以处理非方形数组Rewrite this script to handle a non-square array,
#     例如 6 X 4 的数组.
#     尝试旋转数组时做到最小"失真".

二维数组本质上等同于一维数组, 而只增加了使用行和列的位置来引用和操作元素的寻址模式.

关于二维数组更好的例子, 请参考例子 A-10.

--

另一个有趣的使用数组的脚本:

© 内存溢出 OutOfMemory.CN