BASH: Usar o awk para filtrar linhas únicas resulta em uma matriz de tamanho 0

2

Nota: Obrigado a Jeff Schaller e Steeldriver. Mas como nem postou como resposta, não sei como marcar como resolvido. Agora tenho um melhor entendimento de pipes / subshells. Tenho certeza que uma vez eu sabia disso, mas faz muito tempo desde que eu tentei algo complexo no bash.

Ambos atribuindo o resultado filtrado de awk a uma variável e substituição de processo funcionaram para mim. Meu código final para ler linhas únicas não classificadas de stdin :

while read -r FILE
do
    ...
done < <(awk '!x[$0]++')

Mais leituras sobre substituição de processos para aqueles que encontrarem esta questão procurando soluções para um problema similar.

PERGUNTA ORIGINAL:

Eu pesquisei no site, mas não consigo encontrar uma resposta para o meu problema.

Estou construindo uma matriz de stdin e preciso filtrar por linhas exclusivas. Para fazer isso, estou usando awk '!x[$0]++' , que eu li como abreviação:

awk 'BEGIN { while (getline s) { if (!seen[s]) print s; seen[s]=1 } }' .

O filtro funciona como desejado, mas o problema é que a matriz resultante do loop while read está vazia.

Por exemplo (usando $list como substituto para stdin ):

list=$'red apple\nyellow banana\npurple grape\norange orange\nyellow banana'
while read -r line; do
    array[count++]=$line
done <<< "$list"
echo "array length = ${#array[@]}"
counter=0
while [  $counter -lt ${#array[@]} ]; do
    echo ${array[counter++]}
done

produz:

array length = 5
red apple
yellow banana
purple grape
orange orange
yellow banana

Mas filtrando $list com awk:

list=$'red apple\nyellow banana\npurple grape\norange orange\nyellow banana'
awk '!x[$0]++' <<< "$list" | while read -r line; do
    array[count++]=$line
done
echo "array length = ${#array[@]}"
counter=0
while [  $counter -lt ${#array[@]} ]; do
     echo ${array[counter++]}
done

produz:

array length = 0

Mas a saída de awk '!x[$0]++' <<< "$list" parece boa:

red apple
yellow banana
purple grape
orange orange

Eu tentei examinar cada linha no loop while read :

list=$'red apple\nyellow banana\npurple grape\norange orange\nyellow banana'
i=0
awk '!x[$0]++' <<< "$list" | while read -r line; do
    echo "line[$i] = $line"
    let i=i+1
done

e parece bem:

line[0] = red apple
line[1] = yellow banana
line[2] = purple grape
line[3] = orange orange

O que estou perdendo aqui?

Caso seja importante, estou usando o bash 3.2.57:

GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin15) Copyright (C) 2007 Free Software Foundation, Inc.

    
por Mike 21.06.2016 / 20:59

2 respostas

3
awk '!x[$0]++' <<< "$list" | while read -r line; do
    array[count++]=$line
done

O array ( itálico ) neste caso é uma parte do subshell ( negrito ).

O $line e $array tem um valor enquanto a subshell está viva, por assim dizer.

Quando o subshell terminar, também conhecido como dies, o ambiente pai (spawner) será restaurado. Isso inclui a obliteração de qualquer variável definida no subnível.

Neste caso:

  • $array removido,
  • $line removido.

Tente isto:

list=$'red apple\nyellow banana\npurple grape\norange orange\nyellow banana'
awk '!x[$0]++' <<< "$list" | while read -r line; do
    array[count++]=$line
    printf "array[%d] { %s\n" ${#array[@]} # array[num_of_elements] {
    printf "       %s\n" "${array[@]}"     # elements
    printf "}\n"                           # } end of array

done

printf "\n[ %s ]\n\n" "END OF SUBSHELL (PIPE)"

printf "array[%d] {\n" ${#array[@]}
printf "       %s\n" "${array[@]}"
printf "}\n"

Rendimentos:

array[1] {
       red apple
}
array[2] {
       red apple
       yellow banana
}
array[3] {
       red apple
       yellow banana
       purple grape
}
array[4] {
       red apple
       yellow banana
       purple grape
       orange orange
}

[ END OF SUBSHELL (PIPE) ]

array[0] {

}

Ou como por manual.

Podemos começar com Pipelines

[…] Each command in a pipeline is executed in its own subshell (see Command Execution Environment). […]

E o Ambiente de Execução de Comando expande a aventura da seguinte forma:

[…] A command invoked in this separate environment cannot affect the shell’s execution environment.

Command substitution, commands grouped with parentheses, and asynchronous commands are invoked in a subshell environment that is a duplicate of the shell environment, except that traps caught by the shell are reset to the values that the shell inherited from its parent at invocation. Builtin commands that are invoked as part of a pipeline are also executed in a subshell environment. Changes made to the subshell environment cannot affect the shell’s execution environment. […]

Não pode afetar: assim, não é possível definir.

No entanto, podemos redirecionar e fazer algo na direção de:

list=$'red apple\nyellow banana\npurple grape\norange orange\nyellow banana'

while read -r line; do
    arr[count++]=$line
done <<<"$(awk '!x[$0]++' <<< "$list")"

echo "arr length = ${#arr[@]}"
count=0
while [[  $count -lt ${#arr[@]} ]]; do
    echo ${arr[count++]}
done
    
por 21.06.2016 / 22:19
1

Algumas soluções para o seu problema sem o loop

# use bash's mapfile with process substitution 
mapfile -t arr < <( awk '!x[$0]++' <<<"$list" )

# use array assignment syntax (at least bash, ksh, zsh) 
# of a command-substituted value split at newline only
# and (if the data can contain globs) globbing disabled
set -f; IFS='\n' arr=( $( awk '!x[$0]++' <<<"$list" ) ); set +f
    
por 22.06.2016 / 02:00