¿Cómo puedo obtener valores únicos de una matriz en Bash?

93

Tengo casi la misma pregunta que aquí .

Tengo una matriz que contiene aa ab aa ac aa ad, etc. Ahora quiero seleccionar todos los elementos únicos de esta matriz. Pensé, esto sería simple con sort | uniqo con sort -ucomo mencionaron en esa otra pregunta, pero nada cambió en la matriz ... El código es:

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

¿Qué estoy haciendo mal?

Jetse
fuente

Respuestas:

131

Un poco hacky, pero esto debería funcionar:

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

Para guardar los resultados únicos ordenados nuevamente en una matriz, realice la asignación de matriz :

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

Si su shell admite estas cadenas ( bashdebería), puede ahorrar un echoproceso modificándolo a:

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

Entrada:

ids=(aa ab aa ac aa ad)

Salida:

aa ab ac ad

Explicación:

  • "${ids[@]}"- Sintaxis para trabajar con matrices de shell, ya sea que se utilicen como parte echoo como una cadena. La @parte significa "todos los elementos de la matriz"
  • tr ' ' '\n'- Convertir todos los espacios en nuevas líneas. Debido a que shell ve su matriz como elementos en una sola línea, separados por espacios; y porque sort espera que la entrada esté en líneas separadas.
  • sort -u - ordenar y retener solo elementos únicos
  • tr '\n' ' ' - convertir las líneas nuevas que agregamos anteriormente en espacios.
  • $(...) - Sustitución de mando
  • Aparte: tr ' ' '\n' <<< "${ids[@]}"es una forma más eficiente de hacer:echo "${ids[@]}" | tr ' ' '\n'
sampson-chen
fuente
37
+1. Un poco más ordenado: almacene elementos uniq en una nueva matriz:uniq=($(printf "%s\n" "${ids[@]}" | sort -u)); echo "${uniq[@]}"
glenn jackman
@glennjackman ¡oh genial! Ni siquiera me di cuenta de que puede utilizar printfde esa manera (dar más argumentos que las cadenas de formato)
Sampson-chen
4
1 No estoy seguro de si se trata de un caso aislado, pero poniendo artículos únicos de nuevo en una matriz necesaria paréntesis adicionales, tales como: sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')). Sin los paréntesis adicionales, lo estaba dando como una cadena.
hasta el
3
Si no desea alterar el orden de los elementos, utilice en ... | uniq | ...lugar de ... | sort -u | ....
Jesse Chisholm
2
@Jesse, uniqsolo elimina los duplicados consecutivos . En el ejemplo de esta respuesta, sorted_unique_idsterminará idéntico al original ids. Para preservar el orden, inténtelo ... | awk '!seen[$0]++'. Consulte también stackoverflow.com/questions/1444406/… .
Rob Kennedy
29

Si está ejecutando Bash versión 4 o superior (que debería ser el caso en cualquier versión moderna de Linux), puede obtener valores de matriz únicos en bash creando una nueva matriz asociativa que contenga cada uno de los valores de la matriz original. Algo como esto:

$ 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

Esto funciona porque en cualquier matriz (asociativa o tradicional, en cualquier idioma), cada clave solo puede aparecer una vez. Cuando el forbucle llega al segundo valor de aain a[2], sobrescribe el b[aa]que se estableció originalmente a[0].

Hacer cosas en bash nativo puede ser más rápido que usar tuberías y herramientas externas como sorty uniq, aunque para conjuntos de datos más grandes, probablemente verá un mejor rendimiento si usa un lenguaje más poderoso como awk, python, etc.

Si se siente seguro, puede evitar el forbucle usando printfla capacidad de reciclar su formato para múltiples argumentos, aunque esto parece ser necesario eval. (Deja de leer ahora si estás de acuerdo con eso).

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

La razón por la que esta solución requiere evales que los valores de la matriz se determinen antes de la división de palabras. Eso significa que la salida de la sustitución del comando se considera una sola palabra lugar de un conjunto de pares clave = valor.

