Удалить элемент из массива Bash

116

Мне нужно удалить элемент из массива в оболочке bash. Обычно я просто делал:

array=("${(@)array:#<element to remove>}")

К сожалению, элемент, который я хочу удалить, является переменной, поэтому я не могу использовать предыдущую команду. Вот пример:

array+=(pluto)
array+=(pippo)
delete=(pluto)
array( ${array[@]/$delete} ) -> but clearly doesn't work because of {}

Любая идея?

Alex
источник
Какая оболочка? Ваш пример выглядит так zsh.
Чепнер
array=( ${array[@]/$delete} )работает должным образом в Bash. Вы просто пропустили =?
Кен Шарп
1
@Ken, это не совсем то, что нужно - он удалит все совпадения из каждой строки и оставит пустые строки в массиве, где он соответствует всей строке.
Тоби Спейт

Ответы:

165

Следующее работает так, как вы хотите, bashи zsh:

$ array=(pluto pippo)
$ delete=pluto
$ echo ${array[@]/$delete}
pippo
$ array=( "${array[@]/$delete}" ) #Quotes when working with strings

Если нужно удалить более одного элемента:

...
$ delete=(pluto pippo)
for del in ${delete[@]}
do
   array=("${array[@]/$del}") #Quotes when working with strings
done

Предостережение

Этот метод фактически удаляет префиксы, совпадающие $deleteс элементами, а не обязательно целые элементы.

Обновить

Чтобы действительно удалить точный элемент, вам нужно пройтись по массиву, сравнить цель с каждым элементом и использовать unsetдля удаления точного совпадения.

array=(pluto pippo bob)
delete=(pippo)
for target in "${delete[@]}"; do
  for i in "${!array[@]}"; do
    if [[ ${array[i]} = $target ]]; then
      unset 'array[i]'
    fi
  done
done

Обратите внимание: если вы сделаете это, и один или несколько элементов будут удалены, индексы больше не будут непрерывной последовательностью целых чисел.

$ declare -p array
declare -a array=([0]="pluto" [2]="bob")

Дело в том, что массивы не были предназначены для использования в качестве изменяемых структур данных. Они в основном используются для хранения списков элементов в одной переменной без необходимости тратить символ в качестве разделителя (например, для хранения списка строк, которые могут содержать пробелы).

Если пробелы являются проблемой, вам необходимо перестроить массив, чтобы заполнить пробелы:

for i in "${!array[@]}"; do
    new_array+=( "${array[i]}" )
done
array=("${new_array[@]}")
unset new_array
chepner
источник
43
просто знайте, что: $ array=(sun sunflower) $ delete=(sun) $ echo ${array[@]/$delete}результатыflower
bernstein
12
Обратите внимание, что на самом деле это подстановка, поэтому, если массив выглядит примерно так, (pluto1 pluto2 pippo)вы получите (1 2 pippo).
haridsv
5
Просто будьте осторожны, используя это в цикле for, потому что в итоге вы получите пустой элемент на месте удаленного. Для здравомыслия вы могли бы сделать что-то вродеfor element in "${array[@]}" do if [[ $element ]]; then echo ${element} fi done
Joel B
2
Итак, как удалить только совпадающие элементы?
UmaN
4
Примечание: это может привести к отсутствию соответствующего значения, но элемент все равно останется в массиве.
phil294
29

Вы можете создать новый массив без нежелательного элемента, а затем вернуть его старому массиву. Это работает в bash:

array=(pluto pippo)
new_array=()
for value in "${array[@]}"
do
    [[ $value != pluto ]] && new_array+=($value)
done
array=("${new_array[@]}")
unset new_array

Это дает:

echo "${array[@]}"
pippo
Стив Кехлет
источник
14

Это самый прямой способ сбросить значение, если вы знаете его положение.

