Como posso obter valores exclusivos de uma matriz no Bash?

95

Eu tenho quase a mesma pergunta que aqui .

Eu tenho uma matriz que contém aa ab aa ac aa ad, etc. Agora, quero selecionar todos os elementos exclusivos dessa matriz. Pensei, isso seria simples com sort | uniqou com sort -ucomo eles mencionaram na outra pergunta, mas nada mudou na matriz ... O código é:

echo `echo "${ids[@]}" | sort | uniq`

O que estou fazendo errado?

Jetse
fonte

Respostas:

133

Um pouco maluco, mas deve bastar:

echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '

Para salvar os resultados exclusivos classificados de volta em uma matriz, faça a atribuição de Array :

sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))

Se seu shell oferece suporte a cadeias de caracteres ( bashdeveria), você pode poupar um echoprocesso alterando-o para:

tr ' ' '\n' <<< "${ids[@]}" | sort -u | tr '\n' ' '

Entrada:

ids=(aa ab aa ac aa ad)

Resultado:

aa ab ac ad

Explicação:

  • "${ids[@]}"- Sintaxe para trabalhar com matrizes de shell, sejam usadas como parte de echoou uma cadeia de caracteres. A @parte significa "todos os elementos da matriz"
  • tr ' ' '\n'- Converta todos os espaços em novas linhas. Porque seu array é visto pelo shell como elementos em uma única linha, separados por espaços; e porque o sort espera que a entrada esteja em linhas separadas.
  • sort -u - classificar e reter apenas elementos únicos
  • tr '\n' ' ' - converte as novas linhas que adicionamos anteriormente em espaços.
  • $(...)- Substituição de Comando
  • À parte: tr ' ' '\n' <<< "${ids[@]}"é uma maneira mais eficiente de fazer:echo "${ids[@]}" | tr ' ' '\n'
Sampson-Chen
fonte
37
+1. Um pouco mais organizado: armazene os elementos uniq em uma nova matriz:uniq=($(printf "%s\n" "${ids[@]}" | sort -u)); echo "${uniq[@]}"
glenn jackman
@glennjackman oh legal! Eu nem sabia que você pode usar printfdessa forma (dar mais argumentos do que formato de strings)
sampson-chen
4
1 Eu não tenho certeza se este é um caso isolado, mas colocando itens exclusivos de volta em uma matriz necessária parênteses adicionais, tais como: sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')). Sem os parênteses adicionais, estava sendo fornecido como uma string.
whla
3
Se você não quiser alterar a ordem dos elementos, use em ... | uniq | ...vez de ... | sort -u | ....
Jesse Chisholm
2
@Jesse, uniqremove apenas duplicatas consecutivas . No exemplo desta resposta, sorted_unique_idsficará idêntico ao original ids. Para preservar a ordem, tente ... | awk '!seen[$0]++'. Consulte também stackoverflow.com/questions/1444406/… .
Rob Kennedy
29

Se você estiver executando o Bash versão 4 ou superior (o que deve ser o caso em qualquer versão moderna do Linux), poderá obter valores de array exclusivos em bash criando um novo array associativo que contém cada um dos valores do array original. Algo assim:

$ a=(aa ac aa ad "ac ad")
$ declare -A b
$ for i in "${a[@]}"; do b["$i"]=1; done
$ printf '%s\n' "${!b[@]}"
ac ad
ac
aa
ad

Isso funciona porque em qualquer array (associativo ou tradicional, em qualquer idioma), cada chave só pode aparecer uma vez. Quando o forloop chega ao segundo valor de aain a[2], ele substitui o b[aa]que foi definido originalmente para a[0].

Fazer coisas no bash nativo pode ser mais rápido do que usar canais e ferramentas externas como sorte uniq, embora para conjuntos de dados maiores você provavelmente verá um desempenho melhor se usar uma linguagem mais poderosa como awk, python, etc.

Se você estiver se sentindo confiante, pode evitar o forloop usando printfa capacidade de reciclar seu formato para vários argumentos, embora isso pareça exigir eval. (Pare de ler agora se você estiver bem com isso.)

$ eval b=( $(printf ' ["%s"]=1' "${a[@]}") )
$ declare -p b
declare -A b=(["ac ad"]="1" [ac]="1" [aa]="1" [ad]="1" )

O motivo pelo qual essa solução exige evalé que os valores da matriz sejam determinados antes da divisão das palavras. Isso significa que a saída da substituição do comando é considerada uma única palavra em vez de um conjunto de pares chave = valor.

Embora use um subshell, ele usa apenas bash builtins para processar os valores do array. Certifique-se de avaliar o uso do evalcom um olhar crítico. Se você não estiver 100% confiante de que chepner ou glenn jackman ou greycat não encontrariam nenhuma falha em seu código, use o loop for.