Si bien esto usa una subcapa, solo usa elementos internos de bash para procesar los valores de la matriz. Asegúrese de evaluar su uso evalcon ojo crítico. Si no está 100% seguro de que chepner, glenn jackman o greycat no encontrarán fallas en su código, use el bucle for en su lugar.

ghoti
fuente
produce un error: se superó el nivel de recursividad de la expresión
Benubird
1
@Benubird: ¿quizás puedas pegar el contenido de tu terminal? Funciona perfectamente para mí, así que mi mejor suposición es que tienes (1) un error tipográfico, (2) una versión anterior de bash (se agregaron matrices asociativas a la v4) o (3) una afluencia ridículamente grande de fondo cósmico radiación causada por el agujero negro cuántico en el sótano de su vecino, generando interferencia con las señales dentro de su computadora.
ghoti
1
no puedo, no guardé el que no funcionó. pero intenté ejecutar el tuyo ahora mismo y funcionó, así que probablemente lo de la radiación cósmica.
Benubird
suponiendo que esta respuesta utiliza bash v4 (matrices asociativas) y si alguien intenta en bash v3 no funcionará (probablemente no lo que vio @Benubird). Bash v3 todavía es predeterminado en muchos entornos
nhed
1
@nhed, punto tomado. Veo que mi Macbook Yosemite actualizado tiene la misma versión en la base, aunque he instalado v4 desde macports. Esta pregunta está etiquetada como "linux", pero he actualizado mi respuesta para señalar el requisito.
ghoti
18

Me doy cuenta de que esto ya fue respondido, pero apareció bastante alto en los resultados de búsqueda y podría ayudar a alguien.

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

Ejemplo:

~> 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
fuente
1
para arreglar la matriz, me vi obligado a hacer esto:, ids=(ab "a a" ac aa ad ac aa);IFS=$'\n' ids2=(`printf "%s\n" "${ids[@]}" |sort -u`)así que agregué lo IFS=$'\n'sugerido por @gniourf_gniourf
Aquarius Power
¡También tuve que hacer una copia de seguridad y, después del comando, restaurar el valor IFS! o arruina otras cosas ..
Poder de Acuario
@Jetse Esta debería ser la respuesta aceptada ya que usa solo dos comandos, sin bucles, sin evaluación y es la versión más compacta.
mgutt
1
@AquariusPower Cuidado, básicamente está haciendo:, IFS=$'\n'; ids2=(...)ya que la asignación temporal antes de la asignación de variables no es posible. En lugar de utilizar esta construcción: IFS=$'\n' read -r -a ids2 <<<"$(printf "%s\n" "${ids[@]}" | sort -u)".
Yeti
13

Si los elementos de su matriz tienen espacios en blanco o cualquier otro carácter especial de shell (¿y puede estar seguro de que no lo tienen?), Entonces para capturarlos en primer lugar (y siempre debe hacer esto), exprese su matriz entre comillas dobles. ej "${a[@]}". Bash literalmente interpretará esto como "cada elemento de la matriz en un argumento separado ". Dentro de bash esto simplemente siempre funciona, siempre.

Luego, para obtener una matriz ordenada (y única), tenemos que convertirla a un formato que la ordenación comprenda y poder convertirla nuevamente en elementos de matriz bash. Esto es lo mejor que se me ocurrió:

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

Desafortunadamente, esto falla en el caso especial de la matriz vacía, convirtiendo la matriz vacía en una matriz de 1 elemento vacío (porque printf tenía 0 argumentos pero aún se imprime como si tuviera un argumento vacío - ver explicación). Así que tienes que captar eso en un si o algo.

Explicación: El formato% q para printf "shell escapa" del argumento impreso, de tal manera que bash puede recuperarse en algo como eval! Debido a que cada elemento se imprime y se escapa en su propia línea, el único separador entre elementos es la nueva línea, y la asignación de matriz toma cada línea como un elemento, analizando los valores escapados en texto literal.

p.ej

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

La evaluación es necesaria para eliminar el escape de cada valor que regresa a la matriz.

