Comment trier numériquement une seule ligne d'éléments délimités?

11

J'ai une ligne (ou plusieurs lignes) de nombres délimités par un caractère arbitraire. Quels outils UNIX puis-je utiliser pour trier les éléments de chaque ligne numériquement, en conservant le délimiteur?

Les exemples comprennent:

  • liste des numéros; entrée 10 50 23 42:; trié:10 23 42 50
  • Adresse IP; entrée 10.1.200.42:; trié:1.10.42.200
  • CSV; entrée 1,100,330,42:; trié:1,42,100,330
  • délimité par des tuyaux; entrée 400|500|404:; trié:400|404|500

Étant donné que le délimiteur est arbitraire, n'hésitez pas à fournir (ou étendre) une réponse en utilisant un délimiteur à un caractère de votre choix.

Jeff Schaller
la source
8
vous devriez le poster sur codegolf :)
ivanivan
1
il y a une question similaire ici aussi je voudrais ajouter son lien alphabétiser les mots dans les noms de fichiers en utilisant le tri?
αғsнιη
Juste un indice qui cutprend en charge les délimiteurs arbitraires avec son -doption.
Oleg Lobachev
Veuillez préciser si ces quatre exemples de DSV se trouvent dans le même fichier ou s'il s'agit d'échantillons de quatre fichiers différents.
agc
2
Voir certains des autres commentaires: le délimiteur est arbitraire, mais serait utilisé de manière cohérente dans l'entrée. Supposons une intelligence de la part du producteur de données de sorte qu'il n'utilise pas de virgules comme délimiteur et dans les données (par exemple, 4,325 comma 55 comma 42,430ne se produira pas, ni 1.5 period 4.2).
Jeff Schaller

Réponses:

12

Vous pouvez y parvenir avec:

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

remplacez les points . par votre délimiteur.
ajoutez -uà la sortcommande ci-dessus pour supprimer les doublons.


ou avec gawk( GNU awk ), nous pouvons traiter de nombreuses lignes tandis que celles ci-dessus peuvent également être étendues:

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

remplacer *comme séparateur de champ SEP='*'par votre délimiteur .


