Come posso ordinare numericamente una singola riga di articoli delimitati?


11

Ho una linea (o molte righe) di numeri che sono delimitati da un carattere arbitrario. Quali strumenti UNIX posso usare per ordinare numericamente gli elementi di ogni riga, mantenendo il delimitatore?

Esempi inclusi:

  • elenco di numeri; input 10 50 23 42:; smistato:10 23 42 50
  • Indirizzo IP; input 10.1.200.42:; smistato:1.10.42.200
  • CSV; input 1,100,330,42:; smistato:1,42,100,330
  • tubo delimitato; input 400|500|404:; smistato:400|404|500

Poiché il delimitatore è arbitrario, sentiti libero di fornire (o estendere) una risposta usando un delimitatore a carattere singolo di tua scelta.


8
dovresti postarlo su codegolf :)
ivanivan,

1
c'è una domanda simile anche qui vorrei aggiungere il suo link Alfabetizzare le parole nei nomi dei file usando sort?
αғsнιη,

Solo un suggerimento che cutsupporta delimitatori arbitrari con la sua -dopzione.
Oleg Lobachev

Si prega di chiarire se questi quattro esempi di DSV si trovano nello stesso file o se sono campioni da quattro file diversi.
agc,

2
Vedendo alcuni degli altri commenti: il delimitatore è arbitrario, ma verrebbe utilizzato coerentemente nell'input. Supponiamo che l'intelligence da parte del produttore dei dati non utilizzi le virgole come delimitatore e nei dati (ad esempio, 4,325 comma 55 comma 42,430non si verificherebbe, né 1.5 period 4.2).
Jeff Schaller

Risposte:


12

Puoi farlo con:

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

sostituire i punti . con il delimitatore.
aggiungi -ual sortcomando sopra per rimuovere i duplicati.


o con gawk( GNU awk ) possiamo elaborare molte linee mentre anche quanto sopra può essere esteso anche:

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

sostituire *come separatore di campo SEP='*'con il delimitatore .


Note:
potrebbe essere necessario utilizzare l' -g, --general-numeric-sortopzione sortinvece di -n, --numeric-sortgestire qualsiasi classe di numeri (intero, float, scientifico, esadecimale, ecc.).

$ 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

In awknessun cambiamento bisogno, ancora sarà la manipolazione quelli.


10

Utilizzando perlc'è una versione ovvia; dividere i dati, ordinarli, ricollegarli nuovamente.

Il delimitatore deve essere elencato due volte (una volta nella splite una volta nella join)

ad es. per a ,

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

Così

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

Dato che splitè una regex, il personaggio potrebbe aver bisogno di citare:

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

Utilizzando le opzioni -ae -F, è possibile rimuovere la divisione. Con il -pciclo, come in precedenza e impostare i risultati su $_, che stamperà automaticamente:

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

4
puoi usare l' -lopzione invece di usare chomp. Ciò aggiunge anche la nuova riga al momento della stampa. Vedi anche -a(con -F) per la parte di divisione.
Stéphane Chazelas,

1
Con -le -F, è ancora più bello:perl -F'/\./' -le 'print join(".", sort {$a <=> $b} @F)'
muru

@ StéphaneChazelas grazie per l' -lopzione; L'avevo perso!
Stephen Harris,

1
@muru Inizialmente non ho usato il -Fflag perché non funziona correttamente in tutte le versioni (es. la tua linea in CentOS 7 - perl 5.16.3 - restituisce un output vuoto, sebbene funzioni bene su Debian 9). Ma combinato con -pesso dà un risultato leggermente più piccolo, quindi l'ho aggiunto come alternativa alla risposta. mostrando come -Fpuò essere usato. Grazie!
Stephen Harris,

2
@StephenHarris è perché le versioni più recenti di perl aggiungono automaticamente -ae -nopzioni quando -Fviene utilizzato e -nquando -aviene utilizzato ... quindi basta passare -lea-lane
Sundeep

4

Usando Python e un'idea simile alla risposta di 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>

Quindi qualcosa del tipo:

$ 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

Purtroppo dover eseguire manualmente l'I / O lo rende molto meno elegante della versione Perl.



3

Conchiglia

