¿Cómo puedo ordenar numéricamente una sola línea de elementos delimitados?


11

Tengo una línea (o muchas líneas) de números que están delimitados por un carácter arbitrario. ¿Qué herramientas de UNIX puedo usar para ordenar los elementos de cada línea numéricamente, conservando el delimitador?

Ejemplos incluyen:

  • lista de números; entrada 10 50 23 42:; ordenado:10 23 42 50
  • Dirección IP; entrada 10.1.200.42:; ordenado:1.10.42.200
  • CSV; entrada 1,100,330,42:; ordenado:1,42,100,330
  • delimitado por tubos; entrada 400|500|404:; ordenado:400|404|500

Dado que el delimitador es arbitrario, siéntase libre de proporcionar (o extender) una Respuesta utilizando un delimitador de un solo carácter de su elección.


8
deberías publicarlo en codegolf :)
ivanivan

1
Hay una pregunta similar también aquí. Me gustaría agregar su enlace .
αғsнιη

Solo una pista que cutadmite delimitadores arbitrarios con su -dopción.
Oleg Lobachev

Aclare si esos cuatro ejemplos de DSV están en el mismo archivo o si son muestras de cuatro archivos diferentes.
agc

2
Al ver algunos de los otros comentarios: el delimitador es arbitrario, pero se usaría de manera consistente en la entrada. Asumir inteligencia por parte del productor de datos de manera que no utilicen comas como delimitador y en los datos (por ejemplo, 4,325 comma 55 comma 42,430no ocurriría, ni 1.5 period 4.2).
Jeff Schaller

Respuestas:


12

Puede lograr esto con:

tr '.' '\n' <<<"$aline" | sort -n | paste -sd'.' -

reemplace los puntos . con su delimitador.
agregue -ual sortcomando anterior para eliminar los duplicados.


o con gawk( GNU awk ) podemos procesar muchas líneas, mientras que lo anterior también se puede ampliar:

gawk -v SEP='*' '{ i=0; split($0, arr, SEP); 
    while ( ++i<=asort(arr) ){ printf("%s%s", i>1?SEP:"", arr[i]) }; 
        print "" 
}' infile

reemplace *como el separador de campo SEP='*'con su delimitador .


Notas:
Es posible que deba usar la -g, --general-numeric-sortopción de en sortlugar de -n, --numeric-sortmanejar cualquier clase de números (entero, flotante, científico, hexadecimal, etc.).

$ aline='2e-18,6.01e-17,1.4,-4,0xB000,0xB001,23,-3.e+11'
$ tr ',' '\n' <<<"$aline" |sort -g | paste -sd',' -
-3.e+11,-4,2e-18,6.01e-17,1.4,23,0xB000,0xB001

En awkningún cambio de necesidad, seguirá manejando esos.


10

Usando perlhay una versión obvia; dividir los datos, ordenarlos, volver a unirlos.

El delimitador debe aparecer dos veces (una vez en splity una vez en join)

por ejemplo para un ,

perl -lpi -e '$_=join(",",sort {$a <=> $b} split(/,/))'

Entonces

echo 1,100,330,42 | perl -lpi -e '$_=join(",",sort {$a <=> $b} split(/,/))'
1,42,100,330

Como splites una expresión regular, el personaje puede necesitar una cita:

echo 10.1.200.42 | perl -lpi -e '$_=join(".",sort {$a <=> $b} split(/\./))'
1.10.42.200

Al usar las opciones -ay -F, es posible eliminar la división. Con el -pbucle, como antes y establezca los resultados en $_, que se imprimirán automáticamente:

perl -F'/\./' -aple '$_=join(".", sort {$a <=> $b} @F)'

44
puede usar la -lopción en lugar de usar chomp. Eso también agrega la nueva línea al imprimir. Ver también -a(con -F) para la parte de división.
Stéphane Chazelas

1
Con -ly -F, es aún mejor:perl -F'/\./' -le 'print join(".", sort {$a <=> $b} @F)'
muru

@ StéphaneChazelas gracias por la -lopción; ¡Me lo había perdido!
Stephen Harris el

1
@muru No usé el -Findicador originalmente porque no funciona correctamente en todas las versiones (por ejemplo, su línea en CentOS 7 - perl 5.16.3 - devuelve un resultado en blanco, aunque funciona bien en Debian 9). Pero combinado con -pél da un resultado un poco más pequeño, así que agregué eso como una alternativa a la respuesta. mostrando cómo -Fse puede usar. ¡Gracias!
Stephen Harris

