[arrays] Bash 배열에서 요소 제거

bash 셸의 배열에서 요소를 제거해야합니다. 일반적으로 간단히 다음을 수행합니다.

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

불행히도 제거하려는 요소는 변수이므로 이전 명령을 사용할 수 없습니다. 여기에 예가 있습니다.

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

어떤 생각?



답변

다음은 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


답변

원하지 않는 요소없이 새 배열을 만든 다음 이전 배열에 다시 할당 할 수 있습니다. 이것은 다음에서 작동합니다 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


답변

이것이 위치를 안다면 값을 설정 해제하는 가장 직접적인 방법입니다.

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


답변

다음은 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) 배열의 패턴 대체 또는 (2) 배열 요소에 대한 반복입니다. 첫 번째는 빠르지 만 고유 한 접두사가있는 요소 만 처리 할 수 ​​있고, 두 번째는 O (n * k), n = 배열 ​​크기, k = 제거 할 요소가 있습니다. 연관 배열은 상대적으로 새로운 기능이며 질문이 처음 게시되었을 때 일반적이지 않았을 수 있습니다.

정확한 일치 케이스의 경우 큰 n과 k를 사용하면 O (n k)에서 O (n + k log (k))로 성능을 향상시킬 수 있습니다 . 실제로 O (n)는 k가 n보다 훨씬 낮다고 가정합니다. 대부분의 속도 향상은 제거 할 항목을 식별하기 위해 연관 배열을 사용하는 것입니다.

성능 (n-array 크기, 삭제할 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용액은 약간 느린 VS이다 current용액 때 추가 설정 때문에 K = 1.

‘빠른’솔루션 : 배열 = 입력 목록, 삭제 = 제거 할 값 목록.

        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[@]}")


답변

다음은 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*위해 for 를 전환하십시오 . 사용 ${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쉘 단어 인용 및 공백을 정확히 echo "${a[@]}". 그러나 이것은 길이나 인덱스를 인쇄하는 것이 아니라 배열의 요소를 인쇄 할 때만 작동합니다 ( aryref="!a[@]"또는 aryref="#a[@]"또는 "${!!aryref}"또는 "${#!aryref}"모두 실패).

그래서 bash 간접 지정을 통해 원래 배열을 이름으로 복사하고 복사본에서 색인을 얻습니다. 인덱스를 반대로 반복하기 위해 C 스타일 for 루프를 사용합니다. 또한를 통해 인덱스에 액세스하고을 사용하여 ${!arycopy[@]}반전하여 수행 할 수 있습니다 . 이는 입력 라인 순서를 뒤집는 taca cat입니다.

변수 간접이없는 함수 솔루션은 아마도를 포함해야 할 것입니다 eval. 이는 해당 상황에서 사용하기에 안전 할 수도 있고 안전하지 않을 수도 있습니다 (알 수 없습니다).


답변

위의 답변을 확장하기 위해 다음을 사용하여 부분 일치없이 배열에서 여러 요소를 제거 할 수 있습니다.

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

그러면 다음을 포함하는 배열이 생성됩니다. (two onetwo three threefour “one six”)