Una forma eficiente de transponer un archivo en Bash


110

Tengo un archivo enorme separado por tabulaciones formateado como este

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

Me gustaría transponerlo de una manera eficiente usando solo comandos bash (podría escribir una secuencia de comandos Perl de diez líneas aproximadamente para hacer eso, pero debería ser más lento de ejecutar que las funciones nativas de bash). Entonces la salida debería verse como

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

Pensé en una solución como esta

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

Pero es lento y no parece la solución más eficiente. He visto una solución para vi en esta publicación , pero sigue siendo demasiado lenta. ¿Alguna idea / sugerencia / idea brillante? :-)


12
¿Qué te hace pensar que existiría un script bash que sería más rápido que un script Perl? Este es exactamente el tipo de problema en el que se destaca Perl.
Mark Pim

1
@mark, si es bash puro, podría ser más rápido que encadenar todas esas herramientas de corte / sed, etc. Pero, de nuevo, si define "bash" como en la combinación de herramientas, entonces simplemente escribir un script awk será comparable al procesamiento de texto Perl wrt.
ghostdog74

Agregue otro por no entender cuán lento sería perl aquí. ¿Lento para escribir el código? ¿Lento para ejecutar? Realmente no me gusta Perl, pero sobresale en este tipo de tareas.
Corey Porter

Si sus columnas / campos tienen un tamaño / ancho fijo, entonces puede usar la búsqueda de archivos de Python para evitar leer su archivo en la memoria. ¿Tiene tamaños / anchos de columna / campo fijos?
tommy.carstensen

2
Cualquiera que piense que un script de shell sería más rápido que awk o perl necesita leer unix.stackexchange.com/questions/169716/… para poder entender por qué ese no es el caso.
Ed Morton

Respuestas:


114
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

salida

$ 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

Rendimiento frente a la solución Perl de Jonathan en un archivo de 10000 líneas

$ 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

EDITAR por Ed Morton (@ ghostdog74 no dude en eliminarlo si no lo aprueba).

Quizás esta versión con algunos nombres de variables más explícitos ayude a responder algunas de las preguntas a continuación y, en general, aclare lo que está haciendo el script. También usa pestañas como separador que el OP había solicitado originalmente para manejar campos vacíos y, casualmente, embellece un poco la salida para este caso particular.

$ 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

Las soluciones anteriores funcionarán en cualquier awk (excepto el awk antiguo y roto, por supuesto, hay YMMV).

Sin embargo, las soluciones anteriores leen todo el archivo en la memoria; si los archivos de entrada son demasiado grandes para eso, puede hacer esto:

$ 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

que casi no usa memoria pero lee el archivo de entrada una vez por número de campos en una línea, por lo que será mucho más lento que la versión que lee el archivo completo en la memoria. También asume que el número de campos es el mismo en cada línea y usa GNU awk para ENDFILEy, ARGINDpero cualquier awk puede hacer lo mismo con pruebas en FNR==1y END.


¿Y ahora también para manejar etiquetas de filas y columnas?
Jonathan Leffler

OK, tienes razón; sus datos de muestra no coinciden con los datos de muestra de la pregunta, pero su código funciona bien en los datos de muestra de la pregunta y proporciona la salida requerida (más o menos espacio en blanco frente a tabulaciones). Principalmente mi error.
Jonathan Leffler

Tiempos interesantes: estoy de acuerdo en que ve un beneficio de rendimiento en awk. Estaba usando MacOS X 10.5.8, que no usa 'gawk'; y estaba usando Perl 5.10.1 (compilación de 32 bits). Supongo que sus datos eran 10000 líneas con 4 columnas por línea. De todos modos, no importa mucho; tanto awk como perl son soluciones viables (y la solución awk es más ordenada: las comprobaciones 'definidas' en mi Perl son necesarias para advertir ejecuciones libres bajo estrictas / advertencias) y ninguna de las dos se queda atrás y es probable que ambas sean mucho más rápidas que la original solución de script de shell.
Jonathan Leffler

En mi matriz original de 2.2GB, la solución de perl es ligeramente más rápida que awk - 350.103s vs 369.410s Estaba usando perl 5.8.8 64bit
Federico Giorgi

1
@ zx8754 ese número máximo de campos solo se aplica a un awk antiguo que no es POSIX. Posiblemente el increíblemente lamentablemente llamado "nawk". No se aplica a gawk u otros awks modernos.
Ed Morton

