Bash:如何基于模式从数组中删除元素

31

假设我有一个Bash数组(例如所有参数的数组),我想删除所有与某个模式相匹配的参数,或者将所有剩余元素复制到一个新数组中。或者,反过来,保留与某个模式匹配的元素。

以下是一个示例:

x=(preffoo bar foo prefbaz baz prefbar)

我想要删除以pref开头的所有内容,以获得:

y=(bar foo baz)

如果我想对由空格分隔的单词列表执行相同的操作,该怎么办?

x="preffoo bar foo prefbaz baz prefbar"

然后再删除所有以pref开头的内容,以获得

y="bar foo baz"
6个回答

31

如果考虑到数组中可能包含空格(更不用说“更奇怪”的字符了),筛选一个数组就会变得棘手。特别是迄今为止给出的答案(涉及各种形式的${x[@]//pref*/})将无法处理这样的数组。

我已经调查了这个问题,找到了一个解决方案,但它不是一个漂亮的一行代码。但至少它是可行的。

为了举例说明,让我们假设ARR表示我们想要筛选的数组。 我们将从核心表达式开始:

for index in "${!ARR[@]}" ; do [[ …condition… ]] && unset -v 'ARR[$index]' ; done
ARR=("${ARR[@]}")

以下是值得一提的几个要素:

  1. "${!ARR[@]}"将求出数组的下标(而不是元素)。
  2. 表达式形式"${!ARR[@]}"是必须的。你不能省略引号或改变@*。否则,在关联数组中,键包含空格时,表达式将会出错。
  3. do后面的部分可以是任何你想要的内容。唯一需要做的就是像所示一样对那些你不想在数组中保留的元素执行unset操作。
  4. 建议使用 -v 和引号与 unset 配合使用,否则可能会发生错误。
  5. 如果 do 后面的部分如上所述,则可以使用 &&|| 过滤掉通过或未通过条件的元素。
  6. 第二行重新给ARR赋值,只有在非关联数组时才需要,在关联数组中会出错。(我没有很快想出一个通用的表达式来处理两者,因为我不需要这种表达式…)对于普通数组,如果您想要连续的索引,则需要这样做。因为unset一个数组元素不会修改(减少一个)更高索引的元素-它只是在索引中留下了一个空洞。现在,如果您只迭代整个数组(或将其作为整体扩展),则没有问题。但对于其他情况,您需要重新分配索引。同时注意,如果您之前的索引中有任何空洞,它也会被删除。因此,如果您需要保留现有的空洞,则需要在unset和最终重新赋值之外进行更多的逻辑处理。

现在,就条件而言,如果您可以使用它,那么 [[ ]] 表达式是一种容易的方式。(请参见这里)。特别是它支持使用 扩展正则表达式 进行匹配。(请参见这里)。此外,请小心使用grep或任何其他基于行的工具进行操作,如果您希望数组元素不仅包含空格,还可能包含新行。(虽然一个非常恶心的文件名可能会有换行符我想…)


关于问题本身,[[ ]] 表达式需要如下:

[[ ${ARR[$index]} =~ ^pref ]]

(如上所述,使用&& unset)


现在让我们来看看如何处理那些困难的情况。首先,我们构建数组:

declare -a ARR='([0]="preffoo" [1]="bar" [2]="foo" [3]="prefbaz" [4]="baz" [5]="prefbar" [6]="pref with spaces")'
ARR+=($'pref\nwith\nnew line')
ARR+=($'\npref with new line before')

运行declare -p ARR,我们可以看到所有复杂情况:

declare -a ARR='([0]="preffoo" [1]="bar" [2]="foo" [3]="prefbaz" [4]="baz" [5]="prefbar" [6]="pref with spaces" [7]="pref
with
new line" [8]="
pref with new line before")'

现在我们运行过滤表达式:

for index in "${!ARR[@]}" ; do [[ ${ARR[$index]} =~ ^pref ]] && unset -v 'ARR[$index]' ; done

另一个测试(declare -p ARR)结果如预期:

declare -a ARR='([1]="bar" [2]="foo" [4]="baz" [8]="
pref with new line before")'

注意所有以pref开头的元素都被删除了,但索引没有改变。还要注意,${ARRAY[8]}仍然存在,因为它是以换行符而不是pref开头。

现在进行最终重新赋值:

ARR=("${ARR[@]}")

并检查 (declare -p ARR):

declare -a ARR='([0]="bar" [1]="foo" [2]="baz" [3]="
pref with new line before")'

这正是预期的内容。


最后的注释。如果可以将其转换为灵活的一行代码,那会很好。但我认为在不定义函数或类似内容的情况下,现在无法使它更短或更简单。

至于函数本身,若能够接受数组、返回数组并且有易于配置的测试以排除或保留,那也是很好的。但我对 Bash 不太熟悉,暂时做不到。


谢谢!这个工作得很好...而且足够简单。 - davidhq
第一段代码片段包含 unset -v 'ARR[$index]'。单引号不会阻止 $index 的替换吗? - Roland Weber
@RolandWeber,我想在我写这篇文章的时候它确实有效。但是现在,它需要进行检查。我不够熟练,只能通过实验来判断。 - Adam Badura
1
@RolandWeber:不需要双引号也可以使用单引号。尝试这个命令吧:a=(0 1 "and two"); for i in ${!a[*]}; do echo "$i=${a[$i]}"; ((i=1)) && unset -v 'a[$i]'; done; echo; declare -p a; a=("${a[@]}"); declare -p a - mivk
1
夸奖你的出色回答,简直是纯金啊。 - starfry

13

将一个平面字符串转换为数组,然后使用数组方法是另一种去除字符串的方式:

x="preffoo bar foo prefbaz baz prefbar"
x=($x)
x=${x[@]//pref*}

将其与以数组开头和结尾进行比较:

x=(preffoo bar foo prefbaz baz prefbar)
x=(${x[@]//pref*})

我真的很喜欢这个东西,因为它真正减少了我以前为这种操作所编写的代码量。 - pn1 dude
2
使用数组并不是很好。例如,如果初始元素包含空格,则很难从中获取一个数组。例如,declare -a ARR=('element1' 'with space' 'with two spaces' 'element4'),然后执行VAR=(${ARR[@]//element*/})。你将得到的不是一个包含两个元素(with spacewith two spaces)的数组,而是一个包含五个元素(withspacewithtwospaces)的数组。 - Adam Badura

10

如果要剥离一个扁平的字符串(对于数组,Hulk已经给出了答案),你可以开启extglob shell选项,并运行以下展开式

$ shopt -s extglob
$ unset x
$ x="preffoo bar foo prefbaz baz prefbar"
$ echo ${x//pref*([^ ])?( )}
bar foo baz

在使用*(pattern-list)?(pattern-list)形式时需要启用extglob选项,这样可以使用正则表达式(虽然与大多数正则表达式的形式不同),而不仅仅是路径名扩展(*?[)。

Hulk给出的数组答案只适用于数组。如果它在扁平字符串上运行,那只是因为在测试时数组没有首先被清除。

例如:

$ x=(preffoo bar foo prefbaz baz prefbar)
$ echo ${x[@]//pref*/}
bar foo baz
$ x="preffoo bar foo prefbaz baz prefbar"
$ echo ${x[@]//pref*/}
bar foo baz
$ unset x
$ x="preffoo bar foo prefbaz baz prefbar"
$ echo ${x[@]//pref*/}

$

1
+1 感谢您澄清了 Hulk 的帖子并指出了这条其他路径。 - kynan

7
你可以这样做:
删除所有子字符串的出现。
# Not specifing a replacement defaults to 'delete' ...
echo ${x[@]//pref*/}      # one two three four ve ve
#               ^^          # Applied to all elements of the array.

编辑:

对于空格而言,情况有些类似。

x="preffoo bar foo prefbaz baz prefbar"
echo ${x[@]//pref*/}

输出:

bar foo baz

(该段文字为输出的内容)

有没有类似的东西可以处理由空格分隔的单词字符串? - kynan
似乎不太对,这会删除第一个出现的 pref 之后的所有内容。 - kynan
你在使用bash吗?我尝试了同样的操作,但输出为空。 - kynan
为什么在某些特定需求中需要Bash?不能使用/bin/sh吗? - Hulk
3
数组并不适用于这种情况。结果的echo看起来很好,但重点是(或可能是)将其作为数组输出。如果初始元素包含空格,则很难从中获取一个数组。例如,有一个声明的数组declare -a ARR=('element1' 'with space' 'with two spaces' 'element4'),然后执行VAR=(${ARR[@]//element*/})。你将得到的不是一个两个元素(“with space”和“with two spaces”)的数组,而是五个元素的数组(“with”,“space”,“with”,“two”,“spaces”)。 - Adam Badura
显示剩余2条评论

3

这里有一种使用grep的方法:

(IFS=$'\n' && echo "${MY_ARR[*]}") | grep '[^.]*.pattern/[^.]*.txt'

这里的关键是IFS=$'\n'会导致"${MY_ARR[*]}"扩展为以换行符分隔项目的形式,因此它可以通过grep进行管道处理。
特别是,这将处理嵌入到数组项中的空格。

2
我定义并使用了以下函数:

# Removes elements from an array based on a given regex pattern.
# Usage: filter_arr pattern array
# Usage: filter_arr pattern element1 element2 ...
filter_arr() {  
    arr=($@)
    arr=(${arr[@]:1})
    dirs=($(for i in ${arr[@]}
        do echo $i
    done | grep -v $1))
    echo ${dirs[@]}
}

示例用法:

$ arr=(chicken egg hen omelette)
$ filter_arr "n$" ${arr[@]}

输出:

鸡蛋煎饼

该函数的输出是一个字符串。要将其转换回数组:

$ arr2=(`filter_arr "n$" ${arr[@]}`)

如果数组元素包含空格,则不会保留它们,而是将其拆分为新的元素数组。您可以通过使用declare -a arr=('element1' 'with space' 'with two spaces' 'element4')并过滤element来查看它。结果将包含每个单词作为单独的元素,而不仅仅是with spacewith two spaces - Adam Badura

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接