$ array=(one two three)
$ echo ${#array[@]}
3
$ unset 'array[1]'
$ echo ${array[@]}
one three
$ echo ${#array[@]}
2
знаковый
источник
3
Попробуйте echo ${array[1]}, вы получите нулевую строку. А чтобы получить threeнужно сделать echo ${array[2]}. Так что unsetэто неправильный механизм для удаления элемента в массиве bash.
rashok 03
@rashok, нет, ${array[1]+x}это пустая строка, поэтому array[1]не установлено. unsetне меняет индексы остальных элементов. Аргумент для неустановленного значения указывать не нужно. Способ уничтожения элемента массива описан в руководстве Bash .
Ярно
@rashok Не понимаю, почему бы и нет. Вы не можете предположить, что он ${array[1]}существует только потому, что его размер равен 2. Если вам нужны индексы, проверьте ${!array[@]}.
Дэниел С. Собрал,
4

Вот однострочное решение с mapfile:

$ mapfile -d $'\0' -t arr < <(printf '%s\0' "${arr[@]}" | grep -Pzv "<regexp>")

Пример:

$ arr=("Adam" "Bob" "Claire"$'\n'"Smith" "David" "Eve" "Fred")

$ echo "Size: ${#arr[*]} Contents: ${arr[*]}"

Size: 6 Contents: Adam Bob Claire
Smith David Eve Fred

$ mapfile -d $'\0' -t arr < <(printf '%s\0' "${arr[@]}" | grep -Pzv "^Claire\nSmith$")

$ echo "Size: ${#arr[*]} Contents: ${arr[*]}"

Size: 5 Contents: Adam Bob David Eve Fred

Этот метод обеспечивает большую гибкость за счет изменения / обмена командой grep и не оставляет пустых строк в массиве.

Никлас Холм
источник
1
Пожалуйста , используйте printf '%s\n' "${array[@]}"вместо этого уродливой IFS/ echoвещи.
gniourf_gniourf
Обратите внимание, что это не работает с полями, содержащими символы новой строки.
gniourf_gniourf
@Socowi Вы ошибаетесь, по крайней мере, на bash 4.4.19. -d $'\0'отлично работает, а просто -dбез аргумента - нет.
Никлас Холм
Ах да, перепутала. Сожалею. Я имел в виду: -d $'\0'то же самое -d $'\0 something'или просто -d ''.
Socowi
Но не помешает использовать $'\0'для ясности
Никлас Холм
4

Этот ответ относится к случаю удаления нескольких значений из больших массивов, когда важна производительность.

Самые популярные решения: (1) замена шаблона в массиве или (2) итерация по элементам массива. Первый быстрый, но может работать только с элементами, имеющими отдельный префикс, второй - O (n * k), n = размер массива, k = удаляемые элементы. Ассоциативный массив - это относительно новая функция, которая, возможно, не использовалась при первоначальной публикации вопроса.

Для случая точного совпадения с большими n и k возможно улучшить производительность с O (n k) до O (n + k log (k)). На практике O (n) при условии, что k намного меньше n. Большая часть ускорения основана на использовании ассоциативного массива для идентификации элементов, которые необходимо удалить.

Производительность (размер n-массива, k-значения для удаления). Измерение производительности в секундах пользовательского времени

   N     K     New(seconds) Current(seconds)  Speedup
 1000   10     0.005        0.033             6X
10000   10     0.070        0.348             5X
10000   20     0.070        0.656             9X
10000    1     0.043        0.050             -7%

Как и ожидалось, currentрешение линейно по отношению к N * K, а fastрешение практически линейно по отношению к K с гораздо более низкой константой. fastРаствор немного медленнее по сравнению с currentрешением , когда к = 1, из - за дополнительной настройки.

«Быстрое» решение: массив = список ввода, delete = список значений для удаления.

        declare -A delk
        for del in "${delete[@]}" ; do delk[$del]=1 ; done
                # Tag items to remove, based on
        for k in "${!array[@]}" ; do
                [ "${delk[${array[$k]}]-}" ] && unset 'array[k]'
        done
                # Compaction
        array=("${array[@]}")

Сравнение с currentрешением на основе ответа, получившего наибольшее количество голосов.

    for target in "${delete[@]}"; do
        for i in "${!array[@]}"; do
            if [[ ${array[i]} = $target ]]; then
                unset 'array[i]'
            fi
        done
    done
    array=("${array[@]}")
тире
источник
3

Вот небольшая (вероятно, очень специфичная для bash) функция, включающая косвенное обращение к переменной bash и unset; это общее решение, которое не включает в себя замену текста или отбрасывание пустых элементов и не имеет проблем с цитированием / пробелами и т. д.

delete_ary_elmt() {
  local word=$1      # the element to search for & delete
  local aryref="$2[@]" # a necessary step since '${!$2[@]}' is a syntax error
  local arycopy=("${!aryref}") # create a copy of the input array
  local status=1
  for (( i = ${#arycopy[@]} - 1; i >= 0; i-- )); do # iterate over indices backwards
    elmt=${arycopy[$i]}
    [[ $elmt == $word ]] && unset "$2[$i]" && status=0 # unset matching elmts in orig. ary
  done
  return $status # return 0 if something was deleted; 1 if not
}

array=(a 0 0 b 0 0 0 c 0 d e 0 0 0)
delete_ary_elmt 0 array
for e in "${array[@]}"; do
  echo "$e"
done

# prints "a" "b" "c" "d" in lines

Используйте его как delete_ary_elmt ELEMENT ARRAYNAMEбез $сигилы. Включите == $wordfor == $word*для совпадений префиксов; использовать ${elmt,,} == ${word,,}для совпадений без учета регистра; и т.д., все, что [[поддерживает bash .

Он работает, определяя индексы входного массива и повторяя их в обратном порядке (поэтому удаление элементов не нарушает порядок итераций). Чтобы получить индексы, вам нужно получить доступ к входному массиву по имени, что можно сделать с помощью косвенного обращения к переменной bash x=1; varname=x; echo ${!varname} # prints "1".

Вы не можете получить доступ к массивам по имени, например aryname=a; echo "${$aryname[@]}, это дает вам ошибку. Вы не можете этого сделать aryname=a; echo "${!aryname[@]}", это дает вам индексы переменной aryname(хотя это не массив). Что ДЕЙСТВИТЕЛЬНО работает, так это то aryref="a[@]"; echo "${!aryref}", что будет печатать элементы массива a, сохраняя цитирование слов оболочки и пробелы точно так же, как echo "${a[@]}". Но это работает только для печати элементов массива, а не для печати его длиной или индексов ( aryref="!a[@]"или aryref="#a[@]"или "${!!aryref}"или "${#!aryref}", все они терпят неудачу).

Поэтому я копирую исходный массив по его имени через косвенную адресацию bash и получаю индексы из копии. Чтобы перебирать индексы в обратном порядке, я использую цикл for в стиле C. Я также мог бы сделать это, обратившись к индексам через ${!arycopy[@]}и изменив их с помощью tac, которая catменяет порядок строк ввода.

Функциональное решение без косвенного evalобращения к переменным, вероятно, должно быть задействовано , что может быть безопасным или небезопасным для использования в этой ситуации (я не могу сказать).

SVP
источник
Это почти прекрасно работает, однако не повторно объявляет исходный массив, переданный в функцию, поэтому, хотя в этом исходном массиве отсутствуют значения, у него также есть испорченные индексы. Это означает, что следующий вызов, который вы сделаете для delete_ary_elmt в том же массиве, не будет работать (или удалит неправильные вещи). Например, после того, что вы вставили, попробуйте запустить, delete_ary_elmt "d" arrayа затем повторно распечатать массив. Вы увидите, что удален не тот элемент. Удаление последнего элемента также никогда не сработает.
Скотт
2

Чтобы расширить приведенные выше ответы, для удаления нескольких элементов из массива без частичного сопоставления можно использовать следующее:

ARRAY=(one two onetwo three four threefour "one six")
TO_REMOVE=(one four)

TEMP_ARRAY=()
for pkg in "${ARRAY[@]}"; do
    for remove in "${TO_REMOVE[@]}"; do
        KEEP=true
        if [[ ${pkg} == ${remove} ]]; then
            KEEP=false
            break
        fi
    done
    if ${KEEP}; then
        TEMP_ARRAY+=(${pkg})
    fi
done
ARRAY=("${TEMP_ARRAY[@]}")
unset TEMP_ARRAY

В результате получится массив, содержащий: (два на два три три четыре "один шесть")

Дилан
источник
2

Если кто-то окажется в положении, когда ему нужно запомнить значения set -e или set -x и иметь возможность их восстановить, ознакомьтесь с этой сутью, которая использует первое решение для удаления массива для управления своим собственным стеком:

https://gist.github.com/kigster/94799325e39d2a227ef89676eed44cc6

Константин Гредескул
источник
1

Только частичный ответ

Чтобы удалить первый элемент в массиве

unset 'array[0]'

Чтобы удалить последний элемент в массиве

unset 'array[-1]'
рассмотрение
источник
@gniourf_gniourf нет необходимости использовать кавычки в качестве аргумента unset.
Ярно
2
@jarno: эти кавычки ДОЛЖНЫ использоваться: если у вас есть файл с именем array0в текущем каталоге, то, поскольку array[0]это glob, он сначала будет расширен array0до команды unset.
gniourf_gniourf
@gniourf_gniourf вы правы. Это должно быть исправлено в Справочном руководстве Bash, в котором в настоящее время говорится, что "unset name [subscript] уничтожает элемент массива с индексом subscript".
Ярно
1

С помощью unset

Чтобы удалить элемент по определенному индексу, мы можем использовать, unsetа затем скопировать в другой массив. Только просто unsetв этом случае не требуется. Поскольку unsetне удаляет элемент, он просто устанавливает нулевую строку для определенного индекса в массиве.

declare -a arr=('aa' 'bb' 'cc' 'dd' 'ee')
unset 'arr[1]'
declare -a arr2=()
i=0
for element in "${arr[@]}"
do
    arr2[$i]=$element
    ((++i))
done
echo "${arr[@]}"
echo "1st val is ${arr[1]}, 2nd val is ${arr[2]}"
echo "${arr2[@]}"
echo "1st val is ${arr2[1]}, 2nd val is ${arr2[2]}"

Выход

aa cc dd ee
1st val is , 2nd val is cc
aa cc dd ee
1st val is cc, 2nd val is dd

С помощью :<idx>

Мы можем удалить некоторый набор элементов, используя :<idx>также. Например, если мы хотим удалить 1-й элемент, мы можем использовать, :1как указано ниже.

declare -a arr=('aa' 'bb' 'cc' 'dd' 'ee')
arr2=("${arr[@]:1}")
echo "${arr2[@]}"
echo "1st val is ${arr2[1]}, 2nd val is ${arr2[2]}"

Выход

bb cc dd ee
1st val is cc, 2nd val is dd
rashok
источник
0

Сценарий оболочки POSIX не имеет массивов.

Так что, скорее всего, вы используете определенный диалект, например bash, korn shells или zsh.

Поэтому на ваш вопрос пока нет ответа.

Может быть, это сработает для вас:

unset array[$delete]
ВЫЙТИ - Anony-Mousse
источник
2
Привет, я использую bash shell atm. И «$ delete» - это не позиция элемента, а сама строка. Так что я не думаю, что "unset" сработает
Alex
0

На самом деле, я только что заметил, что синтаксис оболочки в некоторой степени имеет встроенное поведение, которое позволяет легко реконструировать массив, когда, как указано в вопросе, элемент должен быть удален.

# let's set up an array of items to consume:
x=()
for (( i=0; i<10; i++ )); do
    x+=("$i")
done

# here, we consume that array:
while (( ${#x[@]} )); do
    i=$(( $RANDOM % ${#x[@]} ))
    echo "${x[i]} / ${x[@]}"
    x=("${x[@]:0:i}" "${x[@]:i+1}")
done

Обратите внимание, как мы построили массив, используя x+=()синтаксис bash ?

Фактически вы могли бы добавить с ним более одного элемента, содержимое целого другого массива одновременно.

mar77i
источник
0

http://wiki.bash-hackers.org/syntax/pe#substring_removal

$ {PARAMETER # PATTERN} # удалить с начала

$ {PARAMETER ## PATTERN} # удалить с начала, жадное совпадение

$ {PARAMETER% PATTERN} # удалить с конца

$ {PARAMETER %% PATTERN} # удалить с конца, жадное совпадение

Чтобы выполнить полное удаление элемента, вы должны выполнить команду unset с оператором if. Если вы не заботитесь об удалении префиксов из других переменных или о поддержке пробелов в массиве, вы можете просто отбросить кавычки и забыть о циклах for.

См. В примере ниже несколько различных способов очистки массива.

options=("foo" "bar" "foo" "foobar" "foo bar" "bars" "bar")

# remove bar from the start of each element
options=("${options[@]/#"bar"}")
# options=("foo" "" "foo" "foobar" "foo bar" "s" "")

# remove the complete string "foo" in a for loop
count=${#options[@]}
for ((i = 0; i < count; i++)); do
   if [ "${options[i]}" = "foo" ] ; then
      unset 'options[i]'
   fi
done
# options=(  ""   "foobar" "foo bar" "s" "")

# remove empty options
# note the count variable can't be recalculated easily on a sparse array
for ((i = 0; i < count; i++)); do
   # echo "Element $i: '${options[i]}'"
   if [ -z "${options[i]}" ] ; then
      unset 'options[i]'
   fi
done
# options=("foobar" "foo bar" "s")

# list them with select
echo "Choose an option:"
PS3='Option? '
select i in "${options[@]}" Quit
 do
    case $i in 
       Quit) break ;;
       *) echo "You selected \"$i\"" ;;
    esac
 done

Вывод

Choose an option:
1) foobar
2) foo bar
3) s
4) Quit
Option? 

Надеюсь, это поможет.

phyatt
источник
0

В ZSH это очень просто (обратите внимание, что здесь используется более совместимый с bash синтаксис, чем необходимо, где это возможно, для простоты понимания):

# I always include an edge case to make sure each element
# is not being word split.
start=(one two three 'four 4' five)
work=(${(@)start})

idx=2
val=${work[idx]}

# How to remove a single element easily.
# Also works for associative arrays (at least in zsh)
work[$idx]=()

echo "Array size went down by one: "
[[ $#work -eq $(($#start - 1)) ]] && echo "OK"

echo "Array item "$val" is now gone: "
[[ -z ${work[(r)$val]} ]] && echo OK

echo "Array contents are as expected: "
wanted=("${start[@]:0:1}" "${start[@]:2}")
[[ "${(j.:.)wanted[@]}" == "${(j.:.)work[@]}" ]] && echo "OK"

echo "-- array contents: start --"
print -l -r -- "-- $#start elements" ${(@)start}
echo "-- array contents: work --"
print -l -r -- "-- $#work elements" "${work[@]}"

Полученные результаты:

Array size went down by one:
OK
Array item two is now gone:
OK
Array contents are as expected:
OK
-- array contents: start --
-- 5 elements
one
two
three
four 4
five
-- array contents: work --
-- 4 elements
one
three
four 4
five
Trevorj
источник
Извините, только что попробовал. Не сработало в zsh для ассоциативного массива
Falk
Работает нормально, я только что проверил (снова). У вас что-то не работает? Пожалуйста, как можно подробнее объясните, что не сработало. Какую версию ZSH вы используете?
trevorj 02
0

Также существует этот синтаксис, например, если вы хотите удалить второй элемент:

array=("${array[@]:0:1}" "${array[@]:2}")

что на самом деле представляет собой объединение двух вкладок. Первый от индекса 0 до индекса 1 (исключительный) и второй от индекса 2 до конца.

OphyTe
источник
-1

Что я делаю:

array="$(echo $array | tr ' ' '\n' | sed "/itemtodelete/d")"

БАМ, этот предмет удален.

гарфилд
источник
1
Это ломается для array=('first item' 'second item').
Benjamin W.
-1

Это быстрое и грязное решение, которое будет работать в простых случаях, но сломается, если (а) $deleteв каких-либо элементах есть специальные символы регулярных выражений или (б) в любых элементах вообще есть пробелы. Начиная с:

array+=(pluto)
array+=(pippo)
delete=(pluto)

Удалите все записи, точно соответствующие $delete:

array=(`echo $array | fmt -1 | grep -v "^${delete}$" | fmt -999999`)

в результате echo $array-> pippo, и убедившись, что это массив: echo $array[1]-> pippo

fmtнемного неясен: fmt -1переносит на первый столбец (чтобы поместить каждый элемент в отдельную строку. Вот где возникает проблема с элементами в fmt -999999пробелах ). разворачивает его обратно на одну строку, возвращая пробелы между элементами. Есть и другие способы сделать это, например xargs.

Приложение: если вы хотите удалить только первое совпадение, используйте sed, как описано здесь :

array=(`echo $array | fmt -1 | sed "0,/^${delete}$/{//d;}" | fmt -999999`)
Джошуа Голдберг
источник
-1

Как насчет чего-то вроде:

array=(one two three)
array_t=" ${array[@]} "
delete=one
array=(${array_t// $delete / })
unset array_t
user8223227
источник
-1

Для того, чтобы избежать конфликтов с индексом массива с помощью unset- см https://stackoverflow.com/a/49626928/3223785 и https://stackoverflow.com/a/47798640/3223785 для получения дополнительной информации - переназначить массив себе: ARRAY_VAR=(${ARRAY_VAR[@]}).

#!/bin/bash

ARRAY_VAR=(0 1 2 3 4 5 6 7 8 9)
unset ARRAY_VAR[5]
unset ARRAY_VAR[4]
ARRAY_VAR=(${ARRAY_VAR[@]})
echo ${ARRAY_VAR[@]}
A_LENGTH=${#ARRAY_VAR[*]}
for (( i=0; i<=$(( $A_LENGTH -1 )); i++ )) ; do
    echo ""
    echo "INDEX - $i"
    echo "VALUE - ${ARRAY_VAR[$i]}"
done

exit 0

[Ссылка: https://tecadmin.net/working-with-array-bash-script/ ]

Эдуардо Лучио
источник
-2
#/bin/bash

echo "# define array with six elements"
arr=(zero one two three 'four 4' five)

echo "# unset by index: 0"
unset -v 'arr[0]'
for i in ${!arr[*]}; do echo "arr[$i]=${arr[$i]}"; done

arr_delete_by_content() { # value to delete
        for i in ${!arr[*]}; do
                [ "${arr[$i]}" = "$1" ] && unset -v 'arr[$i]'
        done
        }

echo "# unset in global variable where value: three"
arr_delete_by_content three
for i in ${!arr[*]}; do echo "arr[$i]=${arr[$i]}"; done

echo "# rearrange indices"
arr=( "${arr[@]}" )
for i in ${!arr[*]}; do echo "arr[$i]=${arr[$i]}"; done

delete_value() { # value arrayelements..., returns array decl.
        local e val=$1; new=(); shift
        for e in "${@}"; do [ "$val" != "$e" ] && new+=("$e"); done
        declare -p new|sed 's,^[^=]*=,,'
        }

echo "# new array without value: two"
declare -a arr="$(delete_value two "${arr[@]}")"
for i in ${!arr[*]}; do echo "arr[$i]=${arr[$i]}"; done

delete_values() { # arraydecl values..., returns array decl. (keeps indices)
        declare -a arr="$1"; local i v; shift
        for v in "${@}"; do 
                for i in ${!arr[*]}; do
                        [ "$v" = "${arr[$i]}" ] && unset -v 'arr[$i]'
                done
        done
        declare -p arr|sed 's,^[^=]*=,,'
        }
echo "# new array without values: one five (keep indices)"
declare -a arr="$(delete_values "$(declare -p arr|sed 's,^[^=]*=,,')" one five)"
for i in ${!arr[*]}; do echo "arr[$i]=${arr[$i]}"; done

# new array without multiple values and rearranged indices is left to the reader
Гомбок Артур
источник
1
Не могли бы вы добавить комментарии или описание, чтобы рассказать нам о своем ответе?
Майкл