Como eu posso numerar uma única linha de itens delimitados?

10

Eu tenho uma linha (ou muitas linhas) de números que são delimitados por um caractere arbitrário. Quais ferramentas do 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 ; classificado: 10 23 42 50
  • endereço IP; entrada: 10.1.200.42 ; classificado: 1.10.42.200
  • CSV; entrada: 1,100,330,42 ; classificado: 1,42,100,330
  • delimitado por canal; entrada: 400|500|404 ; classificado: 400|404|500

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

    
por Jeff Schaller 07.04.2018 / 02:58

13 respostas

11

Você pode conseguir isso com:

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

substitua pontos . pelo seu delimitador.
adicione -u ao comando sort acima para remover as duplicatas.

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

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 em SEP='*' com o seu delimitador .

Notas:
Você pode precisar usar a opção -g, --general-numeric-sort de sort em vez de -n, --numeric-sort para lidar com qualquer classe de números (inteiro, 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 awk não precisa mudar, ele ainda irá lidar com isso.

    
por 07.04.2018 / 03:27
9

Usando perl , há uma versão óbvia; dividir os dados, classificá-los, juntá-los novamente.

O delimitador precisa ser listado duas vezes (uma vez em split e uma vez em 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 caractere 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 -a e -F , é possível remover a divisão.  Com o -p loop, como antes, e defina os resultados para $_ , que serão impressos automaticamente:

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

Usando o Python e uma ideia semelhante à 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 o I / O manualmente torna isso muito menos elegante do que a versão Perl.

    
por 07.04.2018 / 04:02
3

Script de bash:

#!/usr/bin/env bash

join_by(){ local IFS="$1"; shift; echo "$*"; }

IFS="$1" read -r -a tokens_array <<< "$2"
IFS=$'\n' sorted=($(sort -n <<<"${tokens_array[*]}"))
join_by "$1" "${sorted[@]}"

Exemplo:

$ ./sort_delimited_string.sh "." "192.168.0.1"
0.1.168.192

Baseado em

por 07.04.2018 / 08:14
3

Shell

Carregar um idioma de nível superior leva tempo.
Para algumas linhas, o próprio shell pode ser uma solução.
Podemos usar o comando externo sort e o comando tr . Um é bastante eficiente na classificação de linhas e o outro é 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 bater por causa do uso de <<< apenas. Se isso for substituído por um aqui-doc, a solução é válida para posix.
Isso é capaz de classificar campos com tabulações, espaços ou caracteres de globalização ( * , ? , [ ). Não são novas linhas porque cada linha está sendo classificada.

Altere <<<"$2" para <"$2" para processar nomes de arquivos e chame 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 ser processado. Na verdade, o shell não é a melhor ferramenta para processar arquivos.

Awk

Por mais de algumas linhas (mais do que alguns 10), é melhor usar uma linguagem de programação real. Uma solução awk poderia 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 <"$2" para arquivos pode ser alterado de volta para <<<"$2" para linhas dentro de variáveis do shell.

Perl

A solução mais rápida é o 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ê quiser classificar uma alteração de arquivo <<<"$a" para simplesmente "$a" e adicionar -i às opções perl para tornar a edição de arquivo "no lugar":

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

perlsort '.' infile; exit
    
por 07.04.2018 / 07:19
2

Usando sed para classificar octetos de um endereço IP

sed não tem uma função sort integrada, mas se os dados estiverem suficientemente restringidos no intervalo (como com endereços IP), você pode gerar um script sed que implemente manualmente um simples bubble sort . O mecanismo básico é procurar números adjacentes que estejam fora de ordem. Se os números estiverem fora de ordem, troque-os.

O script sed contém dois comandos de pesquisa e troca para cada par de números fora de ordem: um para os dois primeiros pares de octetos (forçando um delimitador final a estar presente para marcar o fim do terceiro octeto) e um segundo para o terceiro par de octetos (final com EOL). Se ocorrerem swaps, o programa se ramifica para o início do script, procurando por números fora de ordem. Caso contrário, 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 duramente o período como o 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 serve:

#!/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 corrida poderia ser assim:

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 palavra \< e \> para eliminar a 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 do próprio sed (para cerca de 50% -75% do original, dependendo de qual sed está sendo implementada usado):

#!/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'
    
por 07.04.2018 / 12:07
2

Aqui alguns bash que adivinha o delimitador por si só:

#!/bin/bash

delimiter="${1//[[:digit:]]/}"
if echo $delimiter | grep -q "^\(.\)\+$"
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á salvo (caso contrário, sed retornará um erro).

    
por 07.04.2018 / 13:10
2

Esta resposta é baseada em um mal entendido do Q., mas em alguns casos acontece de estar correto de qualquer forma. Se a entrada for totalmente números naturais e tiver apenas um delimitador por linha, (como acontece com os dados de amostra no Q.), funciona corretamente. Ele também lida com arquivos com linhas que cada um tem seu próprio delimitador, o que é um pouco mais do que o pedido.

Esta função do shell read s da entrada padrão, usa substituição do parâmetro POSIX para encontrar o delimitador específico em cada linha (armazenada em $d ) e usa tr para substituir $d com uma nova linha \n e sort s os dados dessa linha e restaura os 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

Saída:

10 23 42 50
1.10.42.200
1,42,100,330
400|404|500
    
por 09.04.2018 / 05:52
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
    
por 16.04.2018 / 08:15
0

Isso deve tratar de qualquer delimitador não-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/"

Saída:

1!2!3!4!5
    
por 14.04.2018 / 10:33
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

Com ruby , que é um pouco semelhante a perl

$ # -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 customizado e passando apenas a cadeia do delimitador (não regex). Funcionará se a entrada tiver dados flutuantes também

$ # 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 customizado 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 esta útil lista de one-liners de perl / ruby

por 07.04.2018 / 07:29
0

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

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

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

Fiz a minha cabeça para obter as expressões regulares corretamente, então vou deixar de descrever os detalhes para outro 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á parecido com isto:

$ 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})\>/.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})\>/.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})\>/.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})\>/.252/g
$ tail -n 5 script.sed
s/\<4\>\.\<([1-3][0-9]{0})\>/.4/g
s/\<3\>\.\<([1-2][0-9]{0})\>/.3/g
s/\<2\>\.\<([1-1][0-9]{0})\>/.2/g
s/\<1\>\.\<(0)\>/.1/g
ttop

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

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

Para soletrar um exemplo, use 101 (com espaços adicionais para legibilidade):

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

Aqui, a primeira alternância permite os números 100 a 100; a segunda alternação permite 0 a 99.

Outro exemplo é 154 :

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

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

Teste 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

Saída:

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
    
por 07.04.2018 / 22:04
-2

Dividir a entrada em várias linhas

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

Esta entrada pode então ser executada através de sort (usando -n se a entrada for numérica).

Se você deseja manter o delimitador na saída, você pode usar tr novamente para adicionar o delimitador.

por exemplo. usando o espaço como um 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

    
por 07.04.2018 / 03:29