47

Otra opción es utilizar rs:

rs -c' ' -C' ' -T

-ccambia el separador de la columna de entrada, -Ccambia el separador de la columna de salida y -Ttranspone filas y columnas. No lo utilice en -tlugar de -T, porque utiliza un número de filas y columnas calculado automáticamente que no suele ser correcto. rs, que lleva el nombre de la función de remodelación en APL, viene con BSD y OS X, pero debería estar disponible en administradores de paquetes en otras plataformas.

Una segunda opción es usar Ruby:

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

Una tercera opción es utilizar jq:

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

jq -R .imprime cada línea de entrada como un literal de cadena JSON, -s( --slurp) crea una matriz para las líneas de entrada después de analizar cada línea como JSON y -r( --raw-output) genera el contenido de cadenas en lugar de literales de cadena JSON. El /operador está sobrecargado para dividir cadenas.


3
No estaba familiarizado con rs, ¡gracias por el puntero! (El enlace es para Debian; el upstream parece ser mirbsd.org/MirOS/dist/mir/rs )
tripleee

2
@lalebarde Al menos en la implementación de rseso viene con OS X, -csolo establece el separador de columna de entrada en una pestaña.
nisetama

2
@lalebarde, pruebe las citas ANSI-C de bash para obtener un carácter de tabulación:$'\t'
glenn jackman

3
Este es un caso extremo, pero para un archivo muy grande con muchas filas como TTC TTA TTC TTC TTT, la ejecución rs -c' ' -C' ' -T < rows.seq > cols.seqda rs: no memory: Cannot allocate memory. Este es un sistema que ejecuta FreeBSD 11.0-RELEASE con 32 GB de RAM. Entonces, supongo que rspone todo en RAM, lo que es bueno para la velocidad, pero no para datos grandes.
jrm

1
jq usó 21 Gb de RAM en un archivo de 766 MB. Lo maté después de 40 minutos sin ningún resultado.
Glubbdrubb

30

Una solución de 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

Lo anterior se basa en lo siguiente:

import sys

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

Este código asume que cada línea tiene el mismo número de columnas (no se realiza ningún relleno).


3
Un problema menor aquí: Reemplazar l.split()por l.strip().split()(Python 2.7), de lo contrario, la última línea de la salida está dañada. Funciona para separadores de columnas arbitrarios, use l.strip().split(sep)y sep.join(c)si su separador está almacenado en variable sep.
krlmlr

21

el proyecto de transposición en sourceforge es un programa en C similar a coreutil para exactamente eso.

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

Gracias por el enlace. Sin embargo, requiere demasiada memoria cuando se trata de matrices / archivos grandes.
tommy.carstensen

tiene argumentos para tamaño de bloque y tamaño de campo: intente ajustar los argumentos -by -f.
oveja voladora

El tamaño de bloque predeterminado (--block o -b) es de 10 kb y el tamaño de campo predeterminado (--fieldmax o -f) es 64, por lo que no puede ser ese. Lo intenté. Sin embargo, gracias por la sugerencia.
tommy.carstensen

1
Funcionó bien con un csv de tamaño 2 GB.
discipulus

2
Para un archivo de matriz con dimensiones de aproximadamente 11k por 5k, encontré que transpose.c es ~ 7 veces más rápido y ~ 5 veces más eficiente en memoria que la primera solución awk de ghostdog74. Además, descubrí que el código awk "casi sin memoria" de ghostdog74 no funcionaba correctamente. Además, tenga cuidado con el indicador --limit en el programa transpose.c, que por defecto limita la salida a la dimensión 1k por 1k.
ncemami

16

Pure BASH, sin proceso adicional. Un buen ejercicio:

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

Esto funcionó para mi archivo, aunque curiosamente imprime una lista de directorios para la primera línea de la tabla. No sé lo suficiente de BASH para averiguar por qué.
bugloaf

@bugloaf tu mesa tiene un * en la esquina.
Hola 71

2
@bugloaf: Citar correctamente las variables debería evitar que:printf "%s\t" "${array[$COUNTER]}"
detenga hasta nuevo aviso.

16

Eche un vistazo a GNU datamash que se puede usar como datamash transpose. Una versión futura también admitirá la tabulación cruzada (tablas dinámicas)


9

Aquí hay un script de Perl moderadamente sólido para hacer el trabajo. Hay muchas analogías estructurales con la awksolución 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";
}

