Un moyen efficace de transposer un fichier dans Bash


110

J'ai un énorme fichier séparé par des tabulations formaté comme ça

X column1 column2 column3
row1 0 1 2
row2 3 4 5
row3 6 7 8
row4 9 10 11

Je voudrais le transposer de manière efficace en n'utilisant que des commandes bash (je pourrais écrire un script Perl d'une dizaine de lignes pour cela, mais il devrait être plus lent à exécuter que les fonctions bash natives). Ainsi, la sortie devrait ressembler à

X row1 row2 row3 row4
column1 0 3 6 9
column2 1 4 7 10
column3 2 5 8 11

J'ai pensé à une solution comme celle-ci

cols=`head -n 1 input | wc -w`
for (( i=1; i <= $cols; i++))
do cut -f $i input | tr $'\n' $'\t' | sed -e "s/\t$/\n/g" >> output
done

Mais c'est lent et ne semble pas la solution la plus efficace. J'ai vu une solution pour vi dans cet article , mais elle est toujours trop lente. Des pensées / suggestions / idées brillantes? :-)


12
Qu'est-ce qui vous fait penser qu'il existerait un script bash qui serait plus rapide qu'un script Perl? C'est exactement le genre de problème dans lequel Perl excelle.
Mark Pim

1
@mark, si c'est purement bash, cela pourrait être plus rapide que d'enchaîner tous ces outils cut / sed etc ensemble. Mais là encore, si vous définissez "bash" comme dans la combinaison d'outils, alors le simple fait d'écrire un script awk sera comparable au traitement de texte en Perl.
ghostdog74

Ajoutez-en un autre pour ne pas comprendre à quel point Perl serait lent ici. Lent à écrire le code? Lent à exécuter? Je n'aime vraiment pas perl, mais il excelle dans ce genre de tâche.
Corey Porter

Si vos colonnes / champs ont une taille / largeur fixe, vous pouvez utiliser la recherche de fichier Python pour éviter de lire votre fichier en mémoire. Avez-vous des tailles / largeurs de colonnes / champs fixes?
tommy.carstensen

2
Quiconque pense qu'un script shell serait plus rapide que awk ou perl a besoin de lire unix.stackexchange.com/questions/169716/… afin de comprendre pourquoi ce n'est pas le cas.
Ed Morton

Réponses:


115
awk '
{ 
    for (i=1; i<=NF; i++)  {
        a[NR,i] = $i
    }
}
NF>p { p = NF }
END {    
    for(j=1; j<=p; j++) {
        str=a[1,j]
        for(i=2; i<=NR; i++){
            str=str" "a[i,j];
        }
        print str
    }
}' file

production

$ more file
0 1 2
3 4 5
6 7 8
9 10 11

$ ./shell.sh
0 3 6 9
1 4 7 10
2 5 8 11

Performance par rapport à la solution Perl de Jonathan sur un fichier de 10000 lignes

$ head -5 file
1 0 1 2
2 3 4 5
3 6 7 8
4 9 10 11
1 0 1 2

$  wc -l < file
10000

$ time perl test.pl file >/dev/null

real    0m0.480s
user    0m0.442s
sys     0m0.026s

$ time awk -f test.awk file >/dev/null

real    0m0.382s
user    0m0.367s
sys     0m0.011s

$ time perl test.pl file >/dev/null

real    0m0.481s
user    0m0.431s
sys     0m0.022s

$ time awk -f test.awk file >/dev/null

real    0m0.390s
user    0m0.370s
sys     0m0.010s

