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"
Ответы
Ответ 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[@]}")
Уже есть несколько элементов, о которых стоит упомянуть:
"${!ARR[@]}"
оценивает индексы массива (в отличие от элементов).
- Форма
"${!ARR[@]}"
является обязательной. Вы не должны пропускать кавычки или изменять @
на *
. Или же выражение будет разбито на ассоциативные массивы, где ключи содержат пробелы (например).
- Партия после
do
может быть любой, какой вы захотите. Идея состоит лишь в том, что вы должны сделать unset
, как показано для элементов, которые вы не хотите иметь в массиве.
- Рекомендуется или даже необходимо использовать
-v
и использовать кавычки с unset
, иначе могут случиться плохие вещи.
- Если деталь после
do
соответствует предложенной выше, вы можете использовать либо &&
, либо ||
, чтобы отфильтровать элементы, которые либо проходят, либо не соответствуют условию.
- Вторая строка, переназначение
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.
В частности, это будет обрабатывать пространства, встроенные в элементы массива.