ghoti
fonte
produz erro: nível de recursão da expressão excedido
Benubird
1
@Benubird - você pode colar o conteúdo do seu terminal? Funciona perfeitamente para mim, então meu melhor palpite é que você tem (1) um erro de digitação, (2) uma versão mais antiga do bash (arrays associativos foram adicionados à v4) ou (3) um influxo ridiculamente grande de fundo cósmico radiação causada pelo buraco negro quântico no porão do seu vizinho, gerando interferência com os sinais dentro do seu computador.
ghoti
1
não pode, não guardei o que não funcionou. mas, eu tentei executar o seu agora e funcionou, então provavelmente a coisa da radiação cósmica.
Benubird
supondo que esta resposta utiliza bash v4 (matrizes associativas) e se alguém tentar no bash v3, não funcionará (provavelmente não o que @Benubird viu). Bash v3 ainda é o padrão em muitos ambientes
2015
1
@nhed, ponto escolhido. Vejo que meu Yosemite Macbook atualizado tem a mesma versão na base, embora eu tenha instalado a v4 de macports. Esta pergunta está marcada como "linux", mas eu atualizei minha resposta para apontar o requisito.
ghoti
18

Sei que isso já foi respondido, mas apareceu bem alto nos resultados da pesquisa e pode ajudar alguém.

printf "%s\n" "${IDS[@]}" | sort -u

Exemplo:

~> IDS=( "aa" "ab" "aa" "ac" "aa" "ad" )
~> echo  "${IDS[@]}"
aa ab aa ac aa ad
~>
~> printf "%s\n" "${IDS[@]}" | sort -u
aa
ab
ac
ad
~> UNIQ_IDS=($(printf "%s\n" "${IDS[@]}" | sort -u))
~> echo "${UNIQ_IDS[@]}"
aa ab ac ad
~>
das.cyklone
fonte
1
para corrigir a matriz, fui forçado a fazer o seguinte :, ids=(ab "a a" ac aa ad ac aa);IFS=$'\n' ids2=(`printf "%s\n" "${ids[@]}" |sort -u`)então adicionei o IFS=$'\n'sugerido por @gniourf_gniourf
Aquarius Power
Também tive que fazer o backup e, após o comando, restaurar o valor IFS! ou bagunça outras coisas ..
Aquarius Power
@Jetse Esta deve ser a resposta aceita, pois usa apenas dois comandos, sem loops, sem eval e é a versão mais compacta.
mgutt
1
@AquariusPower Cuidado, você basicamente está fazendo:, uma IFS=$'\n'; ids2=(...)vez que a atribuição temporária antes das atribuições de variáveis ​​não é possível. Em vez disso usar esta construção: IFS=$'\n' read -r -a ids2 <<<"$(printf "%s\n" "${ids[@]}" | sort -u)".
Yeti
13

Se seus elementos de array têm espaço em branco ou qualquer outro caractere especial de shell (e você pode ter certeza que eles não têm?) Então, para capturá-los antes de tudo (e você deve sempre fazer isso), expresse seu array em aspas duplas! por exemplo "${a[@]}". O Bash interpretará isso literalmente como "cada elemento do array em um argumento separado ". No bash, isso simplesmente sempre funciona, sempre.

Então, para obter um array ordenado (e único), temos que convertê-lo em um formato que a classificação entenda e ser capaz de convertê-lo de volta em elementos do array bash. Este é o melhor que eu fiz:

eval a=($(printf "%q\n" "${a[@]}" | sort -u))

Infelizmente, isso falha no caso especial do array vazio, transformando o array vazio em um array de 1 elemento vazio (porque printf tinha 0 argumentos, mas ainda imprime como se tivesse um argumento vazio - veja a explicação). Então você tem que pegar isso em um se ou algo.

Explicação: O formato% q para printf "shell escapa" do argumento impresso, da mesma forma que o bash pode se recuperar em algo como eval! Como cada elemento é impresso com escape de shell em sua própria linha, o único separador entre os elementos é a nova linha, e a atribuição da matriz leva cada linha como um elemento, analisando os valores de escape em texto literal.

por exemplo

> a=("foo bar" baz)
> printf "%q\n" "${a[@]}"
'foo bar'
baz
> printf "%q\n"
''

O eval é necessário para retirar o escape de cada valor que volta ao array.

vontrapp
fonte
Este é o único código que funcionou para mim porque meu array de strings tinha espaços. O% q é o que funcionou. Obrigado :)
Somaiah Kumbera
E se você não quiser alterar a ordem dos elementos, use em uniqvez de sort -u.
Jesse Chisholm
Observe que uniqnão funciona corretamente em listas não classificadas, portanto, sempre deve ser usado em combinação com sort.
Jean Paul
uniq em uma lista não classificada removerá duplicatas consecutivas . Não removerá elementos de lista idênticos separados por algo mais no meio. O uniq pode ser útil o suficiente dependendo dos dados esperados e do desejo de manter a ordem original.
vontrapp
10

'sort' pode ser usado para ordenar a saída de um loop for:

for i in ${ids[@]}; do echo $i; done | sort