EDIT par Ed Morton (@ ghostdog74 n'hésitez pas à supprimer si vous désapprouvez).

Peut-être que cette version avec des noms de variables plus explicites aidera à répondre à certaines des questions ci-dessous et à clarifier généralement ce que fait le script. Il utilise également des tabulations comme séparateur que l'OP avait initialement demandé afin de gérer les champs vides et cela par coïncidence embellit un peu la sortie pour ce cas particulier.

$ cat tst.awk
BEGIN { FS=OFS="\t" }
{
    for (rowNr=1;rowNr<=NF;rowNr++) {
        cell[rowNr,NR] = $rowNr
    }
    maxRows = (NF > maxRows ? NF : maxRows)
    maxCols = NR
}
END {
    for (rowNr=1;rowNr<=maxRows;rowNr++) {
        for (colNr=1;colNr<=maxCols;colNr++) {
            printf "%s%s", cell[rowNr,colNr], (colNr < maxCols ? OFS : ORS)
        }
    }
}

$ awk -f tst.awk file
X       row1    row2    row3    row4
column1 0       3       6       9
column2 1       4       7       10
column3 2       5       8       11

Les solutions ci-dessus fonctionneront dans n'importe quel awk (sauf l'ancien awk cassé bien sûr - il y a YMMV).

Les solutions ci-dessus lisent cependant le fichier entier en mémoire - si les fichiers d'entrée sont trop volumineux pour cela, vous pouvez le faire:

$ cat tst.awk
BEGIN { FS=OFS="\t" }
{ printf "%s%s", (FNR>1 ? OFS : ""), $ARGIND }
ENDFILE {
    print ""
    if (ARGIND < NF) {
        ARGV[ARGC] = FILENAME
        ARGC++
    }
}
$ awk -f tst.awk file
X       row1    row2    row3    row4
column1 0       3       6       9
column2 1       4       7       10
column3 2       5       8       11

qui n'utilise presque pas de mémoire mais lit le fichier d'entrée une fois par nombre de champs sur une ligne, il sera donc beaucoup plus lent que la version qui lit le fichier entier en mémoire. Il suppose également que le nombre de champs est le même sur chaque ligne et utilise GNU awk pour ENDFILEet ARGINDmais tout awk peut faire la même chose avec des tests sur FNR==1et END.


Et maintenant pour gérer les étiquettes de ligne et de colonne aussi?
Jonathan Leffler

OK - vous avez raison; vos exemples de données ne correspondent pas aux exemples de données de la question, mais votre code fonctionne correctement sur les exemples de données de la question et donne la sortie requise (donner ou prendre l'espacement des blancs vs tabulations). Principalement mon erreur.
Jonathan Leffler

Timings intéressants - Je suis d'accord que vous voyez un avantage de performance dans awk. J'utilisais MacOS X 10.5.8, qui n'utilise pas «gawk»; et j'utilisais Perl 5.10.1 (version 32 bits). Je suppose que vos données étaient de 10000 lignes avec 4 colonnes par ligne? Quoi qu'il en soit, cela n'a pas beaucoup d'importance; awk et perl sont des solutions viables (et la solution awk est plus soignée - les vérifications `` définies '' dans mon Perl sont nécessaires pour avertir les exécutions gratuites sous strict / warnings) et ni l'un ni l'autre n'est en reste et les deux sont susceptibles d'être beaucoup plus rapides que l'original solution de script shell.
Jonathan Leffler

Sur ma matrice d'origine de 2,2 Go, la solution perl est légèrement plus rapide que awk - 350.103s contre 369.410s J'utilisais perl 5.8.8 64bit
Federico Giorgi

1
@ zx8754 que le nombre maximum de champs s'applique uniquement à un ancien awk non POSIX. Peut-être l'incroyablement malheureusement nommé "nawk". Cela ne s'applique pas aux gawk ou autres awks modernes.
Ed Morton

47

Une autre option consiste à utiliser rs:

rs -c' ' -C' ' -T

-cmodifie le séparateur de colonne d'entrée, -Cmodifie le séparateur de colonne de sortie et -Ttranspose les lignes et les colonnes. Ne pas utiliser à la -tplace de -T, car il utilise un nombre de lignes et de colonnes calculé automatiquement qui n'est généralement pas correct. rs, qui porte le nom de la fonction de remodelage dans APL, est fourni avec les BSD et OS X, mais il devrait être disponible auprès des gestionnaires de paquets sur d'autres plates-formes.

Une deuxième option consiste à utiliser Ruby:

ruby -e'puts readlines.map(&:split).transpose.map{|x|x*" "}'

Une troisième option consiste à utiliser jq:

jq -R .|jq -sr 'map(./" ")|transpose|map(join(" "))[]'

jq -R .imprime chaque ligne d'entrée en tant que littéral de chaîne JSON, -s( --slurp) crée un tableau pour les lignes d'entrée après avoir analysé chaque ligne en tant que JSON, et -r( --raw-output) génère le contenu des chaînes au lieu des littéraux de chaîne JSON. L' /opérateur est surchargé pour fractionner les chaînes.


3
Je n'étais pas familier avec rs- merci pour le pointeur! (Le lien est vers Debian; l'amont semble être mirbsd.org/MirOS/dist/mir/rs )
tripleee

