Видаліть елемент із масиву Bash


116

Мені потрібно видалити елемент з масиву в оболонці bash. Як правило, я б просто робив:

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

На жаль, елемент, який я хочу видалити, є змінною, тому я не можу використовувати попередню команду. Ось тут приклад:

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

Будь-яка ідея?


Яка оболонка? Ваш приклад виглядає так zsh.
чепнер

array=( ${array[@]/$delete} )працює, як очікувалося в Bash. Ви просто пропустили =?
Кен Шарп

1
@Ken, це не зовсім те, що потрібно - він видалить будь-які збіги з кожного рядка і залишить порожні рядки в масиві, де він відповідає всій рядку.
Toby Speight

Відповіді:


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

Caveat

Цей прийом фактично видаляє збіги префіксів $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

43
просто знайте, що: $ array=(sun sunflower) $ delete=(sun) $ echo ${array[@]/$delete}результати вflower
Бернштейн

12
Зауважте, що це насправді робить заміну, тому, якщо масив є чимось подібним, (pluto1 pluto2 pippo)тоді ви закінчите (1 2 pippo).
haridsv

5
Будьте обережні, використовуючи це в циклі for, тому що у вас виявиться порожній елемент, де був видалений елемент. З розуму можна зробити щось на кшталтfor element in "${array[@]}" do if [[ $element ]]; then echo ${element} fi done
Джоель Б

2
Тож як видалити лише відповідні елементи?
Умань

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, ні, ${array[1]+x}є нульовим рядком, тому array[1]не встановлено. unsetне змінює індекси решти елементів. Цитування аргументу для скасування не потрібно. Спосіб знищення елемента масиву описаний у посібнику Bash .
jarno

@rashok Я не бачу, чому ні. Ви не можете припустити, що ${array[1]}існує лише тому, що розмір становить 2. Якщо потрібно індекси, поставте прапорець ${!array[@]}.
Даніель К. Собрал

4

Ось однолінійне рішення з картографічним файлом:

$ 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 Ви неправі, принаймні, на базі 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, з - за додаткового налаштування.

Рішення "Швидкий": array = список вводу, 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

Ось (мабуть, дуже баш-певна) мала функція, що включає баш змінної непрямості та 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,,}для нечутливих до регістрів сірників; і т.д., що б баш [[підтримував.

Він працює, визначаючи індекси вхідного масиву та повторюючи їх назад (тому видалення елементів не порушує порядок ітерації). Для отримання індексів потрібно отримати доступ до вхідного масиву за назвою, що можна зробити за допомогою баш-змінної непрямості 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 indirect і отримую індекси від копії. Для повторення індексів у зворотному напрямку я використовую С-стиль для циклу. Я також міг би це зробити, отримуючи доступ до індексів через ${!arycopy[@]}та реверсуючи їх tac, що означає, catщо обертається навколо порядку введення рядка.

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


Це майже добре працює, однак він не переосмислює початковий масив, переданий у функцію, тому, хоча у цього початкового масиву відсутні його значення, його індекси також змішуються. Це означає, що наступний дзвінок, який ви зробите для видалення_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

Це призведе до масиву, що містить: (два onetwo три триfour "один шість")


2

Якщо хтось опинився в положенні, коли йому потрібно запам'ятати значення set -e або set -x і змогти відновити їх, будь ласка, ознайомтеся з цим суттю, яке використовує рішення для видалення першого масиву для управління його власним стеком:

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


1

Часткова відповідь

Щоб видалити перший елемент з масиву

unset 'array[0]'

Щоб видалити останній елемент з масиву

unset 'array[-1]'

@gniourf_gniourf не потрібно використовувати лапки для аргументу unset.
jarno

2
@jarno: ОБОВ'ЯЗКОВО використовувати ці лапки: якщо array0у поточному каталозі у вас є названий файл , то оскільки array[0]глобус є, він спочатку буде розширений до array0команди unset.
gniourf_gniourf

@gniourf_gniourf ви праві. Це слід виправити в Довідковому посібнику Баша, де в даний час сказано, що "unset name [subscript] знищує елемент масиву в індексі індексу".
jarno

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

0

У скрипту оболонки POSIX немає масивів.

Так що, швидше за все, ви використовуєте конкретний діалект, наприклад bash, korn shell або zsh.

Тому на ваше запитання відтепер не можна відповісти.

Можливо, це працює для вас:

unset array[$delete]

2
Привіт, я використовую bash shell atm. І "$ delete" - це не елемент елемента, а сам рядок. Тож я не думаю, що "невдалий" спрацює
Алекс

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 ?

Справді ви можете додати більше одного елемента разом із цим вмістом цілого іншого масиву одночасно.


0

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

$ {PARAMETER # PATTERN} # видалити з початку

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

$ {PARAMETER% PATTERN} # видалити з кінця

$ {PARAMETER %% PATTERN} # видалити з кінця, жадібна відповідність

Для того, щоб зробити повний елемент видалення, ви повинні виконати команду unset з оператором if. Якщо вам не важливо видаляти префікси з інших змінних або підтримувати пробіл у масиві, тоді ви можете просто кинути лапки і забути про циклі.

Дивіться приклад нижче для кількох різних способів очищення масиву.

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

У 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

Вибачте, щойно спробував. Це не спрацювало в zsh для асоціативного масиву
Falk

Це працює просто чудово, я просто перевірив це (знову). Речі для вас не працюють? Поясніть, будь ласка, що не вийшло точно так детально, як ви можете. Яку версію ZSH ви використовуєте?
trevorj

0

Також є цей синтаксис, наприклад, якщо ви хочете видалити 2-й елемент:

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

що насправді є з'єднанням 2 вкладки. Перший від індексу 0 до індексу 1 (виключно) та 2-го від індексу 2 до кінця.



-1

Це швидке і брудне рішення, яке буде працювати в простих випадках, але зламається, якщо (a) в ньому є спеціальні символи регулярного вираження $delete, або (b) у будь-яких елементах взагалі є пробіли. Починаючи з:

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

-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
Чи можете ви додати коментарі чи опис, щоб розповісти про свою відповідь?
Майкл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.