2
@StephenHarris se debe a que las versiones más nuevas de perl agregan automáticamente -ay -nopciones cuando -Fse usa y -ncuando -ase usa ... así que simplemente cambie -lea-lane
Sundeep

4

Usando Python y una idea similar a la respuesta de Stephen Harris :

python3 -c 'import sys; c = sys.argv[1]; sys.stdout.writelines(map(lambda x: c.join(sorted(x.strip().split(c), key=int)) + "\n", sys.stdin))' <delmiter>

Entonces algo como:

$ cat foo
10.129.3.4
1.1.1.1
4.3.2.1
$ python3 -c 'import sys; c = sys.argv[1]; sys.stdout.writelines(map(lambda x: c.join(sorted(x.strip().split(c), key=int)) + "\n", sys.stdin))' . < foo
3.4.10.129
1.1.1.1
1.2.3.4

Lamentablemente, tener que hacer la E / S manualmente hace que esto sea mucho menos elegante que la versión Perl.



3

Cáscara

Cargar un idioma de nivel superior lleva tiempo.
Para algunas líneas, el shell en sí mismo puede ser una solución.
Podemos usar el comando externo sorty el comando tr. Uno es bastante eficiente en la clasificación de líneas y el otro es efectivo para convertir un delimitador en líneas nuevas:

#!/bin/bash
shsort(){
           while IFS='' read -r line; do
               echo "$line" | tr "$1" '\n' |
               sort -n   | paste -sd "$1" -
           done <<<"$2"
    }

shsort ' '    '10 50 23 42'
shsort '.'    '10.1.200.42'
shsort ','    '1,100,330,42'
shsort '|'    '400|500|404'
shsort ','    '3 b,2       x,45    f,*,8jk'
shsort '.'    '10.128.33.6
128.17.71.3
44.32.63.1'

Esto necesita bash debido al uso de <<<solo. Si eso se reemplaza con un documento aquí, la solución es válida para posix.
Este es capaz de ordenar campos con tabulaciones, espacios o caracteres shell glob ( *, ?, [). No nuevas líneas porque cada línea se está ordenando.

Cambie <<<"$2"a <"$2"para procesar nombres de archivos y llámelo como:

shsort '.'    infile

El delimitador es el mismo para todo el archivo. Si eso es una limitación, podría mejorarse.

Sin embargo, un archivo con solo 6000 líneas tarda 15 segundos en procesarse. En verdad, el shell no es la mejor herramienta para procesar archivos.

Awk

Para más de unas pocas líneas (más de unos pocos 10) es mejor usar un lenguaje de programación real. Una solución awk podría ser:

#!/bin/bash
awksort(){
           gawk -v del="$1" '{
               split($0, fields, del)
               l=asort(fields)
               for(i=1;i<=l;i++){
                   printf( "%s%s" , (i==0)?"":del , fields[i] )
               }
               printf "\n"
           }' <"$2"
         }

awksort '.'    infile

Que toma solo 0.2 segundos para el mismo archivo de 6000 líneas mencionado anteriormente.

Comprenda que los <"$2"archivos for podrían cambiarse nuevamente <<<"$2"por líneas dentro de variables de shell.

Perl

La solución más rápida es perl.

#!/bin/bash
perlsort(){  perl -lp -e '$_=join("'"$1"'",sort {$a <=> $b} split(/['"$1"']/))' <<<"$2";   }

perlsort ' '    '10 50 23 42'
perlsort '.'    '10.1.200.42'
perlsort ','    '1,100,330,42'
perlsort '|'    '400|500|404'
perlsort ','    '3 b,2       x,45    f,*,8jk'
perlsort '.'    '10.128.33.6
128.17.71.3
44.32.63.1'

Si desea ordenar un cambio de archivo <<<"$a"simplemente "$a"y agregar -ia las opciones de perl para que la edición del archivo esté "en su lugar":

#!/bin/bash
perlsort(){  perl -lpi -e '$_=join("'"$1"'",sort {$a <=> $b} split(/['"$1"']/))' "$2"; }

perlsort '.' infile; exit

2

Utilizando sedpara ordenar octetos de una dirección IP

sedno tiene una sortfunción incorporada, pero si sus datos están lo suficientemente limitados en el rango (como con las direcciones IP), puede generar un script sed que implemente manualmente una ordenación de burbuja simple . El mecanismo básico es buscar números adyacentes que están fuera de servicio. Si los números están fuera de servicio, cámbielos.

El sedscript en sí contiene dos comandos de búsqueda e intercambio para cada par de números desordenados: uno para los primeros dos pares de octetos (obligando a que esté presente un delimitador final para marcar el final del tercer octeto), y un segundo para el tercer par de octetos (finaliza con EOL). Si se producen intercambios, el programa se bifurca a la parte superior del script, buscando números que están fuera de orden. De lo contrario, sale.