Con el tamaño de los datos de la muestra, la diferencia de rendimiento entre perl y awk fue insignificante (1 milisegundo de un total de 7). Con un conjunto de datos más grande (matriz de 100x100, entradas de 6 a 8 caracteres cada una), perl superó ligeramente a awk: 0,026 s frente a 0,042 s. Es probable que ninguno de los dos sea un problema.


Tiempos representativos para Perl 5.10.1 (32 bits) vs awk (versión 20040207 cuando se le da '-V') vs gawk 3.1.7 (32 bits) en MacOS X 10.5.8 en un archivo que contiene 10,000 líneas con 5 columnas por línea:

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: 

Tenga en cuenta que gawk es mucho más rápido que awk en esta máquina, pero aún más lento que perl. Claramente, su millaje variará.


en mi sistema, gawk supera a perl. puedes ver mis resultados en mi publicación editada
ghostdog74

4
Conclusión recogida: plataforma diferente, versión de software diferente, resultados diferentes.
ghostdog74

6

Si lo ha scinstalado, puede hacer:

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

4
Tenga en cuenta que esto admite un número limitado de líneas porque scnombra sus columnas como uno o una combinación de dos caracteres. El límite es 26 + 26^2 = 702.
Thor


5

Suponiendo que todas sus filas tienen el mismo número de campos, este programa awk resuelve el problema:

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

En palabras, a medida que recorre las filas, para cada campo fcrece un ':' - cadena separada que col[f]contiene los elementos de ese campo. Una vez que haya terminado con todas las filas, imprima cada una de esas cadenas en una línea separada. Luego puede sustituir ':' por el separador que desea (digamos, un espacio) canalizando la salida tr ':' ' '.

Ejemplo:

$ 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 se adapta perfectamente a este problema con solo una línea de código y potencialmente arbitrariamente grande.

datamash -W transpose infile > outfile

3

Una solución hackish perl puede ser así. Es bueno porque no carga todo el archivo en la memoria, imprime archivos temporales intermedios y luego usa la maravillosa pasta

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

el uso de archivos temporales y de pegado son operaciones adicionales innecesarias. simplemente puede hacer manipulación dentro de la memoria, por ejemplo, matrices / hashes
ghostdog74

2
Sí, pero ¿eso no significaría guardar todo en la memoria? Los archivos con los que estoy tratando tienen un tamaño de entre 2 y 20 GB.
Federico Giorgi

3

La única mejora que puedo ver en su propio ejemplo es el uso de awk que reducirá la cantidad de procesos que se ejecutan y la cantidad de datos que se canalizan entre ellos:

/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

Normalmente uso este pequeño awkfragmento para este requisito:

  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

Esto simplemente carga todos los datos en una matriz bidimensional a[line,column]y luego los vuelve a imprimir como a[column,line], de modo que transpone la entrada dada.

Esto necesita realizar un seguimiento de la maxcantidad inmensa de columnas que tiene el archivo inicial, de modo que se use como el número de filas para imprimir.


2

Usé la solución de fgm (¡gracias fgm!), Pero necesitaba eliminar los caracteres de tabulación al final de cada fila, así que modifiqué el script así:

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

Solo estaba buscando una transposición de bash similar pero con soporte para relleno. Aquí está el script que escribí basado en la solución de fgm, que parece funcionar. Si puede ser de ayuda ...

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

Estaba buscando una solución para transponer cualquier tipo de matriz (nxn o mxn) con cualquier tipo de datos (números o datos) y obtuve la siguiente solución:

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 solo desea tomar una sola línea (delimitada por comas) $ N de un archivo y convertirla en una columna:

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

2

No es muy elegante, pero este comando de "línea única" resuelve el problema rápidamente:

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

Aquí cols es el número de columnas, donde puede reemplazar 4 por head -n 1 input | wc -w.


2

Otra awksolución y entrada limitada con el tamaño de memoria que tienes.

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

Esto une cada posición del mismo número archivado en juntos e ENDimprime el resultado que sería la primera fila en la primera columna, la segunda fila en la segunda columna, etc.

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

2

Algunos ejemplos de utilidades estándar * nix , no se necesitan archivos temporales. NB: el OP quería una solución eficiente (es decir, más rápida), y las respuestas principales suelen ser más rápidas que esta respuesta. Estas frases breves son para quienes gustan de las herramientas de software * nix , por cualquier motivo. En casos raros ( por ejemplo, IO y memoria escasos), estos fragmentos pueden ser más rápidos que algunas de las respuestas principales.