Remarques:
Vous devrez peut-être utiliser l' -g, --general-numeric-sortoption de sortau lieu de -n, --numeric-sortpour gérer n'importe quelle classe de nombres (entier, flottant, scientifique, hexadécimal, 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 awkaucun besoin de changement, il s'en chargera toujours.

αғsнιη
la source
10

En utilisant perlil y a une version évidente; diviser les données, les trier, les rejoindre à nouveau.

Le délimiteur doit être répertorié deux fois (une fois dans splitet une fois dans join)

par exemple pour un ,

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

Donc

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

Puisque le splitest une expression régulière, le personnage peut avoir besoin de citer:

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

En utilisant les options -aet -F, il est possible de supprimer le fractionnement. Avec la -pboucle, comme précédemment et définissez les résultats sur $_, qui s'imprimeront automatiquement:

perl -F'/\./' -aple '$_=join(".", sort {$a <=> $b} @F)'
Stephen Harris
la source
4
vous pouvez utiliser l' -loption au lieu d'utiliser chomp. Cela ajoute également la nouvelle ligne lors de l'impression. Voir aussi -a(avec -F) pour la partie de fendage.
Stéphane Chazelas
1
Avec -let -F, c'est encore plus agréable:perl -F'/\./' -le 'print join(".", sort {$a <=> $b} @F)'
muru le
@ StéphaneChazelas merci pour l' -loption; J'avais raté ça!
Stephen Harris
1
@muru Je n'ai pas utilisé l' -Findicateur à l'origine car il ne fonctionne pas correctement dans toutes les versions (par exemple, votre ligne dans CentOS 7 - perl 5.16.3 - renvoie une sortie vierge, bien que cela fonctionne très bien sur Debian 9). Mais combiné avec -pcela donne un résultat légèrement plus petit, j'ai donc ajouté cela comme alternative à la réponse. montrant comment -Fles utiliser. Merci!
Stephen Harris
2
@StephenHarris c'est parce que les nouvelles versions de perl ajoutent -aet -noptions automatiquement quand -Fest utilisé et -nquand -aest utilisé ... alors changez simplement -leen-lane
Sundeep
4

Utiliser Python et une idée similaire à celle de la réponse 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>

Donc quelque chose comme:

$ 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

Malheureusement, devoir faire les E / S manuellement rend cela beaucoup moins élégant que la version Perl.

muru
la source
3

coquille

Le chargement d'une langue de niveau supérieur prend du temps.
Pour quelques lignes, le shell lui-même peut être une solution.
Nous pouvons utiliser la commande externe sortet la commande tr. L'une est assez efficace pour trier les lignes et l'autre est efficace pour convertir un délimiteur en nouvelles lignes:

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

Ce besoin bash en raison de l'utilisation de <<<seulement. S'il est remplacé par un here-doc, la solution est valable pour posix.
Ceci est en mesure de trier les champs avec des onglets, des espaces ou des caractères glob shell ( *, ?, [). Pas de nouvelles lignes car chaque ligne est en cours de tri.

Changez <<<"$2"pour <"$2"traiter les noms de fichiers et appelez-le comme:

shsort '.'    infile

Le délimiteur est le même pour tout le fichier. Si c'est une limitation, elle pourrait être améliorée.

Cependant, un fichier avec seulement 6000 lignes prend 15 secondes pour être traité. Vraiment, le shell n'est pas le meilleur outil pour traiter les fichiers.

Awk

Pour plus de quelques lignes (plus de quelques 10), il est préférable d'utiliser un vrai langage de programmation. Une solution awk pourrait être:

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

Ce qui ne prend que 0,2 seconde pour le même fichier de 6000 lignes mentionné ci-dessus.

Comprenez que les <"$2"fichiers for peuvent être modifiés <<<"$2"pour les lignes dans les variables shell.

Perl

La solution la plus rapide est 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 vous voulez trier un changement de fichier <<<"$a"simplement "$a"et ajouter des -ioptions à perl pour rendre l'édition de fichier "en place":

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

perlsort '.' infile; exit
Isaac
la source
2

Utilisation sedpour trier les octets d'une adresse IP

sedn'a pas de sortfonction intégrée, mais si vos données sont suffisamment limitées dans la plage (comme avec les adresses IP), vous pouvez générer un script sed qui implémente manuellement un tri à bulles simple . Le mécanisme de base consiste à rechercher les numéros adjacents qui sont dans le désordre. Si les numéros sont en panne, échangez-les.

Le sedscript lui-même contient deux commandes de recherche et d'échange pour chaque paire de nombres dans le désordre: une pour les deux premières paires d'octets (forçant un délimiteur de fin à être présent pour marquer la fin du troisième octet), et un deuxième pour la troisième paire d'octets (fin avec EOL). Si des échanges se produisent, le programme se branche en haut du script, à la recherche de numéros qui ne sont pas en ordre. Sinon, il se ferme.

Le script généré est en partie:

$ 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

Cette approche code en dur la période en tant que délimiteur, ce qui doit être échappé, sinon elle serait "spéciale" à la syntaxe des expressions régulières (autorisant n'importe quel caractère).

Pour générer un tel script sed, cette boucle fera:

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

Redirigez la sortie de ce script vers un autre fichier, par exemple sort-ips.sed.

Un échantillon pourrait alors ressembler à ceci:

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

La variation suivante sur le script de génération utilise les marqueurs de limite de mot \<et \>pour se débarrasser de la nécessité de la deuxième substitution. Cela réduit également la taille du script généré de 1,3 Mo à un peu moins de 900 Ko et réduit considérablement le temps d'exécution de sedlui - même (à environ 50% -75% de l'original, selon l' sedimplémentation utilisée):

#!/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
la source
1
Une idée intéressante, mais cela semble compliquer un peu les choses.
Matt
1
@Matt C'est un peu le point. Trier quoi que ce soit sedest ridicule, c'est pourquoi c'est un défi intéressant.
Kusalananda
2

Voici un bash qui devine le délimiteur par lui-même:

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

Ce n'est peut-être pas très efficace ni propre mais ça marche.

Utilisez comme bash my_script.sh "00/00/18/29838/2".

Renvoie une erreur lorsque le même délimiteur n'est pas utilisé de manière cohérente ou lorsque deux délimiteurs ou plus se succèdent.

Si le délimiteur utilisé est un caractère spécial, il est échappé (sinon sedrenvoie une erreur).

jkd
la source
Cela a inspiré cela .
agc
2

Cette réponse est basée sur une mauvaise compréhension du Q., mais dans certains cas, elle se trouve être correcte de toute façon. Si l'entrée est des nombres entièrement naturels et ne comporte qu'un seul délimiteur par ligne (comme avec les exemples de données dans le Q.), cela fonctionne correctement. Il traitera également les fichiers avec des lignes qui ont chacune leur propre délimiteur, ce qui est un peu plus que ce qui était demandé.

Cette fonction shell reads à partir de l'entrée standard, utilise la substitution de paramètres POSIX pour trouver le délimiteur spécifique sur chaque ligne, (stockée dans $d), et utilise trpour remplacer $dpar une nouvelle ligne \net sorts les données de cette ligne, puis restaure les délimiteurs d'origine de chaque ligne:

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

Appliqué aux données fournies dans le PO :

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

Production:

10 23 42 50
1.10.42.200
1,42,100,330
400|404|500
agc
la source
Le délimiteur dans n'importe quelle ligne sera cohérent; les solutions générales qui permettent à l'utilisateur de déclarer le délimiteur sont impressionnantes, mais les réponses peuvent supposer tout délimiteur qui a du sens pour eux (caractère unique et non présent dans les données numériques elles-mêmes).
Jeff Schaller
2

Pour les délimiteurs arbitraires:

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

Sur une entrée comme:

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

Il donne:

2,3,4,5
2|4,5|6
There are 3 numbers in those 10 lines
Stéphane Chazelas
la source
0

Cela devrait gérer tout délimiteur non numérique (0-9). Exemple:

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

Production:

1!2!3!4!5
Alexandre
la source
0

Avec 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

Avec ruby, qui est quelque peu similaire à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


Commande personnalisée et passage juste la chaîne de délimitation (pas regex). Fonctionne si l'entrée contient également des données flottantes

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


Commande personnalisée pour 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


Pour en savoir plus - J'avais déjà cette liste pratique de monoplaces perl / ruby

Sundeep
la source
0

Ce qui suit est une variation de la réponse de Jeff dans le sens où il génère un sedscript qui fera le tri par bulles, mais est suffisamment différent pour justifier sa propre réponse.

La différence est qu'au lieu de générer O (n ^ 2) expressions régulières de base, cela génère O (n) expressions régulières étendues. Le script résultant fera environ 15 Ko. Le temps d'exécution du sedscript est en fractions de seconde (il faut un peu plus de temps pour générer le script).

Il est limité au tri des entiers positifs délimités par des points, mais il n'est pas limité à la taille des entiers (augmentez simplement 255dans la boucle principale) ou au nombre d'entiers. Le délimiteur peut être modifié en modifiant delim='.'le code.

J'ai fait ma tête pour obtenir les bonnes expressions régulières, alors je vais laisser la description des détails pour un autre jour.

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

Le script ressemblera à ceci:

$ 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'idée derrière les expressions régulières générées est de faire correspondre les motifs pour les nombres inférieurs à chaque entier; ces deux numéros seraient hors service et sont donc échangés. Les expressions régulières sont regroupées en plusieurs options OR. Portez une attention particulière aux plages ajoutées à chaque élément, parfois elles le sont {0}, ce qui signifie que l'élément immédiatement précédent doit être omis de la recherche. Les options d'expression régulière, de gauche à droite, correspondent à des nombres plus petits que le nombre donné par:

  • ceux qui se trouvent
  • la place des dizaines
  • la place des centaines
  • (continué au besoin, pour un plus grand nombre)
  • ou en étant plus petit (nombre de chiffres)

Pour donner un exemple, prenez 101(avec des espaces supplémentaires pour la lisibilité):

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

Ici, la première alternance permet les nombres 100 à 100; la deuxième alternance permet de 0 à 99.

Un autre exemple est 154:

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

Ici, la première option permet 150 à 153; le second permet 100 à 149 et le dernier permet 0 à 99.

Tester quatre fois en boucle:

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

Production:

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
la source
-2

Division de l'entrée en plusieurs lignes

À l'aide de tr, vous pouvez diviser l'entrée à l'aide d'un délimiteur arbitraire en plusieurs lignes.

Cette entrée peut ensuite être parcourue sort(en utilisant -nsi l'entrée est numérique).

Si vous souhaitez conserver le délimiteur dans la sortie, vous pouvez ensuite utiliser à trnouveau pour rajouter le délimiteur.

p.ex. en utilisant l'espace comme délimiteur

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

entrée: 1 2 4 1 4 32 18 3 sortie:1 1 2 3 4 4 18 32

Mat
la source
Vous pouvez supposer en toute sécurité les éléments numériques, et oui: le délimiteur doit être remplacé.
Jeff Schaller