Uma maneira eficiente de transpor um arquivo no Bash


110

Eu tenho um arquivo enorme separado por tabulação formatado assim

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

Gostaria de transpô- lo de uma forma eficiente usando apenas comandos bash (eu poderia escrever um script Perl de dez ou mais linhas para fazer isso, mas deve ser mais lento para executar do que as funções bash nativas). Portanto, a saída deve ser semelhante a

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

Eu pensei em uma solução 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

Mas é lento e não parece a solução mais eficiente. Eu vi uma solução para o vi neste post , mas ainda está lento demais. Quaisquer pensamentos / sugestões / ideias brilhantes? :-)


12
O que o faz pensar que existiria um script bash que seria mais rápido do que um script Perl? Este é exatamente o tipo de problema em que Perl se destaca.
Mark Pim

1
@mark, se for puro bash, pode ser mais rápido do que encadear todas as ferramentas cut / sed etc. Mas, novamente, se você definir "bash" como combinação de ferramentas, apenas escrever um script awk será comparável ao processamento de texto de escrita em Perl.
ghostdog74

Adicione outro por não entender como o perl seria lento aqui. Lento para escrever o código? Lento para executar? Eu realmente não gosto do perl, mas ele se destaca nesse tipo de tarefa.
Corey Porter

Se suas colunas / campos têm tamanho / largura fixos, você pode usar o arquivo Python para evitar a leitura do arquivo na memória. Você fixou tamanhos / larguras de coluna / campo?
tommy.carstensen

2
Qualquer um que pense que um script de shell seria mais rápido do que awk ou perl precisa ler unix.stackexchange.com/questions/169716/… para que possam entender por que não é o caso.
Ed Morton de

Respostas:


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

resultado

$ 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

Desempenho em relação à solução Perl de Jonathan em um arquivo de 10.000 linhas

$ 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 por Ed Morton (@ ghostdog74 sinta-se à vontade para deletar se você desaprovar).

Talvez esta versão com alguns nomes de variáveis ​​mais explícitos ajude a responder algumas das perguntas abaixo e, de modo geral, esclareça o que o script está fazendo. Ele também usa as guias como o separador que o OP havia originalmente solicitado para lidar com os campos vazios e, por coincidência, aprimora um pouco a saída para este caso específico.

$ 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

As soluções acima funcionarão em qualquer awk (exceto awk antigo e quebrado, é claro - há YMMV).

As soluções acima leem todo o arquivo para a memória - se os arquivos de entrada forem muito grandes para isso, você pode fazer o seguinte:

$ 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 quase não usa memória, mas lê o arquivo de entrada uma vez por número de campos em uma linha, portanto, será muito mais lento do que a versão que lê todo o arquivo na memória. Ele também assume que o número de campos é o mesmo em cada linha e usa GNU awk para ENDFILEe, ARGINDmas qualquer awk pode fazer o mesmo com testes em FNR==1e END.


E agora lidar com rótulos de linha e coluna também?
Jonathan Leffler

OK - você está correto; seus dados de amostra não correspondem aos dados de amostra da pergunta, mas seu código funciona bem com os dados de amostra da pergunta e fornece a saída necessária (dar ou receber espaço em branco x tabulação). Principalmente meu erro.
Jonathan Leffler

Timings interessantes - concordo que você vê um benefício de desempenho no awk. Eu estava usando MacOS X 10.5.8, que não usa 'gawk'; e eu estava usando Perl 5.10.1 (compilação de 32 bits). Percebi que seus dados eram de 10.000 linhas com 4 colunas por linha? De qualquer forma, não importa muito; ambos awk e perl são soluções viáveis ​​(e a solução awk é mais limpa - as verificações 'definidas' em meu Perl são necessárias para executar avisos livres sob estrito / avisos) e nenhum é desleixado e ambos são provavelmente muito mais rápidos do que o original solução de script de shell.
Jonathan Leffler

Na minha matriz original de 2.2GB, a solução perl é um pouco mais rápida que awk - 350.103s vs. 369.410s Eu estava usando perl 5.8.8 64 bits
Federico Giorgi

1
@ zx8754 esse número máximo de campos se aplica apenas a um awk antigo não POSIX. Possivelmente o incrivelmente infelizmente chamado de "nawk". Não se aplica a gawk ou outros awks modernos.
Ed Morton de

47

Outra opção é usar rs:

rs -c' ' -C' ' -T

-caltera o separador da coluna de entrada, -Caltera o separador da coluna de saída e -Ttranspõe linhas e colunas. Não use em -tvez de -T, porque ele usa um número calculado automaticamente de linhas e colunas que geralmente não é correto. rs, que tem o nome da função reshape em APL, vem com BSDs e OS X, mas deve estar disponível em gerenciadores de pacotes em outras plataformas.

Uma segunda opção é usar Ruby:

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

Uma terceira opção é usar jq:

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

jq -R .imprime cada linha de entrada como um literal de string JSON, -s( --slurp) cria um array para as linhas de entrada depois de analisar cada linha como JSON e -r( --raw-output) produz o conteúdo de strings em vez de literais de string JSON. O /operador está sobrecarregado para dividir strings.


3
Eu não estava familiarizado com rs- obrigado pelo ponteiro! (O link é para o Debian; o upstream parece ser mirbsd.org/MirOS/dist/mir/rs )
tripleee

2
@lalebarde Pelo menos na implementação de rsque vem com o OS X, -csozinho define o separador de coluna de entrada para uma guia.
nisetama

2
@lalebarde, experimente a citação ANSI-C do bash para obter um caractere de tabulação:$'\t'
glenn jackman de

3
Este é um caso extremo, mas para um arquivo muito grande com muitas linhas como TTC TTA TTC TTC TTT, executando rs -c' ' -C' ' -T < rows.seq > cols.seqrs: no memory: Cannot allocate memory. Este é um sistema rodando FreeBSD 11.0-RELEASE com 32 GB de RAM. Então, meu palpite é que isso rscoloca tudo na RAM, o que é bom para velocidade, mas não para dados grandes.
jrm

1
jq usou 21 GB de RAM em um arquivo de 766 MB. Eu matei após 40 minutos sem qualquer saída.
Glubbdrubb

30

Uma solução 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

O acima é baseado no seguinte:

import sys

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

Este código assume que cada linha tem o mesmo número de colunas (nenhum preenchimento é executado).


3
Um pequeno problema aqui: Substitua l.split()por l.strip().split()(Python 2.7), caso contrário, a última linha da saída ficará prejudicada. Funciona para separadores de coluna arbitrários, use l.strip().split(sep)e sep.join(c)se o seu separador estiver armazenado na variável sep.
krlmlr

21

o projeto transpose no sourceforge é um programa C semelhante ao coreutil para exatamente isso.

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

Obrigado pelo link. No entanto, requer muita memória, ao lidar com matrizes / arquivos grandes.
tommy.carstensen

ele tem argumentos para tamanho de bloco e tamanho de campo: tente ajustar os argumentos -be -f.
ovelhas voadoras de

O tamanho do bloco padrão (--block ou -b) é 10kb e o tamanho do campo padrão (--fieldmax ou -f) é 64, então não pode ser isso. Eu tentei. Obrigado pela sugestão embora.
tommy.carstensen

1
Funcionou bem com um csv de 2 GB.
discipulus

2
Para um arquivo de matriz com dimensões de aproximadamente 11k por 5k, descobri que o transpose.c é ~ 7x mais rápido e ~ 5x mais eficiente em termos de memória do que a primeira solução awk do ghostdog74. Além disso, descobri que o código awk "quase não usa memória" do ghostdog74 não funcionou corretamente. Além disso, atente para o sinalizador --limit no programa transpose.c, que por padrão limita a saída para dimensionar 1k por 1k.
ncemami

16

BASH puro, nenhum processo adicional. Um bom exercício:

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

Isso funcionou para o meu arquivo, embora seja interessante que ele imprima uma lista de diretórios para a primeira linha da tabela. Não sei BASH o suficiente para descobrir por quê.
bugloaf de

@bugloaf sua mesa tem um * no canto.
Hello71

2
@bugloaf: Citar variáveis ​​corretamente deve evitar que:printf "%s\t" "${array[$COUNTER]}"
Pausado até novo aviso.

16

Dê uma olhada no GNU datamash que pode ser usado como datamash transpose. Uma versão futura também suportará tabulação cruzada (tabelas dinâmicas)


9

Aqui está um script Perl moderadamente sólido para fazer o trabalho. Existem muitas analogias estruturais com a awksolução 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";
}