2
@lalebarde Au moins dans l'implémentation de rsqui vient avec OS X, -cseul définit le séparateur de colonne d'entrée sur un onglet.
nisetama

2
@lalebarde, essayez la citation ANSI-C de bash pour obtenir un caractère de tabulation:$'\t'
glenn jackman

3
C'est un cas extrême, mais pour un fichier très volumineux avec de nombreuses lignes comme TTC TTA TTC TTC TTT, courir rs -c' ' -C' ' -T < rows.seq > cols.seqdonne rs: no memory: Cannot allocate memory. Il s'agit d'un système exécutant FreeBSD 11.0-RELEASE avec 32 Go de RAM. Donc, je suppose que rstout place dans la RAM, ce qui est bon pour la vitesse, mais pas pour les données volumineuses.
jrm

1
jq a utilisé 21 Go de RAM sur un fichier de 766 Mo. Je l'ai tué après 40 minutes sans aucune sortie.
Glubbdrubb

30

Une solution Python:

python -c "import sys; print('\n'.join(' '.join(c) for c in zip(*(l.split() for l in sys.stdin.readlines() if l.strip()))))" < input > output

Ce qui précède est basé sur les éléments suivants:

import sys

for c in zip(*(l.split() for l in sys.stdin.readlines() if l.strip())):
    print(' '.join(c))

