如何使用数组变量(一)?

超级欧派课程 2024-03-09 00:18:18

本答案假设您对数组有基本的了解。如果您对这种编程方式还不熟悉,可以从指南的解释开始。本页面更加详细。底部的链接提供了更多资源。

1. 简介

Bash、Zsh和大多数的KornShell(包括 AT&T ksh88 或更高版本、mksh 和 pdksh)都实现了一维整数索引数组。POSIX标准没有指定数组,并且在像BourneShell和Dash这样的遗留或精简shell中不可用。支持数组的符合POSIX标准的shell在基本原则上大致一致,但在细节上有一些显著差异。多个shell的高级用户应该确保研究具体情况。Ksh93、Zsh和Bash 4.0还具有关联数组(请参见我上一篇文章)。本文重点介绍索引数组,因为它们是最常见的类型。

基本语法摘要(适用于 bash,数学索引数组):

以下是一个典型的使用模式,使用名为host的数组:

# Bash# 将值 "mickey"、"minnie" 和 "goofy" 分配给从零开始的连续索引。host=(mickey minnie goofy)# 遍历 "host" 的索引。for idx in "${!host[@]}"; do printf 'Host number %d is %s\n' "$idx" "${host[idx]}"done

"${!host[@]}" 扩展为host数组的索引,每个索引作为一个单独的单词。

索引数组是稀疏的,元素可以按任意顺序插入和删除。

# Bash/ksh# 简单的赋值语法。arr[0]=0arr[2]=2arr[1]=1arr[42]='what was the question?'# 取消设置 "arr" 的第二个元素。unset -v 'arr[2]'# 将值连接为由空格分隔的单个参数,并输出结果。echo "${arr[*]}"# 输出:"0 1 what was the question?"

在编写代码时,最好以一种可以处理稀疏数组的方式编写,即使您认为永远不会出现任何"空洞"。只有在确信可以将数组视为"列表",并且复杂性的节省足够大时,才将其视为"列表"。

2. 将值加载到数组中

逐个分配元素很简单且具有可移植性:

# Bash/ksharr[0]=0arr[42]='the answer'

也可以一次将多个值分配给数组,但是语法在不同的shell中有所不同。Bash仅支持arrName=(args...)的语法。ksh88仅支持set -A arrName -- args...的语法。ksh93、mksh和zsh都支持这两种语法。如果仔细观察,你会发现所有这些shell之间的这两种方法都存在一些细微的差异。

# Bash, ksh93, mksh, zsharray=(zero one two three four)# ksh88/93, mksh, zshset -A array -- zero one two three four

在这种方式初始化时,除非指定了不同的索引,否则第一个索引将为0。

通过复合赋值,括号之间的空格将以与命令参数相同的方式进行求值,包括路径名扩展和单词拆分。可以使用任何类型的扩展或替换。在其中,所有通常的引用规则都适用。

# Bash/ksh93oggs=(*.ogg)

使用ksh88风格的赋值时,参数只是命令的普通参数。

# Kornset -A oggs -- *.ogg# Bash(花括号扩展要求3.0或更高版本)homeDirs=(~{,root}) # 花括号扩展在ksh中的顺序不同,所以这仅适用于bash。letters=({a..z}) # 并非所有支持序列扩展的shell都能使用字母。# Kornset -A args -- "$@"2.1. 从文件或流中加载行

在bash 4中,mapfile命令(也称为readarray)可以实现这一目标:

# Bash 4mapfile -t lines <myfile# 或者mapfile -t lines < <(some command)

mapfile命令通过将空行插入为空数组元素来处理空行,并且(使用-t选项)如果输入流缺少最后一个换行符,则会自动添加缺失的换行符。这些在以其他方式读取数据时可能会引发问题(请参阅下一节)。在bash 4.0到4.3中,mapfile确实有一个严重的缺点:它只能处理换行符作为行终止符。Bash 4.4添加了-d选项以提供不同的行分隔符。

当mapfile不可用时,我们必须非常努力地尝试复制它。有很多方法可以几乎正确地实现它,但其中许多方法在细微的方式上失败。