Com o tamanho dos dados de amostra, a diferença de desempenho entre perl e awk era insignificante (1 milissegundo de um total de 7). Com um conjunto de dados maior (matriz 100x100, entradas de 6 a 8 caracteres cada), perl teve desempenho ligeiramente superior ao awk - 0,026s vs 0,042s. Provavelmente, nenhum dos dois será um problema.


Temporizações representativas para Perl 5.10.1 (32 bits) vs awk (versão 20040207 quando fornecido '-V') vs gawk 3.1.7 (32 bits) no MacOS X 10.5.8 em um arquivo contendo 10.000 linhas com 5 colunas por linha:

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: 

Observe que o gawk é muito mais rápido do que o awk nesta máquina, mas ainda mais lento do que o perl. Claramente, sua milhagem irá variar.


no meu sistema, gawk supera perl. você pode ver meus resultados na minha postagem editada
ghostdog74

4
conclusão obtida: plataforma diferente, versão de software diferente, resultados diferentes.
ghostdog74

6

Se você scinstalou, você pode fazer:

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

4
Observe que isso oferece suporte a um número limitado de linhas porque scnomeia suas colunas como um ou uma combinação de dois caracteres. O limite é 26 + 26^2 = 702.
Thor


5

Supondo que todas as suas linhas tenham o mesmo número de campos, este programa awk resolve o problema:

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

Em palavras, conforme você percorre as linhas, para cada campo fcresce uma string separada por ':' col[f]contendo os elementos daquele campo. Depois de terminar com todas as linhas, imprima cada uma dessas strings em uma linha separada. Você pode então substituir ':' pelo separador que deseja (digamos, um espaço) canalizando a saída tr ':' ' '.

Exemplo:

$ 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 é perfeitamente adequado para este problema com apenas uma linha de código e tamanho de arquivo potencialmente arbitrariamente grande!

datamash -W transpose infile > outfile

3

Uma solução perl hackish pode ser assim. É bom porque não carrega todos os arquivos na memória, imprime arquivos temporários intermediários e, em seguida, usa a pasta maravilhosa

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

usar arquivos colar e temporários são apenas operações extras desnecessárias. você pode apenas fazer a manipulação dentro da própria memória, por exemplo, arrays / hashes
ghostdog74

2
Sim, mas isso não significaria manter tudo na memória? Os arquivos com os quais estou lidando têm cerca de 2 a 20 GB de tamanho.
Federico Giorgi

3

A única melhoria que posso ver em seu próprio exemplo é usar o awk, que reduzirá o número de processos executados e a quantidade de dados canalizados entre eles:

/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

Eu normalmente uso este pequeno awksnippet 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

Isso apenas carrega todos os dados em uma matriz bidimensional a[line,column]e, em seguida, imprime de volta como a[column,line], de modo que transpõe a entrada fornecida.

Isso precisa manter o controle da maxquantidade máxima de colunas que o arquivo inicial possui, de modo que seja usado como o número de linhas a serem impressas de volta.


2

Usei a solução do fgm (obrigado fgm!), Mas precisava eliminar os caracteres de tabulação no final de cada linha, então modifiquei o script assim:

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

Eu estava apenas procurando por uma base bash semelhante, mas com suporte para preenchimento. Aqui está o script que escrevi com base na solução da fgm, que parece funcionar. Se puder ajudar ...

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

Eu estava procurando uma solução para transpor qualquer tipo de matriz (nxn ou mxn) com qualquer tipo de dado (números ou dados) e consegui a seguinte solução:

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

Se você quiser apenas pegar uma única linha $ N (delimitada por vírgulas) de um arquivo e transformá-la em uma coluna:

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

2

Não é muito elegante, mas este comando de "linha única" resolve o problema rapidamente:

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

Aqui, cols é o número de colunas, onde você pode substituir 4 por head -n 1 input | wc -w.


2

Outra awksolução e entrada limitada com o tamanho da memória que você tem.

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

Isso une cada posição do mesmo número de arquivo e ENDimprime o resultado que seria a primeira linha na primeira coluna, a segunda linha na segunda coluna, etc.

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

2

Alguns * nix padrão util one-liners, nenhum arquivo temporário necessário. NB: o OP queria uma solução eficiente (ou seja, mais rápida), e as principais respostas geralmente são mais rápidas do que esta resposta. Esses one-liners são para aqueles que gostam de ferramentas de software * nix , por qualquer motivo. Em casos raros ( por exemplo, IO e memória escassos), esses trechos podem ser mais rápidos do que algumas das principais respostas.