e elimine duplicatas com "-u":

for i in ${ids[@]}; do echo $i; done | sort -u

Por fim, você pode apenas substituir sua matriz com os elementos exclusivos:

ids=( `for i in ${ids[@]}; do echo $i; done | sort -u` )
Corbyn42
fonte
E se você não quiser alterar a ordem do que resta, você não precisa:ids=( `for i in ${ids[@]}; do echo $i; done | uniq` )
Jesse Chisholm
Observe, entretanto, que se você não alterar a ordem, também não obterá o resultado desejado, pois uniqapenas remove as linhas duplicadas adjacentes .
Jason Kohles
3

este também preservará a ordem:

echo ${ARRAY[@]} | tr [:space:] '\n' | awk '!a[$0]++'

e para modificar a matriz original com os valores únicos:

ARRAY=($(echo ${ARRAY[@]} | tr [:space:] '\n' | awk '!a[$0]++'))
faustus
fonte
Não use uniq. Ele precisa de classificação, onde awk não, e a intenção dessa resposta é preservar a ordem quando a entrada não é classificada.
bukzor
2

Para criar uma nova matriz consistindo em valores únicos, certifique-se de que sua matriz não esteja vazia e execute um dos seguintes procedimentos:

Remover entradas duplicadas (com classificação)

readarray -t NewArray < <(printf '%s\n' "${OriginalArray[@]}" | sort -u)

Remova entradas duplicadas (sem classificação)

readarray -t NewArray < <(printf '%s\n' "${OriginalArray[@]}" | awk '!x[$0]++')

Aviso: Não tente fazer algo assim NewArray=( $(printf '%s\n' "${OriginalArray[@]}" | sort -u) ). Ele vai quebrar em espaços.

Seis
fonte
Remover entradas duplicadas (sem classificação) é exatamente como (com classificação), exceto a mudança sort -upara ser uniq.
Jesse Chisholm
@JesseChisholm uniqmescla apenas linhas duplicadas adjacentes, portanto, não é o mesmo que awk '!x[$0]++'.
Seis
@JesseChisholm Por favor, exclua o comentário enganoso.
bukzor
2

cat number.txt

1 2 3 4 4 3 2 5 6

imprimir linha na coluna: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}'

1
2
3
4
4
3
2
5
6

encontre os registros duplicados: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk 'x[$0]++'

4
3
2

Substitua os registros duplicados: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk '!x[$0]++'

1
2
3
4
5
6

Encontre apenas registros Uniq: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i|"sort|uniq -u"}

1
5
6
VIPIN KUMAR
fonte
1

Sem perder o pedido original:

uniques=($(tr ' ' '\n' <<<"${original[@]}" | awk '!u[$0]++' | tr '\n' ' '))
Estani
fonte
1

Se você deseja uma solução que usa apenas componentes internos do bash, pode definir os valores como chaves em uma matriz associativa e extrair as chaves:

declare -A uniqs
list=(foo bar bar "bar none")
for f in "${list[@]}"; do 
  uniqs["${f}"]=""
done

for thing in "${!uniqs[@]}"; do
  echo "${thing}"
done

Isso irá produzir

bar
foo
bar none
rln
fonte
Acabei de notar que esta é essencialmente a mesma resposta de @ghotis acima, exceto que sua solução não leva os itens da lista com espaços em consideração.
rln
Bom ponto. Eu adicionei aspas à minha solução, então agora ela trata de espaços. Eu o escrevi originalmente apenas para lidar com os dados de amostra da pergunta, mas é sempre bom cobrir contingências como essa. Obrigado pela sugestão.
ghoti
1

Outra opção para lidar com espaços em branco incorporados é delimitar nulos printf, fazer distinção com e sort, em seguida, usar um loop para empacotá-los de volta em uma matriz:

input=(a b c "$(printf "d\ne")" b c "$(printf "d\ne")")
output=()

while read -rd $'' element
do 
  output+=("$element")
done < <(printf "%s\0" "${input[@]}" | sort -uz)

No final disso, inpute outputcontêm os valores desejados (a ordem fornecida não é importante):

$ printf "%q\n" "${input[@]}"
a
b
c
$'d\ne'
b
c
$'d\ne'

$ printf "%q\n" "${output[@]}"
a
b
c
$'d\ne'
Morgen
fonte
1

Que tal essa variação?

printf '%s\n' "${ids[@]}" | sort -u
jmg
fonte
E então sorted_arr=($(printf '%s\n' "${ids[@]}" | sort -u).
algas
0

Tente isto para obter valores uniq para a primeira coluna no arquivo

awk -F, '{a[$1];}END{for (i in a)print i;}'
Suresh Aitha
fonte
-3
# Read a file into variable
lines=$(cat /path/to/my/file)

# Go through each line the file put in the variable, and assign it a variable called $line
for line in $lines; do
  # Print the line
  echo $line
# End the loop, then sort it (add -u to have unique lines)
done | sort -u
K Law
fonte