Ce code suppose que chaque ligne a le même nombre de colonnes (aucun remplissage n'est effectué).


3
Un problème mineur ici: Remplacez l.split()par l.strip().split()(Python 2.7), sinon la dernière ligne de la sortie est paralysée. Fonctionne pour les séparateurs de colonnes arbitraires, utilisez l.strip().split(sep)et sep.join(c)si votre séparateur est stocké dans une variable sep.
krlmlr

21

le projet transpose sur sourceforge est un programme C de type coreutil pour exactement cela.

gcc transpose.c -o transpose
./transpose -t input > output #works with stdin, too.

Merci pour le lien. Cependant, il nécessite trop de mémoire, lorsqu'il s'agit de matrices / fichiers volumineux.
tommy.carstensen

il a des arguments pour la taille de bloc et la taille du champ: essayez de modifier les arguments -bet -f.
mouton volant

La taille de bloc par défaut (--block ou -b) est de 10 Ko et la taille de champ par défaut (--fieldmax ou -f) est de 64, donc cela ne peut pas être le cas. J'ai essayé. Merci pour la suggestion.
tommy.carstensen

1
Fonctionne bien avec un csv de taille 2 Go.
discipulus

2
Pour un fichier de matrice avec des dimensions d'environ 11k sur 5k, j'ai trouvé que transpose.c était ~ 7x plus rapide et ~ 5x plus efficace en mémoire que la première solution awk de ghostdog74. De plus, j'ai trouvé que le code awk «n'utilise presque aucune mémoire» de ghostdog74 ne fonctionnait pas correctement. Faites également attention à l'indicateur --limit dans le programme transpose.c, qui par défaut limite la sortie à la dimension 1k par 1k.
ncemami

16

Pure BASH, pas de processus supplémentaire. Un bel exercice:

declare -a array=( )                      # we build a 1-D-array

read -a line < "$1"                       # read the headline

COLS=${#line[@]}                          # save number of columns

index=0
while read -a line ; do
    for (( COUNTER=0; COUNTER<${#line[@]}; COUNTER++ )); do
        array[$index]=${line[$COUNTER]}
        ((index++))
    done
done < "$1"

for (( ROW = 0; ROW < COLS; ROW++ )); do
  for (( COUNTER = ROW; COUNTER < ${#array[@]}; COUNTER += COLS )); do
    printf "%s\t" ${array[$COUNTER]}
  done
  printf "\n" 
done

Cela a fonctionné pour mon fichier, bien que cela soit intéressant, il imprime une liste de répertoires pour la première ligne de la table. Je ne connais pas assez BASH pour comprendre pourquoi.
bugloaf

@bugloaf votre table a un * dans le coin.
Hello71

2
@bugloaf: Le fait de citer correctement les variables devrait éviter que:printf "%s\t" "${array[$COUNTER]}"
Suspendu jusqu'à nouvel ordre.

16

Jetez un œil au datamash GNU qui peut être utilisé comme datamash transpose. Une future version prendra également en charge la tabulation croisée (tableaux croisés dynamiques)


9

Voici un script Perl moyennement solide pour faire le travail. Il existe de nombreuses analogies structurelles avec la awksolution de @ ghostdog74 .

#!/bin/perl -w
#
# SO 1729824

use strict;

my(%data);          # main storage
my($maxcol) = 0;
my($rownum) = 0;
while (<>)
{
    my(@row) = split /\s+/;
    my($colnum) = 0;
    foreach my $val (@row)
    {
        $data{$rownum}{$colnum++} = $val;
    }
    $rownum++;
    $maxcol = $colnum if $colnum > $maxcol;
}

my $maxrow = $rownum;
for (my $col = 0; $col < $maxcol; $col++)
{
    for (my $row = 0; $row < $maxrow; $row++)
    {
        printf "%s%s", ($row == 0) ? "" : "\t",
                defined $data{$row}{$col} ? $data{$row}{$col} : "";
    }
    print "\n";
}

Avec la taille des données de l'échantillon, la différence de performance entre perl et awk était négligeable (1 milliseconde sur 7 au total). Avec un ensemble de données plus grand (matrice 100x100, entrées de 6 à 8 caractères chacune), perl a légèrement surpassé awk - 0,026s contre 0,042s. Ni l'un ni l'autre ne posera probablement de problème.


Timings représentatifs pour Perl 5.10.1 (32 bits) vs awk (version 20040207 lorsque '-V') vs gawk 3.1.7 (32 bits) sur MacOS X 10.5.8 sur un fichier contenant 10000 lignes avec 5 colonnes par ligne:

Osiris JL: time gawk -f tr.awk xxx  > /dev/null

real    0m0.367s
user    0m0.279s
sys 0m0.085s
Osiris JL: time perl -f transpose.pl xxx > /dev/null

real    0m0.138s
user    0m0.128s
sys 0m0.008s
Osiris JL: time awk -f tr.awk xxx  > /dev/null

real    0m1.891s
user    0m0.924s
sys 0m0.961s
Osiris-2 JL: 

Notez que gawk est beaucoup plus rapide que awk sur cette machine, mais toujours plus lent que perl. De toute évidence, votre kilométrage variera.


sur mon système, gawk surpasse perl. vous pouvez voir mes résultats dans mon post édité
ghostdog74

4
conclusion recueillie: plate-forme différente, version logicielle différente, résultats différents.
ghostdog74

6

Si vous avez scinstallé, vous pouvez faire:

psc -r < inputfile | sc -W% - > outputfile

4
Notez que cela prend en charge un nombre limité de lignes car scnomme ses colonnes comme un ou une combinaison de deux caractères. La limite est 26 + 26^2 = 702.
Thor


5

En supposant que toutes vos lignes ont le même nombre de champs, ce programme awk résout le problème:

{for (f=1;f<=NF;f++) col[f] = col[f]":"$f} END {for (f=1;f<=NF;f++) print col[f]}

En mots, lorsque vous bouclez sur les lignes, pour chaque champ, fdéveloppez une chaîne séparée par un «:» col[f]contenant les éléments de ce champ. Une fois que vous avez terminé avec toutes les lignes, imprimez chacune de ces chaînes sur une ligne distincte. Vous pouvez ensuite remplacer «:» pour le séparateur souhaité (par exemple, un espace) en acheminant la sortie à travers tr ':' ' '.

Exemple:

$ echo "1 2 3\n4 5 6"
1 2 3
4 5 6

$ echo "1 2 3\n4 5 6" | awk '{for (f=1;f<=NF;f++) col[f] = col[f]":"$f} END {for (f=1;f<=NF;f++) print col[f]}' | tr ':' ' '
 1 4
 2 5
 3 6

5

GNU datamash est parfaitement adapté à ce problème avec une seule ligne de code et une taille de fichier potentiellement arbitrairement grande!

datamash -W transpose infile > outfile

3

Une solution perl hackish peut être comme ça. C'est bien car il ne charge pas tous les fichiers en mémoire, imprime les fichiers temporaires intermédiaires, puis utilise la pâte merveilleuse

#!/usr/bin/perl
use warnings;
use strict;

my $counter;
open INPUT, "<$ARGV[0]" or die ("Unable to open input file!");
while (my $line = <INPUT>) {
    chomp $line;
    my @array = split ("\t",$line);
    open OUTPUT, ">temp$." or die ("unable to open output file!");
    print OUTPUT join ("\n",@array);
    close OUTPUT;
    $counter=$.;
}
close INPUT;

# paste files together
my $execute = "paste ";
foreach (1..$counter) {
    $execute.="temp$counter ";
}
$execute.="> $ARGV[1]";
system $execute;

l'utilisation de fichiers de collage et temporaires ne sont que des opérations supplémentaires inutiles. vous pouvez simplement faire de la manipulation dans la mémoire elle-même, par exemple des tableaux / hachages
ghostdog74

2
Oui, mais cela ne signifierait-il pas garder tout en mémoire? Les fichiers dont je traite ont une taille d'environ 2 à 20 Go.
Federico Giorgi

3

La seule amélioration que je peux voir pour votre propre exemple est l'utilisation de awk, qui réduira le nombre de processus exécutés et la quantité de données acheminées entre eux:

/bin/rm output 2> /dev/null

cols=`head -n 1 input | wc -w` 
for (( i=1; i <= $cols; i++))
do
  awk '{printf ("%s%s", tab, $'$i'); tab="\t"} END {print ""}' input
done >> output

3

J'utilise normalement ce petit awkextrait de code pour cette exigence:

  awk '{for (i=1; i<=NF; i++) a[i,NR]=$i
        max=(max<NF?NF:max)}
        END {for (i=1; i<=max; i++)
              {for (j=1; j<=NR; j++) 
                  printf "%s%s", a[i,j], (j==NR?RS:FS)
              }
        }' file

Cela charge simplement toutes les données dans un tableau bidimensionnel a[line,column], puis les réimprime en tant que a[column,line], de sorte qu'il transpose l'entrée donnée.

Cela doit garder une trace du maxnombre imum de colonnes du fichier initial, de sorte qu'il soit utilisé comme nombre de lignes à imprimer.


2

J'ai utilisé la solution de fgm (merci fgm!), Mais j'avais besoin d'éliminer les caractères de tabulation à la fin de chaque ligne, donc j'ai modifié le script ainsi:

#!/bin/bash 
declare -a array=( )                      # we build a 1-D-array

read -a line < "$1"                       # read the headline

COLS=${#line[@]}                          # save number of columns

index=0
while read -a line; do
    for (( COUNTER=0; COUNTER<${#line[@]}; COUNTER++ )); do
        array[$index]=${line[$COUNTER]}
        ((index++))
    done
done < "$1"

for (( ROW = 0; ROW < COLS; ROW++ )); do
  for (( COUNTER = ROW; COUNTER < ${#array[@]}; COUNTER += COLS )); do
    printf "%s" ${array[$COUNTER]}
    if [ $COUNTER -lt $(( ${#array[@]} - $COLS )) ]
    then
        printf "\t"
    fi
  done
  printf "\n" 
done

2

Je cherchais juste un tranpose bash similaire mais avec un support pour le rembourrage. Voici le script que j'ai écrit basé sur la solution de fgm, qui semble fonctionner. Si cela peut être utile ...

#!/bin/bash 
declare -a array=( )                      # we build a 1-D-array
declare -a ncols=( )                      # we build a 1-D-array containing number of elements of each row

SEPARATOR="\t";
PADDING="";
MAXROWS=0;
index=0
indexCol=0
while read -a line; do
    ncols[$indexCol]=${#line[@]};
((indexCol++))
if [ ${#line[@]} -gt ${MAXROWS} ]
    then
         MAXROWS=${#line[@]}
    fi    
    for (( COUNTER=0; COUNTER<${#line[@]}; COUNTER++ )); do
        array[$index]=${line[$COUNTER]}
        ((index++))

    done
done < "$1"

for (( ROW = 0; ROW < MAXROWS; ROW++ )); do
  COUNTER=$ROW;
  for (( indexCol=0; indexCol < ${#ncols[@]}; indexCol++ )); do
if [ $ROW -ge ${ncols[indexCol]} ]
    then
      printf $PADDING
    else
  printf "%s" ${array[$COUNTER]}
fi
if [ $((indexCol+1)) -lt ${#ncols[@]} ]
then
  printf $SEPARATOR
    fi
    COUNTER=$(( COUNTER + ncols[indexCol] ))
  done
  printf "\n" 
done

2

Je cherchais une solution pour transposer tout type de matrice (nxn ou mxn) avec tout type de données (nombres ou données) et j'ai obtenu la solution suivante:

Row2Trans=number1
Col2Trans=number2

for ((i=1; $i <= Line2Trans; i++));do
    for ((j=1; $j <=Col2Trans ; j++));do
        awk -v var1="$i" -v var2="$j" 'BEGIN { FS = "," }  ; NR==var1 {print $((var2)) }' $ARCHIVO >> Column_$i
    done
done

paste -d',' `ls -mv Column_* | sed 's/,//g'` >> $ARCHIVO

2

Si vous ne voulez extraire qu'une seule ligne (délimitée par des virgules) $ N d'un fichier et la transformer en colonne:

head -$N file | tail -1 | tr ',' '\n'

2

Pas très élégant, mais cette commande "sur une seule ligne" résout le problème rapidement:

cols=4; for((i=1;i<=$cols;i++)); do \
            awk '{print $'$i'}' input | tr '\n' ' '; echo; \
        done

Ici cols est le nombre de colonnes, où vous pouvez remplacer 4 par head -n 1 input | wc -w.


2

Une autre awksolution et une entrée limitée avec la taille de la mémoire dont vous disposez.

awk '{ for (i=1; i<=NF; i++) RtoC[i]= (RtoC[i]? RtoC[i] FS $i: $i) }
    END{ for (i in RtoC) print RtoC[i] }' infile

Cela joint chaque même position de numéro de fichier dans ensemble et ENDimprime le résultat qui serait la première ligne de la première colonne, la deuxième ligne de la deuxième colonne, etc.

X row1 row2 row3 row4
column1 0 3 6 9
column2 1 4 7 10
column3 2 5 8 11

2

Certains utilitaires standard * nix one-liners, aucun fichier temporaire n'est nécessaire. NB: le PO voulait une solution efficace , (c'est-à-dire plus rapide), et les premières réponses sont généralement plus rapides que cette réponse. Ces one-liners sont pour ceux qui aiment les outils logiciels * nix , pour quelque raison que ce soit. Dans de rares cas ( par exemple, E / S et mémoire rares), ces extraits peuvent en fait être plus rapides que certaines des principales réponses.

Appelez le fichier d'entrée foo .

  1. Si nous savons que foo a quatre colonnes:

    for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done
  2. Si nous ne savons pas combien de colonnes foo a:

    n=$(head -n 1 foo | wc -w)
    for f in $(seq 1 $n) ; do cut -d ' ' -f $f foo | xargs echo ; done

    xargsa une taille limite et ferait donc un travail incomplet avec un long fichier. Quelle taille limite dépend du système, par exemple:

    { timeout '.01' xargs --show-limits ; } 2>&1 | grep Max

    Longueur maximale de commande que nous pourrions réellement utiliser: 2088944

  3. tr& echo:

    for f in 1 2 3 4; do cut -d ' ' -f $f foo | tr '\n\ ' ' ; echo; done

    ... ou si le nombre de colonnes est inconnu:

    n=$(head -n 1 foo | wc -w)
    for f in $(seq 1 $n); do 
        cut -d ' ' -f $f foo | tr '\n' ' ' ; echo
    done
  4. L'utilisation set, qui aime xargs, a des limitations similaires basées sur la taille de la ligne de commande:

    for f in 1 2 3 4 ; do set - $(cut -d ' ' -f $f foo) ; echo $@ ; done

2
Celles-ci seraient toutes des ordres de grandeur plus lentes qu'une solution awk ou perl et fragiles. Lisez unix.stackexchange.com/questions/169716/… .
Ed Morton

@EdMorton, merci, introduction qualifiée de ma réponse pour répondre à vos problèmes de vitesse. Re "fragile": pas 3) , ni les autres lorsque le programmeur sait que les données sont sûres pour une technique donnée; et le code shell compatible POSIX n'est-il pas un standard plus stable que perl ?
agc

désolé, j'aime beaucoup perl. Dans ce cas, l'outil à utiliser serait awk. cut, head, echo, Etc. ne sont pas plus POSIX code shell compatible qu'un awkscript est - ils sont tous en standard sur chaque installation UNIX. Il n'y a tout simplement aucune raison d'utiliser un ensemble d'outils qui, en combinaison, vous obligent à faire attention au contenu de votre fichier d'entrée et au répertoire à partir duquel vous exécutez le script lorsque vous pouvez simplement utiliser awk et que le résultat final est plus rapide et plus robuste .
Ed Morton

S'il vous plaît, je ne suis pas anti- awk , mais les conditions varient. Raison n ° 1: for f in cut head xargs seq awk ; do wc -c $(which $f) ; done lorsque le stockage est trop lent ou que les E / S sont trop faibles, les interprètes plus gros aggravent les choses, quelle que soit leur qualité dans des circonstances plus idéales. Raison n ° 2: awk (ou la plupart des langages) souffre également d'une courbe d'apprentissage plus raide qu'un petit utilitaire conçu pour bien faire une chose. Lorsque le temps d'exécution est moins cher que les heures de travail du codeur, un codage facile avec des «outils logiciels» permet d'économiser de l'argent.
agc

1
#!/bin/bash

aline="$(head -n 1 file.txt)"
set -- $aline
colNum=$#

#set -x
while read line; do
  set -- $line
  for i in $(seq $colNum); do
    eval col$i="\"\$col$i \$$i\""
  done
done < file.txt

for i in $(seq $colNum); do
  eval echo \${col$i}
done

une autre version avec set eval


Lisez unix.stackexchange.com/questions/169716/… pour comprendre certains des problèmes liés à cette solution, mais pas tous.
Ed Morton

1

Une autre variante bash

$ cat file 
XXXX    col1    col2    col3
row1    0       1       2
row2    3       4       5
row3    6       7       8
row4    9       10      11

Scénario

#!/bin/bash

I=0
while read line; do
    i=0
    for item in $line; { printf -v A$I[$i] $item; ((i++)); }
    ((I++))
done < file
indexes=$(seq 0 $i)

for i in $indexes; {
    J=0
    while ((J<I)); do
        arr="A$J[$i]"
        printf "${!arr}\t"
        ((J++))
    done
    echo
}

Production

$ ./test 
XXXX    row1    row2    row3    row4    
col1    0       3       6       9   
col2    1       4       7       10  
col3    2       5       8       11

0

Voici une solution Haskell. Une fois compilé avec -O2, il fonctionne légèrement plus vite que awk de ghostdog et légèrement plus lent que le python c finement enveloppé de Stephan sur ma machine pour les lignes d'entrée répétées "Hello world". Malheureusement, le support de GHC pour passer le code de ligne de commande est inexistant pour autant que je sache, vous devrez donc l'écrire dans un fichier vous-même. Il tronquera les lignes à la longueur de la ligne la plus courte.

transpose :: [[a]] -> [[a]]
transpose = foldr (zipWith (:)) (repeat [])

main :: IO ()
main = interact $ unlines . map unwords . transpose . map words . lines

0

Une solution awk qui stocke l'ensemble de la baie en mémoire

    awk '$0!~/^$/{    i++;
                  split($0,arr,FS);
                  for (j in arr) {
                      out[i,j]=arr[j];
                      if (maxr<j){ maxr=j}     # max number of output rows.
                  }
            }
    END {
        maxc=i                 # max number of output columns.
        for     (j=1; j<=maxr; j++) {
            for (i=1; i<=maxc; i++) {
                printf( "%s:", out[i,j])
            }
            printf( "%s\n","" )
        }
    }' infile

Mais nous pouvons "parcourir" le fichier autant de fois que les lignes de sortie sont nécessaires:

#!/bin/bash
maxf="$(awk '{if (mf<NF); mf=NF}; END{print mf}' infile)"
rowcount=maxf
for (( i=1; i<=rowcount; i++ )); do
    awk -v i="$i" -F " " '{printf("%s\t ", $i)}' infile
    echo
done

Qui (pour un faible nombre de lignes de sortie est plus rapide que le code précédent).


0

Voici un one-liner Bash basé sur la simple conversion de chaque ligne en colonne et pasteleur assemblage:

echo '' > tmp1;  \
cat m.txt | while read l ; \
            do    paste tmp1 <(echo $l | tr -s ' ' \\n) > tmp2; \
                  cp tmp2 tmp1; \
            done; \
cat tmp1

m.txt:

0 1 2
4 5 6
7 8 9
10 11 12
  1. crée un tmp1fichier pour qu'il ne soit pas vide.

  2. lit chaque ligne et la transforme en colonne en utilisant tr

  3. colle la nouvelle colonne dans le tmp1fichier

  4. les copies sont renvoyées dans tmp1.

PS: Je voulais vraiment utiliser des io-descripteurs mais je n'ai pas pu les faire fonctionner.


Assurez-vous de définir un réveil si vous comptez l'exécuter sur un gros fichier. Lisez unix.stackexchange.com/questions/169716/… pour comprendre certains, mais pas tous, des problèmes liés à cette approche.
Ed Morton

0

Un oneliner utilisant R ...

  cat file | Rscript -e "d <- read.table(file('stdin'), sep=' ', row.names=1, header=T); write.table(t(d), file=stdout(), quote=F, col.names=NA) "

0

J'ai utilisé ci-dessous deux scripts pour effectuer des opérations similaires auparavant. Le premier est en awk qui est beaucoup plus rapide que le second qui est en bash "pur". Vous pourrez peut-être l'adapter à votre propre application.

awk '
{
    for (i = 1; i <= NF; i++) {
        s[i] = s[i]?s[i] FS $i:$i
    }
}
END {
    for (i in s) {
        print s[i]
    }
}' file.txt
declare -a arr

while IFS= read -r line
do
    i=0
    for word in $line
    do
        [[ ${arr[$i]} ]] && arr[$i]="${arr[$i]} $word" || arr[$i]=$word
        ((i++))
    done
done < file.txt

for ((i=0; i < ${#arr[@]}; i++))
do
    echo ${arr[i]}
done
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.