Il caricamento di una lingua di livello superiore richiede tempo.
Per poche righe, la shell stessa potrebbe essere una soluzione.
Possiamo usare il comando esterno sorte il comando tr. Uno è abbastanza efficiente nelle linee di smistamento e l'altro è efficace per convertire un delimitatore in newline:

#!/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'

Questo bisogno bash a causa dell'uso di <<<solo. Se questo viene sostituito con un here-doc, la soluzione è valida per posix.
Questo è in grado di ordinare i campi con tabulazioni, spazi o caratteri shell glob ( *, ?, [). Non newline perché ogni riga viene ordinata.

Passare <<<"$2"a <"$2"elaborare i nomi dei file e chiamarlo come:

shsort '.'    infile

Il delimitatore è lo stesso per l'intero file. Se questa è una limitazione, potrebbe essere migliorata.

Tuttavia, l'elaborazione di un file con solo 6000 righe richiede 15 secondi. In verità, la shell non è lo strumento migliore per elaborare i file.

awk

Per più di alcune righe (più di qualche decina) è meglio usare un vero linguaggio di programmazione. Una soluzione awk potrebbe essere:

#!/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

Ciò richiede solo 0,2 secondi per lo stesso file di 6000 righe sopra menzionato.

Comprendi che i <"$2"file for potrebbero essere cambiati nuovamente <<<"$2"per le righe all'interno delle variabili della shell.

Perl

La soluzione più veloce è il 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'

Se si desidera ordinare una modifica del file <<<"$a"in semplicemente "$a"e aggiungere -ialle opzioni perl per rendere "in atto" l'edizione del file:

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

perlsort '.' infile; exit

2

Utilizzo sedper ordinare gli ottetti di un indirizzo IP

sednon ha una sortfunzione integrata, ma se i tuoi dati sono sufficientemente limitati nell'intervallo (come con gli indirizzi IP), puoi generare uno script sed che implementa manualmente un semplice ordinamento a bolle . Il meccanismo di base è cercare numeri adiacenti fuori servizio. Se i numeri sono fuori servizio, scambiarli.

Lo sedscript stesso contiene due comandi di ricerca e scambio per ciascuna coppia di numeri fuori ordine: uno per le prime due coppie di ottetti (forzando la presenza di un delimitatore finale per contrassegnare la fine del terzo ottetto), e un secondo per la terza coppia di ottetti (termina con EOL). Se si verificano degli swap, il programma si dirama in cima allo script, cercando numeri fuori servizio. Altrimenti, esce.

Lo script generato è, in 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

Questo approccio codifica il periodo come delimitatore, che deve essere evitato, altrimenti sarebbe "speciale" per la sintassi delle espressioni regolari (permettendo qualsiasi carattere).

Per generare un tale script sed, questo ciclo farà:

#!/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'

Reindirizza l'output di quello script su un altro file, ad esempio sort-ips.sed.

Un'esecuzione di esempio potrebbe quindi apparire come:

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

La seguente variazione sullo script di generazione utilizza i marcatori di confine di parola \<e \>per eliminare la necessità della seconda sostituzione. Ciò riduce anche le dimensioni dello script generato da 1,3 MB a poco meno di 900 KB, riducendo notevolmente il tempo di esecuzione dello sedstesso (a circa il 50% -75% dell'originale, a seconda seddell'implementazione utilizzata):

#!/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
Un'idea interessante, ma sembra complicare un po 'le cose.
Matt

1
@Matt È un po 'il punto. Ordinare qualcosa con sedè ridicolo, motivo per cui è una sfida interessante.
Kusalananda

2

Ecco alcuni bash che indovina il delimitatore da solo:

#!/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

Potrebbe non essere molto efficiente né pulito ma funziona.

Usa come bash my_script.sh "00/00/18/29838/2".

Restituisce un errore quando lo stesso delimitatore non viene utilizzato in modo coerente o quando due o più delimitatori si susseguono.

Se il delimitatore usato è un carattere speciale, viene evaso (altrimenti sedrestituisce un errore).


Ciò ha ispirato questo .
agc,

2

Questa risposta si basa su un fraintendimento del Q., ma in alcuni casi sembra comunque corretto. Se l'input è costituito da numeri completamente naturali e ha un solo delimitatore per riga (come per i dati di esempio nella Q.), funziona correttamente. Gestirà anche i file con righe che hanno ciascuna il proprio delimitatore, che è un po 'più di quello che è stato richiesto.

Questa funzione della shell readproviene dall'input standard, utilizza la sostituzione dei parametri POSIX per trovare il delimitatore specifico su ciascuna riga, (memorizzato in $d), e utilizza trper sostituire $dcon una nuova riga \ne sorti dati di quella riga, quindi ripristina i delimitatori originali di ogni riga:

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 ; }

