Como posso ordenar numericamente uma única linha de itens delimitados?

11

Eu tenho uma linha (ou muitas linhas) de números que são delimitados por um caractere arbitrário. Quais ferramentas UNIX posso usar para classificar os itens de cada linha numericamente, mantendo o delimitador?

Exemplos incluem:

  • lista de números; entrada 10 50 23 42:; classificados:10 23 42 50
  • Endereço de IP; entrada 10.1.200.42:; classificados:1.10.42.200
  • CSV; entrada 1,100,330,42:; classificados:1,42,100,330
  • delimitado por tubo; entrada 400|500|404:; classificados:400|404|500

Como o delimitador é arbitrário, fique à vontade para fornecer (ou estender) uma Resposta usando um delimitador de um caractere de sua escolha.

Jeff Schaller
fonte
8
você deve postar sobre isso em codegolf :)
ivanivan
1
existe uma pergunta semelhante também aqui gostaria de adicionar seu link Alfabetizar palavras nos nomes de arquivos usando sort?
αғsнιη
Apenas uma dica que cutsuporta delimitadores arbitrários com sua -dopção.
amigos estão dizendo sobre oleg lobachev
Esclareça se esses quatro exemplos de DSVs estão no mesmo arquivo ou são amostras de quatro arquivos diferentes.
agc
2
Vendo alguns dos outros comentários: o delimitador é arbitrário, mas seria usado consistentemente na entrada. Suponha inteligência por parte do produtor de dados para que eles não usem vírgulas como delimitador e nos dados (por exemplo, 4,325 comma 55 comma 42,430não ocorreriam nem 1.5 period 4.2).
Jeff Schaller

Respostas:

12

Você pode conseguir isso com:

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

substitua pontos . pelo seu delimitador.
adicione -uao sortcomando acima para remover as duplicatas.


ou com gawk( GNU awk ), podemos processar muitas linhas, enquanto o acima também pode ser estendido:

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

substitua *como o separador de campos SEP='*'pelo seu delimitador .