vontrapp
fuente
Este es el único código que me funcionó porque mi matriz de cadenas tenía espacios. El% q es lo que hizo el truco. Gracias :)
Somaiah Kumbera
Y si no desea alterar el orden de los elementos, utilice en uniqlugar de sort -u.
Jesse Chisholm
Tenga en cuenta que uniqno funciona correctamente en listas sin clasificar, por lo que siempre debe usarse en combinación con sort.
Jean Paul
uniq en una lista sin clasificar eliminará los duplicados consecutivos . No eliminará elementos de lista idénticos separados por algo más entre ellos. uniq puede ser lo suficientemente útil dependiendo de los datos esperados y el deseo de mantener el orden original.
vontrapp
10

'sort' se puede usar para ordenar la salida de un bucle for:

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

y elimine los duplicados con "-u":

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

Finalmente, puede sobrescribir su matriz con los elementos únicos:

ids=( `for i in ${ids[@]}; do echo $i; done | sort -u` )
corbyn42
fuente
Y si no quieres cambiar el orden de lo que queda, no tienes que hacerlo:ids=( `for i in ${ids[@]}; do echo $i; done | uniq` )
Jesse Chisholm
3

este también preservará el orden:

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

y modificar la matriz original con los valores únicos:

ARRAY=($(echo ${ARRAY[@]} | tr [:space:] '\n' | awk '!a[$0]++'))
fausto
fuente
No lo use uniq. Necesita ordenar, donde awk no lo hace, y la intención de esta respuesta es preservar el orden cuando la entrada no está ordenada.
Bukzor
2

Para crear una nueva matriz que consta de valores únicos, asegúrese de que su matriz no esté vacía y luego realice una de las siguientes acciones:

Eliminar entradas duplicadas (con clasificación)

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

Eliminar entradas duplicadas (sin clasificar)

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

Advertencia: no intente hacer algo como NewArray=( $(printf '%s\n' "${OriginalArray[@]}" | sort -u) ). Se romperá en espacios.

Seis
fuente
Eliminar entradas duplicadas (sin ordenar) es como (con ordenar) excepto que cambia sort -ua ser uniq.
Jesse Chisholm
@JesseChisholm uniqsolo fusiona líneas duplicadas adyacentes, por lo que no es lo mismo que awk '!x[$0]++'.
Seis
@JesseChisholm Elimine el comentario engañoso.
bukzor
2

cat number.txt

1 2 3 4 4 3 2 5 6

imprimir línea en columna: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}'

1
2
3
4
4
3
2
5
6

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

4
3
2

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

1
2
3
4
5
6

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

1
5
6
VIPIN KUMAR
fuente
1

Sin perder el pedido original:

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

Si desea una solución que solo use componentes internos de bash, puede establecer los valores como claves en una matriz asociativa y luego extraer las claves:

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

Esto saldrá

bar
foo
bar none
rln
fuente
Acabo de notar que esto es esencialmente lo mismo que la respuesta de @ghotis anterior, excepto que su solución no tiene en cuenta los elementos de la lista con espacios.
rln
Buen punto. Agregué comillas a mi solución, por lo que ahora maneja espacios. Originalmente lo escribí simplemente para manejar los datos de muestra en la pregunta, pero siempre es bueno cubrir contingencias como esta. Gracias por la sugerencia.
ghoti
1

Otra opción para tratar con espacios en blanco incrustados es delimitar nulo con printf, hacer distintivo con sorty luego usar un bucle para empaquetarlo nuevamente en una 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)

Al final de esto, inputy outputcontenga los valores deseados (el orden proporcionado no es importante):

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

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

¿Qué tal esta variación?

printf '%s\n' "${ids[@]}" | sort -u
jmg
fuente
Y luego sorted_arr=($(printf '%s\n' "${ids[@]}" | sort -u).
algas
0

Intente esto para obtener valores uniq para la primera columna en el archivo

awk -F, '{a[$1];}END{for (i in a)print i;}'
Suresh Aitha
fuente
-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
Ley K
fuente