it-roy-ru.com

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

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

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

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

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

Любая идея?

67
Alex

Следующее работает так, как вы хотели бы в 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]} = "${delete[0]}" ]]; 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
112
chepner

Вы можете создать новый массив без нежелательного элемента, а затем назначить его обратно старому массиву. Это работает в 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
18
Steve Kehlet

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

$ array=(one two three)
$ echo ${#array[@]}
3
$ unset 'array[1]'
$ echo ${array[@]}
one three
$ echo ${#array[@]}
2
4
signull

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

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
Dylan

Вот решение с одной строкой с 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, и не оставляет пустых строк в массиве.

2
Niklas Holm

Скрипт POSIX Shell не имеет массивов.

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

Таким образом, ваш вопрос на данный момент не может быть дан ответ.

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

unset array[$delete]
1
Anony-Mousse

Вот маленькая (вероятно, очень специфичная для 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 без каких-либо символов $. Переключите == $Word для == $Word* для совпадений префиксов; используйте ${elmt,,} == ${Word,,} для совпадений без учета регистра; и т.д., что бы ни поддерживал bash [[.

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

Вы не можете получить доступ к массивам по имени, как aryname=a; echo "${$aryname[@]}, это выдает ошибку. Вы не можете делать aryname=a; echo "${!aryname[@]}", это дает вам индексы переменной aryname (хотя это не массив). Что работает, так это aryref="a[@]"; echo "${!aryref}", который будет печатать элементы массива a, сохраняя кавычки Shell-Word и пробелы точно так же, как echo "${a[@]}". Но это работает только для печати элементов массива, а не для печати его длины или индексов (aryref="!a[@]" или aryref="#a[@]" или "${!!aryref}" или "${#!aryref}", они все терпят неудачу).

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

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

1
S.V.P.

Использование 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
0
rashok

В 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
0
trevorj

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

Удалить первый элемент в массиве

unset 'array[0]'

Удалить последний элемент в массиве

unset 'array[-1]'
0
consideRatio

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? 

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

0
phyatt

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

# 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

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

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

0
mar77i

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

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

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

0
OphyTe