Notas:
Pode ser necessário usar a -g, --general-numeric-sortopção, em sortvez de, -n, --numeric-sortpara lidar com qualquer classe de números (número inteiro, número flutuante, 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

Em awknenhuma necessidade de mudança, ele ainda vai lidar com isso.

αғsнιη
fonte
10

Usando perlhá uma versão óbvia; divida os dados, classifique-os e junte-os novamente.

O delimitador precisa ser listado duas vezes (uma vez na splite uma vez na join)

por exemplo, para um ,

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

então

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

Como o splité um regex, o personagem pode precisar de citação:

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

Usando as opções -ae -F, é possível remover a divisão. Com o -ploop, como antes, e defina os resultados como $_, que serão impressos automaticamente:

perl -F'/\./' -aple '$_=join(".", sort {$a <=> $b} @F)'
Stephen Harris
fonte
4
você pode usar a -lopção em vez de usar chomp. Isso também adiciona de volta a nova linha na impressão. Veja também -a(com -F) a parte de divisão.
Stéphane Chazelas
1
Com -le -F, é ainda melhor:perl -F'/\./' -le 'print join(".", sort {$a <=> $b} @F)'
Muru
@ StéphaneChazelas obrigado pela -lopção; Eu tinha perdido isso!
Stephen Harris
1
@muru Eu não usei a -Fflag originalmente porque ela não funciona corretamente em todas as versões (por exemplo, sua linha no CentOS 7 - perl 5.16.3 - retorna saída em branco, embora funcione bem no Debian 9). Mas, combinada a -pela, fornece um resultado um pouco menor, então eu adicionei isso como uma alternativa à resposta. mostrando como -Fpode ser usado. Obrigado!
Stephen Harris
2
@StephenHarris Isso porque versões mais recentes do perl adiciona automaticamente -ae -nopções quando -Fé utilizado e -nquando -aé usado ... então apenas mudar -lepara-lane
Sundeep
4

Usando Python e uma idéia semelhante à da resposta 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>

Então, 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

Infelizmente, ter que fazer a E / S manualmente torna isso muito menos elegante que a versão Perl.

muru
fonte
3

Concha

Carregar um idioma de nível superior leva tempo.
Por algumas linhas, o próprio shell pode ser uma solução.
Podemos usar o comando externo sorte do comando tr. Uma é bastante eficiente na classificação de linhas e a outra é eficaz para converter um delimitador em novas linhas:

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

Isso precisa do bash por causa do uso de <<<only. Se isso for substituído por um documento aqui, a solução é válida para o posix.
Este é capaz de classificar campos com tabulações, espaços ou caracteres shell glob ( *, ?, [). Não novas linhas porque cada linha está sendo classificada.

Mude <<<"$2"para <"$2"para processar nomes de arquivos e chame-o como:

shsort '.'    infile

O delimitador é o mesmo para o arquivo inteiro. Se isso é uma limitação, pode ser melhorado.

No entanto, um arquivo com apenas 6000 linhas leva 15 segundos para processar. Na verdade, o shell não é a melhor ferramenta para processar arquivos.

Awk

Por mais do que algumas linhas (mais do que algumas dez), é melhor usar uma linguagem de programação real. Uma solução awk pode 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

O que leva apenas 0,2 segundos para o mesmo arquivo de 6000 linhas mencionado acima.

Entenda que os <"$2"arquivos for podem ser alterados novamente <<<"$2"para linhas dentro de variáveis ​​do shell.

Perl

A solução mais rápida é 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 você deseja classificar um arquivo, mude <<<"$a"para simplesmente "$a"e adicione -iàs opções perl para tornar a edição do arquivo "no lugar":

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

perlsort '.' infile; exit
NotAnUnixNazi
fonte
2

Usando sedpara classificar octetos de um endereço IP

sednão possui uma sortfunção interna, mas se seus dados estiverem suficientemente restritos no intervalo (como nos endereços IP), você poderá gerar um script sed que implemente manualmente uma classificação de bolha simples . O mecanismo básico é procurar números adjacentes que estejam fora de ordem. Se os números estiverem fora de ordem, troque-os.

O sedscript em si contém dois comandos de busca e troca para cada par de números fora de ordem: um para os dois primeiros pares de octetos (forçando a presença de um delimitador à direita para marcar o final do terceiro octeto) e um segundo para o terceiro par de octetos (final com EOL). Se ocorrerem trocas, o programa se ramifica na parte superior do script, procurando números que estão fora de ordem. Caso contrário, ele sai.

O script gerado é, em 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

Essa abordagem codifica o período como delimitador, que precisa ser escapado, caso contrário, seria "especial" para a sintaxe da expressão regular (permitindo qualquer caractere).

Para gerar um script sed, esse loop 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'

Redirecione a saída desse script para outro arquivo, digamos sort-ips.sed.

Uma amostra de execução pode parecer com:

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

A seguinte variação no script de geração usa os marcadores de limite de palavras \<e \>para se livrar da necessidade da segunda substituição. Isso também reduz o tamanho do script gerado de 1,3 MB para pouco menos de 900 KB, além de reduzir bastante o tempo de execução em sedsi (para cerca de 50% a 75% do original, dependendo de qual sedimplementação está sendo usada):

#!/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'
Jeff Schaller
fonte
1
Uma ideia interessante, mas parece complicar um pouco demais as coisas.
Matt
1
@ Matt É meio que o ponto. Classificar qualquer coisa com sedé ridículo, e é por isso que é um desafio interessante.
Kusalananda
2

Aqui, uma festança que adivinha o delimitador por si só:

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

Pode não ser muito eficiente nem limpo, mas funciona.

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

Retorna um erro quando o mesmo delimitador não é usado consistentemente ou quando dois ou mais delimitadores se seguem.

Se o delimitador usado for um caractere especial, ele será escapado (caso contrário, sedretornará um erro).

jkd
fonte
Isso inspirou isso .
9138
2

Essa resposta é baseada em um mal-entendido do Q., mas, em alguns casos, está correto de qualquer maneira. Se a entrada for números totalmente naturais e tiver apenas um delimitador por linha (como nos dados de amostra no Q.), ela funcionará corretamente. Ele também manipula arquivos com linhas que cada um tem seu próprio delimitador, o que é um pouco mais do que o solicitado.

Esta função shell reads da entrada padrão, utiliza o parâmetro de substituição POSIX para encontrar o delimitador específico em cada linha, (armazenado em $d), e usos trpara substituir $dcom uma nova linha \ne sorts dados dessa linha, em seguida, restaura delimitadores originais de cada linha:

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 aos dados fornecidos no OP :

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

Resultado:

10 23 42 50
1.10.42.200
1,42,100,330
400|404|500
agc
fonte
O delimitador em qualquer linha será consistente; soluções gerais que permitem ao usuário declarar o delimitador são impressionantes, mas as respostas podem assumir qualquer delimitador que faça sentido para ele (caractere único e não presente nos próprios dados numéricos).
Jeff Schaller
2

Para delimitadores arbitrários:

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

Em uma entrada como:

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
Stéphane Chazelas
fonte
0

Isso deve lidar com qualquer delimitador sem dígito (0-9). Exemplo:

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

Resultado:

1!2!3!4!5
Alexander
fonte
0

Com 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

With ruby, que é um pouco semelhante 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 e passagem apenas da string delimitadora (não regex). Funcionará se a entrada também tiver dados flutuantes

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


Outras leituras - Eu já tinha essa lista útil de one-liners perl / ruby

Sundeep
fonte
0

A seguir, é apresentada uma variação da resposta de Jeff, no sentido de que ele gera um sedscript que será do tipo Bubble, mas é suficientemente diferente para garantir sua própria resposta.

A diferença é que, em vez de gerar O (n ^ 2) expressões regulares básicas, isso gera O (n) expressões regulares estendidas. O script resultante terá cerca de 15 KB. O tempo de execução do sedscript está em frações de segundo (demora um pouco mais para gerar o script).

Ele é restrito à classificação de números inteiros positivos delimitados por pontos, mas não se limita ao tamanho dos números inteiros (apenas aumente 255no loop principal) ou ao número de números inteiros. O delimitador pode ser alterado alterando delim='.'o código.

Está pronto para acertar as expressões regulares, então deixarei de descrever os detalhes por mais um dia.

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

O script será mais ou menos assim:

$ 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

A idéia por trás das expressões regulares geradas é padronizar a correspondência para números menores que cada número inteiro; esses dois números estão fora de ordem e, portanto, são trocados. As expressões regulares são agrupadas em várias opções OR. Preste muita atenção aos intervalos anexados a cada item, às vezes {0}, o que significa que o item imediatamente anterior deve ser omitido na pesquisa. As opções de regex, da esquerda para a direita, correspondem a números menores que o número especificado por:

  • os lugares
  • o lugar das dezenas
  • as centenas colocam
  • (continuação conforme necessário, para números maiores)
  • ou sendo menor em magnitude (número de dígitos)

Para explicar um exemplo, use 101(com espaços adicionais para facilitar a leitura):

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

Aqui, a primeira alternância permite os números 100 a 100; a segunda alternância permite de 0 a 99.

Outro exemplo é 154:

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

Aqui a primeira opção permite 150 a 153; o segundo permite 100 a 149 e o último permite 0 a 99.

Testando quatro vezes em um loop:

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

Resultado:

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
Kusalananda
fonte
-2

Dividindo a entrada em várias linhas

Usando tr, você pode dividir a entrada usando um delimitador arbitrário em várias linhas.

Essa entrada pode ser executada sort(usando -nse a entrada for numérica).

Se desejar reter o delimitador na saída, você poderá usá-lo trnovamente para adicionar novamente o delimitador.

por exemplo, usando o espaço como delimitador

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

entrada: 1 2 4 1 4 32 18 3 saída:1 1 2 3 4 4 18 32

Matt
fonte
Você pode assumir com segurança itens numéricos e sim: o delimitador deve ser substituído.
Jeff Schaller