以下示例将在较旧的shell中复制mapfile的大部分基本功能。如果您使用的是bash 4,则可以跳过所有这些替代示例。

切换行号显示

# 替代方案:Bash 3.1,Ksh93,mkshunset -v lineswhile IFS= read -r; do lines+=("$REPLY")done <file[[ $REPLY ]] && lines+=("$REPLY")

当与括号一起使用时,+=运算符将元素附加到数组中当前最高索引加1的位置。

切换行号显示

# 替代方案:ksh88# Ksh88不支持前/后增量/减量。mksh和其他一些支持。i=0unset -v lineswhile IFS= read -r; do lines[i+=1,$i]=$REPLY # 模拟 lines[i++]=$REPLYdone <file[[ $REPLY ]] && lines[i]=$REPLY

方括号创建了一个数学环境。表达式的结果用于赋值的索引。

2.1.1. 处理文件末尾的换行符(或其缺失)

当read读取文件的最后一行时,如果文件包含尾部换行符,read会返回false。这会带来一个问题:如果文件包含尾部换行符,则当读取/赋值最后一行时,read将返回false;否则,当读取/赋值数据的最后一行时,read将返回false。如果没有对这些情况进行特殊处理,无论使用什么逻辑,最终都会导致结果数组中要么多出一个空元素,要么缺少最后一个元素。

明确一点 - 文本文件应该以换行符作为文件的最后一个字符。大多数文本编辑器会在文件末尾添加换行符,同时Here documents和Here strings也会添加换行符。通常情况下,只有在从管道或进程替代输出读取时,或者从使用有问题或配置错误的工具创建的“损坏”文本文件中读取时,才会出现此问题。让我们看一些示例。

这种方法使用循环逐个读取元素。

# 无法正确工作!unset -v arr iwhile IFS= read -r 'arr[i++]'; do :done < <(printf '%s\n' {a..d})

不幸的是,如果文件或输入流包含尾部换行符,那么在最后一行包含文本的行之后,read -r arr[i++]会额外执行一次,从而在数组末尾添加一个空元素,然后返回false。

# 仍然无法正确工作!unset -v arr iwhile read -r; do arr[i++]=$REPLYdone < <(printf %s {a..c}$'\n' d)

方括号创建了一个数学上下文。在方括号内,i++的工作方式与C程序员的预期相同(除了ksh88之外)。

这种方法在相反的情况下失败 - 它可以正确处理空行和以换行符结尾的输入,但无法记录输入的最后一行。如果文件或流缺少最后一个换行符,则需要特殊处理这种情况:

# 替代方法:Bash,ksh93,mkshunset -v arr iwhile IFS= read -r; do arr[i++]=$REPLYdone <file[[ $REPLY ]] && arr[i++]=$REPLY # 如果有未终止的数据行,则追加该行。

这非常接近我们之前给出的“最终解决方案” - 同时处理文件内的空行和未终止的最后一行。使用空的IFS可以防止read从行的开头和结尾去除可能的空格,以便保留它们。

另一种解决方法是在循环之后移除空元素:

# 替代方法:Bashunset -v arr iwhile IFS= read -r 'arr[i++]'; do :done <file# 移除末尾的空元素(如果有的话)。[[ ${arr[i-1]} ]] || unset -v 'arr[--i]'

无论您更喜欢读取过多然后删除一个,还是读取过少然后添加一个,这都是个人选择。

注意:有必要引用传递给read的'arr[i++]',以防方括号被解释为globs。对于其他接受带下标的变量名的非关键字内建命令(例如let和unset),也是如此。

未完待续,请继续阅读《如何使用数组变量(二)?》……

如果您觉得文章内容对你有一点帮助可以关注我,我在头条平台会持续分享更多实用的shell技巧和最佳实践,如果想系统的快速学习shell的各种高阶用法和生产环境避坑指南可以看看《shell脚本编程最佳实践》专栏,专栏里有更多的实用小技巧和脚本代码分享。

0 阅读:0

超级欧派课程

简介:感谢大家的关注