Подтвердить что ты не робот

Bash: как удалить элементы из массива на основе шаблона

Скажем, у меня есть массив 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"
4b9b3361

Ответ 1

Чтобы удалить плоскую строку (Халк уже дал ответ для массивов), вы можете включить опцию оболочки extglob и запустить следующее расширение

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

Опция extglob необходима для форм *(pattern-list) и ?(pattern-list). Это позволяет использовать регулярные выражения (хотя в другой форме для большинства регулярных выражений) вместо просто расширения пути (*?[).

Ответ, который Халк дал для массивов, будет работать только на массивах. Если он работает с плоскими строками, то только потому, что при тестировании массив сначала не был отменен.

например.

$ 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*/}

$

Ответ 2

Фильтровать массив сложно, если учесть возможность элементов, содержащих пробелы (не говоря уже о "более странных" символах). В частности, ответы, данные до сих пор (относящиеся к различным формам ${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")'

это именно то, что ожидалось.


Для заключительных заметок. Было бы хорошо, если бы это могло быть изменено на гибкую однострочную. Но я не думаю, что есть способ сделать его короче и проще, как сейчас, без определения функций или тому подобного.

Что касается функции, было бы неплохо, чтобы она принимала массив, возвращала массив и имела простую настройку теста для исключения или сохранения. Но я не достаточно хорош с Башом, чтобы сделать это сейчас.

Ответ 3

Другой способ разбить плоскую строку - преобразовать ее в массив, а затем использовать метод массива:

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

Контрастируйте это с началом и концом массива:

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

Ответ 4

Вы можете сделать это:

Удалить все вхождения подстроки.

# Not specifing a replacement defaults to 'delete' ...
echo ${x[@]//pref*/}      # one two three four ve ve
#               ^^          # Applied to all elements of the array.

Edit:

Для белых пространств это то же самое

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

Выход:

bar foo baz

Ответ 5

Я определил и использовал следующую функцию:

# 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=([email protected])
    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[@]}`)

Ответ 6

Вот способ использования grep:

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

Суть в том, что IFS=$'\n' заставляет "${MY_ARR[*]}" расширяться с помощью новых строк, разделяющих элементы, поэтому его можно пропустить через grep.

В частности, это будет обрабатывать пространства, встроенные в элементы массива.