Chame o arquivo de entrada de foo .

  1. Se soubermos que foo tem quatro colunas:

    for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done
  2. Se não soubermos quantas colunas foo tem:

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

    xargstem um limite de tamanho e, portanto, tornaria o trabalho incompleto com um arquivo longo. O limite de tamanho depende do sistema, por exemplo:

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

    Comprimento máximo do comando que podemos realmente usar: 2088944

  3. tr& echo:

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

    ... ou se o número de colunas for desconhecido:

    n=$(head -n 1 foo | wc -w)
    for f in $(seq 1 $n); do 
        cut -d ' ' -f $f foo | tr '\n' ' ' ; echo
    done
  4. Usando set, que, assim xargs, tem limitações baseadas no tamanho da linha de comando semelhantes:

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

2
Tudo isso seria muito mais lento do que uma solução awk ou perl e frágil. Leia unix.stackexchange.com/questions/169716/… .
Ed Morton de

@EdMorton, obrigado, introdução qualificada da minha resposta para abordar suas preocupações com a velocidade. Re "frágil": não 3) , e nem os demais quando o programador sabe que os dados são seguros para uma dada técnica; e o código de shell compatível com POSIX não é um padrão mais estável do que o perl ?
agc de

desculpe, idk muito sobre perl. Nesse caso, a ferramenta a ser usada seria awk. cut, head, echo, Etc. não são mais POSIX código shell compatíveis do que um awkscript é - todos eles são padrão em cada instalação UNIX. Simplesmente não há razão para usar um conjunto de ferramentas que, em combinação, exigem que você tome cuidado com o conteúdo do seu arquivo de entrada e com o diretório de execução do script, quando você pode apenas usar o awk e o resultado final é mais rápido e mais robusto .
Ed Morton de

Por favor, não sou anti- awk , mas as condições variam. Razão nº 1: for f in cut head xargs seq awk ; do wc -c $(which $f) ; done quando o armazenamento é muito lento ou o IO é muito baixo, intérpretes maiores tornam as coisas piores, não importa o quão bons seriam em circunstâncias mais ideais. Razão # 2: awk , (ou qualquer linguagem), também sofre de uma curva de aprendizado mais íngreme do que um pequeno utilitário projetado para fazer bem uma coisa. Quando o tempo de execução é mais barato do que horas de trabalho do codificador, a codificação fácil com "ferramentas de software" economiza dinheiro.
agc de

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

outra versão com set eval


Leia unix.stackexchange.com/questions/169716/… para entender alguns, mas não todos, os problemas com essa solução.
Ed Morton

1

Outra variante do bash

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

Roteiro

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

Resultado

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

0

Aqui está uma solução Haskell. Quando compilado com -O2, ele é executado um pouco mais rápido do que o awk do ghostdog e um pouco mais lento do que o c python c de Stephan em minha máquina para linhas de entrada "Hello world" repetidas. Infelizmente, o suporte do GHC para a passagem de código de linha de comando é inexistente, pelo que eu posso dizer, então você terá que escrevê-lo em um arquivo. Isso truncará as linhas no comprimento da linha mais curta.

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

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

0

Uma solução awk que armazena todo o array na memória

    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

Mas podemos "percorrer" o arquivo quantas vezes forem necessárias as linhas de saída:

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

Que (para uma contagem baixa de linhas de saída é mais rápido do que o código anterior).


0

Aqui está um one-liner do Bash que se baseia na simples conversão de cada linha em uma coluna e pastejuntá-las:

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. cria o tmp1arquivo para que não fique vazio.

  2. lê cada linha e a transforma em uma coluna usando tr

  3. cola a nova coluna no tmp1arquivo

  4. o resultado das cópias de volta para tmp1.

PS: Eu realmente queria usar descritores io, mas não consegui fazê-los funcionar.


Certifique-se de definir um despertador se for executá-lo em um arquivo grande. Leia unix.stackexchange.com/questions/169716/… para entender alguns, mas não todos, os problemas com essa abordagem.
Ed Morton de

0

Um oneliner usando 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

Eu usei a seguir dois scripts para fazer operações semelhantes antes. O primeiro está em awk, que é muito mais rápido do que o segundo, em bash "puro". Você pode ser capaz de adaptá-lo ao seu próprio aplicativo.

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.