El script generado es, en parte:

$ head -n 3 generated.sed
:top
s/255\.254\./254.255./g; s/255\.254$/254.255/
s/255\.253\./253.255./g; s/255\.253$/253.255/

# ... middle of the script omitted ...

$ tail -n 4 generated.sed
s/2\.1\./1.2./g; s/2\.1$/1.2/
s/2\.0\./0.2./g; s/2\.0$/0.2/
s/1\.0\./0.1./g; s/1\.0$/0.1/
ttop

Este enfoque codifica el período como el delimitador, que debe ser escapado, ya que de lo contrario sería "especial" para la sintaxis de expresión regular (permitiendo cualquier carácter).

Para generar una secuencia de comandos sed, este bucle hará:

#!/bin/bash

echo ':top'

for (( n = 255; n >= 0; n-- )); do
  for (( m = n - 1; m >= 0; m-- )); do
    printf '%s; %s\n' "s/$n\\.$m\\./$m.$n./g" "s/$n\\.$m\$/$m.$n/"
  done
done

echo 'ttop'

Redireccionar la salida de ese script a otro archivo, digamos sort-ips.sed.

Una ejecución de muestra podría verse así:

ip=$((RANDOM % 256)).$((RANDOM % 256)).$((RANDOM % 256)).$((RANDOM % 256))
printf '%s\n' "$ip" | sed -f sort-ips.sed

La siguiente variación en el script generador utiliza los marcadores de límite de palabras \<y \>elimina la necesidad de la segunda sustitución. Esto también reduce el tamaño del script generado de 1.3 MB a poco menos de 900 KB junto con una reducción considerable del tiempo de ejecución del sedmismo (a aproximadamente 50% -75% del original, dependiendo de qué sedimplementación se esté usando):

#!/bin/bash

echo ':top'

for (( n = 255; n >= 0; --n )); do
  for (( m = n - 1; m >= 0; --m )); do
      printf '%s\n' "s/\\<$n\\>\\.\\<$m\\>/$m.$n/g"
  done
done

echo 'ttop'

1
Una idea interesante, pero parece complicar un poco las cosas.
Matt

1
@ Matt Es un poco el punto. Ordenar cualquier cosa sedes ridículo, por eso es un desafío interesante.
Kusalananda

2

Aquí un golpe que adivina el delimitador por sí mismo:

#!/bin/bash