Llame al archivo de entrada foo .

  1. Si sabemos que foo tiene cuatro columnas:

    for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done
  2. Si no sabemos cuántas columnas tiene foo :

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

    xargstiene un límite de tamaño y, por lo tanto, haría un trabajo incompleto con un archivo largo. ¿Qué límite de tamaño depende del sistema, por ejemplo:

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

    Longitud máxima de comando que podríamos usar: 2088944

  3. tr& echo:

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

    ... o si se desconoce el número de columnas:

    n=$(head -n 1 foo | wc -w)
    for f in $(seq 1 $n); do 
        cut -d ' ' -f $f foo | tr '\n' ' ' ; echo
    done
  4. El uso set, que como xargs, tiene limitaciones similares basadas en el tamaño de la línea de comandos:

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

2
Todos esos serían órdenes de magnitud más lentos que una solución de awk o perl y frágiles. Lea unix.stackexchange.com/questions/169716/… .
Ed Morton

@EdMorton, gracias, introducción calificada de mi respuesta para abordar sus preocupaciones sobre la velocidad. Re "frágil": no 3) , ni los demás cuando el programador sabe que los datos son seguros para una técnica determinada; ¿y no es el código de shell compatible con POSIX un estándar más estable que perl ?
agc

Lo siento, no sé mucho sobre Perl. En este caso la herramienta a utilizar sería awk. cut, head, echo, Etc no son más POSIX código shell compatible que un awkguión es - todos ellos son estándar en todas las instalaciones de UNIX. Simplemente no hay razón para usar un conjunto de herramientas que en combinación requieran que tenga cuidado con el contenido de su archivo de entrada y el directorio desde el que ejecuta el script cuando puede usar awk y el resultado final es más rápido y más sólido .
Ed Morton

Por favor, no soy anti- awk , pero las condiciones varían. Razón n. ° 1: for f in cut head xargs seq awk ; do wc -c $(which $f) ; done cuando el almacenamiento es demasiado lento o el IO es demasiado bajo, los intérpretes más grandes empeoran las cosas sin importar lo buenos que serían en circunstancias más ideales. Razón # 2: awk , (o la mayoría de los idiomas), también sufre de una curva de aprendizaje más pronunciada que una pequeña utilidad diseñada para hacer bien una cosa. Cuando el tiempo de ejecución es más barato que las horas de trabajo del codificador, la codificación sencilla con "herramientas de software" ahorra dinero.
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

otra versión con set eval


Lea unix.stackexchange.com/questions/169716/… para comprender algunos, pero no todos, los problemas con esa solución.
Ed Morton

1

Otra variante de bash

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

Guión

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

Salida

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

0

Aquí tienes una solución de Haskell. Cuando se compila con -O2, se ejecuta un poco más rápido que el awk de ghostdog y un poco más lento que el c python de Stephan en mi máquina para las líneas de entrada repetidas de "Hola mundo". Lamentablemente, el soporte de GHC para pasar código de línea de comando no existe hasta donde yo sé, por lo que tendrá que escribirlo en un archivo usted mismo. Truncará las filas a la longitud de la fila más corta.

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

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

0

Una solución awk que almacena toda la matriz en la memoria.

    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

Pero podemos "recorrer" el archivo tantas veces como las filas de salida sean necesarias:

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

Cuál (para un recuento bajo de filas de salida es más rápido que el código anterior).


0

Aquí hay un resumen de Bash que se basa simplemente en convertir cada línea en una columna y pastejuntarlas:

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. crea el tmp1archivo para que no esté vacío.

  2. lee cada línea y la transforma en una columna usando tr

  3. pega la nueva columna al tmp1archivo

  4. las copias resultan de nuevo en tmp1.

PD: Tenía muchas ganas de usar descriptores io pero no pude hacer que funcionaran.


Asegúrese de configurar un reloj de alarma si va a ejecutarlo en un archivo grande. Lea unix.stackexchange.com/questions/169716/… para comprender algunos, pero no todos, los problemas con ese enfoque.
Ed Morton

0

Un delineador con 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

He usado a continuación dos scripts para realizar operaciones similares antes. El primero está en awk, que es mucho más rápido que el segundo, que está en bash "puro". Es posible que pueda adaptarlo a su propia aplicación.

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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.