Applicato ai dati forniti nel PO :

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

Produzione:

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

Il delimitatore in qualsiasi riga sarà coerente; le soluzioni generali che consentono all'utente di dichiarare il delimitatore sono fantastiche, ma le risposte possono assumere qualsiasi delimitatore che abbia senso per loro (carattere singolo e non presente nei dati numerici stessi).
Jeff Schaller

2

Per delimitatori arbitrari:

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

Su un input come:

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

Dà:

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

0

Questo dovrebbe gestire qualsiasi delimitatore senza cifre (0-9). Esempio:

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/"

Produzione:

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, che è in qualche modo simile 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 personalizzato e passaggio solo della stringa del delimitatore (non regex). Funzionerà se l'input ha anche dati mobili

$ # 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 personalizzato per 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


Ulteriori letture - Avevo già questo utile elenco di linee guida perl / ruby


0

Quella che segue è una variazione della risposta di Jeff, nel senso che genera uno sedscript che farà l'ordinamento Bubble, ma è sufficientemente diverso da giustificare la propria risposta.

La differenza è che invece di generare O (n ^ 2) espressioni regolari di base, questo genera O (n) espressioni regolari estese. Lo script risultante avrà una dimensione di circa 15 KB. Il tempo di esecuzione dello sedscript è in frazioni di secondo (richiede un po 'più di tempo per generare lo script).

È limitato all'ordinamento di numeri interi positivi delimitati da punti, ma non è limitato alla dimensione degli interi (basta aumentare 255nel ciclo principale) o al numero di numeri interi. Il delimitatore può essere modificato modificando delim='.'il codice.

Mi ha fatto la testa per ottenere le espressioni regolari giuste, quindi lascerò descrivendo i dettagli per un altro giorno.

#!/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'

La sceneggiatura avrà un aspetto simile al seguente:

$ 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

L'idea alla base delle espressioni regolari generate è quella di modellare la corrispondenza per i numeri inferiori a ciascun numero intero; quei due numeri sarebbero fuori servizio e quindi vengono scambiati. Le espressioni regolari sono raggruppate in diverse opzioni OR. Presta molta attenzione agli intervalli aggiunti a ciascun articolo, a volte lo sono {0}, il che significa che l'articolo immediatamente precedente deve essere omesso dalla ricerca. Le opzioni di regex, da sinistra a destra, corrispondono a numeri più piccoli del numero indicato da:

  • quelli posto
  • il posto delle decine
  • il centinaio di posti
  • (continua secondo necessità, per numeri maggiori)
  • o essendo di dimensioni inferiori (numero di cifre)

Per scrivere un esempio, prendi 101(con spazi aggiuntivi per la leggibilità):

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

Qui, la prima alternanza consente i numeri da 100 a 100; la seconda alternanza consente da 0 a 99.

Un altro esempio è 154:

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

Qui la prima opzione consente da 150 a 153; il secondo consente da 100 a 149 e l'ultimo consente da 0 a 99.

Test quattro volte in un ciclo:

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

Produzione:

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

Suddivisione dell'input in più righe

Usando tr, puoi dividere l'input usando un delimitatore arbitrario in più righe.

Questo input può quindi essere eseguito sort(utilizzando -nse l'input è numerico).

Se si desidera conservare il delimitatore nell'output, è possibile utilizzare trnuovamente per aggiungere nuovamente il delimitatore.

ad es. usare lo spazio come delimitatore

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

input: 1 2 4 1 4 32 18 3 output:1 1 2 3 4 4 18 32


Puoi assumere in sicurezza elementi numerici e sì: il delimitatore deve essere sostituito.
Jeff Schaller
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.