delimiter="${1//[[:digit:]]/}"
if echo $delimiter | grep -q "^\(.\)\1\+$"
then
  delimiter="${delimiter:0:1}"
  if [[ -z $(echo $1 | grep "^\([0-9]\+"$delimiter"\([0-9]\+\)*\)\+$") ]]
  then
    echo "You seem to have empty fields between the delimiters."
    exit 1
  fi
  if [[ './\' == *$delimiter* ]]
  then
    n=$( echo $1 | sed "s/\\"$delimiter"/\\n/g" | sort -n | tr '\n' ' ' | sed -e "s/\\s/\\"$delimiter"/g")
  else
    n=$( echo $1 | sed "s/"$delimiter"/\\n/g" | sort -n | tr '\n' ' ' | sed -e "s/\\s/"$delimiter"/g")
  fi
  echo ${n%$delimiter}
  exit 0
else
  echo "The string does not consist of digits separated by one unique delimiter."
  exit 1
fi

Puede que no sea muy eficiente ni limpio, pero funciona.

Utilizar como bash my_script.sh "00/00/18/29838/2".

Devuelve un error cuando el mismo delimitador no se usa de manera consistente o cuando dos o más delimitadores se suceden.

Si el delimitador utilizado es un carácter especial, se escapa (de lo contrario, seddevuelve un error).


Eso inspiró esto .
agc

2

Esta respuesta se basa en un malentendido de la Q., pero en algunos casos resulta ser correcta de todos modos. Si la entrada es números enteramente naturales y tiene un solo delimitador por línea (como con los datos de muestra en la Q), funciona correctamente. También manejará archivos con líneas que tienen cada uno su propio delimitador, que es un poco más de lo que se solicitó.

Esta función de shell reads de entrada estándar, utiliza la sustitución de parámetros POSIX para encontrar el delimitador específico en cada línea, (almacenado en $d), y utiliza trpara reemplazar $dcon una nueva línea \ny sortlos datos de esa línea, luego restaura los delimitadores originales de cada línea:

sdn() { while read x; do
            d="${x#${x%%[^0-9]*}}"   d="${d%%[0-9]*}"
            x=$(echo -n "$x" | tr "$d" '\n' | sort -g | tr '\n' "$d")
            echo ${x%?}
        done ; }

Aplicado a los datos dados en el OP :

printf "%s\n" "10 50 23 42" "10.1.200.42" "1,100,330,42" "400|500|404" | sdn

Salida:

10 23 42 50
1.10.42.200
1,42,100,330
400|404|500

El delimitador en cualquier línea será consistente; Las soluciones generales que permiten al usuario declarar el delimitador son impresionantes, pero las respuestas pueden asumir cualquier delimitador que tenga sentido para ellos (un solo carácter y no presente en los datos numéricos en sí).
Jeff Schaller

2

Para delimitadores arbitrarios:

perl -lne '
  @list = /\D+|\d+/g;
  @sorted = sort {$a <=> $b} grep /\d/, @list;
  for (@list) {$_ = shift@sorted if /\d/};
  print @list'

En una entrada como:

5,4,2,3
6|5,2|4
There are 10 numbers in those 3 lines

Da:

2,3,4,5
2|4,5|6
There are 3 numbers in those 10 lines

0

Esto debería manejar cualquier delimitador sin dígitos (0-9). Ejemplo:

x='1!4!3!5!2'; delim=$(echo "$x" | tr -d 0-9 | cut -b1); echo "$x" | tr "$delim" '\n' | sort -g | tr '\n' "$delim" | sed "s/$delim$/\n/"

Salida:

1!2!3!4!5

0

Con perl:

$ # -a to auto-split on whitespace, results in @F array
$ echo 'foo baz v22 aimed' | perl -lane 'print join " ", sort @F'
aimed baz foo v22
$ # {$a <=> $b} for numeric comparison, {$b <=> $a} will give descending order
$ echo '1,100,330,42' | perl -F, -lane 'print join ",", sort {$a <=> $b} @F'
1,42,100,330

Con ruby, que es algo similar aperl

$ # -a to auto-split on whitespace, results in $F array
$ # $F is sorted and then joined using the given string
$ echo 'foo baz v22 aimed' | ruby -lane 'print $F.sort * " "'
aimed baz foo v22

$ # (&:to_i) to convert string to integer
$ echo '1,100,330,42' | ruby -F, -lane 'print $F.sort_by(&:to_i) * ","'
1,42,100,330

$ echo '10.1.200.42' | ruby -F'\.' -lane 'print $F.sort_by(&:to_i) * "."'
1.10.42.200


Comando personalizado y pasar solo la cadena delimitador (no regex). Funcionará si la entrada también tiene datos flotantes

$ # by default join uses value of $,
$ sort_line(){ ruby -lne '$,=ENV["d"]; print $_.split($,).sort_by(&:to_f).join' ; }

$ s='103,14.5,30,24'
$ echo "$s" | d=',' sort_line
14.5,24,30,103
$ s='10.1.200.42'
$ echo "$s" | d='.' sort_line
1.10.42.200

$ # for file input
$ echo '123--87--23' > ip.txt
$ echo '3--12--435--8' >> ip.txt
$ d='--' sort_line <ip.txt
23--87--123
3--8--12--435


Comando personalizado para perl

$ sort_line(){ perl -lne '$d=$ENV{d}; print join $d, sort {$a <=> $b} split /\Q$d/' ; }
$ s='123^[]$87^[]$23'
$ echo "$s" | d='^[]$' sort_line 
23^[]$87^[]$123


Lecturas adicionales: ya tenía esta lista útil de frases de perl / ruby


0

La siguiente es una variación de la respuesta de Jeff en el sentido de que genera un sedscript que hará el tipo Bubble, pero es lo suficientemente diferente como para garantizar su propia respuesta.

La diferencia es que en lugar de generar expresiones regulares básicas O (n ^ 2), esto genera expresiones regulares extendidas O (n). El script resultante tendrá aproximadamente 15 KB de tamaño. El tiempo de ejecución del sedscript es en fracciones de segundo (se tarda un poco más en generar el script).

Está restringido a la clasificación de enteros positivos delimitados por puntos, pero no se limita al tamaño de los enteros (solo aumenta 255en el bucle principal) o al número de enteros. El delimitador se puede cambiar cambiando delim='.'el código.

Se me ha ocurrido entender bien las expresiones regulares, así que me iré describiendo los detalles para otro día.

#!/bin/bash

# This function creates a extended regular expression
# that matches a positive number less than the given parameter.
lt_pattern() {
    local n="$1"  # Our number.
    local -a res  # Our result, an array of regular expressions that we
                  # later join into a string.

    for (( i = 1; i < ${#n}; ++i )); do
        d=$(( ${n: -i:1} - 1 )) # The i:th digit of the number, from right to left, minus one.

        if (( d >= 0 )); then
            res+=( "$( printf '%d[0-%d][0-9]{%d}' "${n:0:-i}" "$d" "$(( i - 1 ))" )" )
        fi
    done

    d=${n:0:1} # The first digit of the number.
    if (( d > 1 )); then
        res+=( "$( printf '[1-%d][0-9]{%d}' "$(( d - 1 ))" "$(( ${#n} - 1 ))" )" )
    fi

    if (( n > 9 )); then
        # The number is 10 or larger.
        res+=( "$( printf '[0-9]{1,%d}' "$(( ${#n} - 1 ))" )" )
    fi

    if (( n == 1 )); then
        # The number is 1. The only thing smaller is zero.
        res+=( 0 )
    fi

    # Join our res array of expressions into a '|'-delimited string.
    ( IFS='|'; printf '%s\n' "${res[*]}" )
}

echo ':top'

delim='.'

for (( n = 255; n > 0; --n )); do
    printf 's/\\<%d\\>\\%s\\<(%s)\\>/\\1%s%d/g\n' \
        "$n" "$delim" "$( lt_pattern "$n" )" "$delim" "$n"
done

echo 'ttop'

El guión se verá así:

$ bash generator.sh >script.sed
$ head -n 5 script.sed
:top
s/\<255\>\.\<(25[0-4][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.255/g
s/\<254\>\.\<(25[0-3][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.254/g
s/\<253\>\.\<(25[0-2][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.253/g
s/\<252\>\.\<(25[0-1][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.252/g
$ tail -n 5 script.sed
s/\<4\>\.\<([1-3][0-9]{0})\>/\1.4/g
s/\<3\>\.\<([1-2][0-9]{0})\>/\1.3/g
s/\<2\>\.\<([1-1][0-9]{0})\>/\1.2/g
s/\<1\>\.\<(0)\>/\1.1/g
ttop

La idea detrás de las expresiones regulares generadas es la coincidencia de patrones para números que son menores que cada número entero; esos dos números estarían fuera de servicio, por lo que se intercambian. Las expresiones regulares se agrupan en varias opciones OR. Preste mucha atención a los rangos agregados a cada elemento, a veces lo son {0}, lo que significa que el elemento inmediatamente anterior debe omitirse de la búsqueda. Las opciones de expresiones regulares, de izquierda a derecha, coinciden con números que son más pequeños que el número dado por:

  • el lugar
  • el lugar de las decenas
  • el lugar de los cientos
  • (continúa según sea necesario, para números más grandes)
  • o por ser más pequeño en magnitud (número de dígitos)

Para deletrear un ejemplo, tome 101(con espacios adicionales para facilitar la lectura):

s/ \<101\> \. \<(10[0-0][0-9]{0} | [0-9]{1,2})\> / \1.101 /g

Aquí, la primera alternancia permite los números del 100 al 100; la segunda alternancia permite 0 a 99.

Otro ejemplo es 154:

s/ \<154\> \. \<(15[0-3][0-9]{0} | 1[0-4][0-9]{1} | [0-9]{1,2})\> / \1.154 /g

Aquí la primera opción permite 150 a 153; el segundo permite de 100 a 149, y el último permite de 0 a 99.

Prueba cuatro veces en un bucle:

for test_run in {1..4}; do
    nums=$(( RANDOM%256 )).$(( RANDOM%256 )).$(( RANDOM%256 )).$(( RANDOM%256 ))
    printf 'nums=%s\n' "$nums"
    sed -E -f script.sed <<<"$nums"
done

Salida:

nums=90.19.146.232
19.90.146.232
nums=8.226.70.154
8.70.154.226
nums=1.64.96.143
1.64.96.143
nums=67.6.203.56
6.56.67.203

-2

División de entrada en varias líneas

Utilizando tr, puede dividir la entrada utilizando un delimitador arbitrario en varias líneas.

Esta entrada puede ejecutarse sort(usando -nsi la entrada es numérica).

Si desea retener el delimitador en la salida, puede volver a usarlo trpara volver a agregar el delimitador.

por ejemplo, usar el espacio como delimitador

cat input.txt | tr " " "\n" | sort -n | tr "\n" " "

entrada: 1 2 4 1 4 32 18 3 salida:1 1 2 3 4 4 18 32


Puede asumir elementos numéricos de forma segura y sí: el delimitador debe reemplazarse.
Jeff Schaller
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.