Un modo efficiente per trasporre un file in Bash


110

Ho un enorme file separato da tabulazioni formattato in questo modo

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

Vorrei trasporlo in modo efficiente usando solo i comandi bash (potrei scrivere uno script Perl di circa dieci righe per farlo, ma dovrebbe essere più lento da eseguire rispetto alle funzioni bash native). Quindi l'output dovrebbe essere simile a

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

Ho pensato a una soluzione come questa

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

Ma è lento e non sembra la soluzione più efficiente. Ho visto una soluzione per vi in questo post , ma è ancora troppo lento. Pensieri / suggerimenti / idee brillanti? :-)


12
Cosa ti fa pensare che esisterebbe uno script bash che sarà più veloce di uno script Perl? Questo è esattamente il tipo di problema in cui eccelle Perl.
Mark Pim

1
@mark, se è puro bash, potrebbe essere più veloce del concatenamento di tutti gli strumenti cut / sed ecc. Ma poi di nuovo, se definisci "bash" come nella combinazione di strumenti, allora la semplice scrittura di uno script awk sarà paragonabile all'elaborazione del testo di Perl.
ghostdog74

Aggiungine un altro per non capire come perl sarebbe lento qui. Lento a scrivere il codice? Lento da eseguire? Sinceramente non mi piace perl, ma eccelle in questo tipo di attività.
Corey Porter,

Se le tue colonne / campi hanno una dimensione / larghezza fissa, puoi usare la ricerca di file Python per evitare di leggere il tuo file in memoria. Hai dimensioni / larghezze di colonne / campi fissi?
tommy.carstensen

2
Chiunque pensi che uno script di shell sarebbe più veloce di awk o perl deve leggere unix.stackexchange.com/questions/169716/… in modo da poter capire perché non è così.
Ed Morton

Risposte:


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

produzione

$ 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

Prestazioni contro la soluzione Perl di Jonathan su un file di 10000 righe

$ 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 di Ed Morton (@ ghostdog74 sentiti libero di eliminare se disapprovi).

Forse questa versione con alcuni nomi di variabili più espliciti aiuterà a rispondere ad alcune delle domande seguenti e in generale a chiarire cosa sta facendo lo script. Usa anche le tabulazioni come separatore che l'OP aveva originariamente richiesto in modo da gestire i campi vuoti e, per coincidenza, abbellisce un po 'l'output per questo caso particolare.

$ 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

Le soluzioni di cui sopra funzioneranno in qualsiasi awk (eccetto ovviamente il vecchio awk rotto - lì YMMV).

Le soluzioni di cui sopra leggono l'intero file in memoria, tuttavia, se i file di input sono troppo grandi per quello, puoi farlo:

$ 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

che non utilizza quasi memoria ma legge il file di input una volta per numero di campi su una riga, quindi sarà molto più lenta della versione che legge l'intero file in memoria. Presume inoltre che il numero di campi sia lo stesso su ogni riga e utilizza GNU awk per ENDFILEe ARGINDma qualsiasi awk può fare lo stesso con i test su FNR==1e END.


E ora per gestire anche le etichette di riga e colonna?
Jonathan Leffler

OK, hai ragione; i tuoi dati di esempio non corrispondono ai dati di esempio della domanda, ma il tuo codice funziona bene sui dati di esempio della domanda e fornisce l'output richiesto (dare o prendere spaziatura tra spazi vuoti e tabulazioni). Principalmente il mio errore.
Jonathan Leffler

Tempi interessanti: sono d'accordo che vedi un vantaggio in termini di prestazioni in awk. Stavo usando MacOS X 10.5.8, che non usa "gawk"; e stavo usando Perl 5.10.1 (build a 32 bit). Ho capito che i tuoi dati erano 10000 righe con 4 colonne per riga? Comunque, non ha molta importanza; sia awk che perl sono soluzioni praticabili (e la soluzione awkèpiù ordinata - i controlli 'definiti' nel mio Perl sono necessari per le esecuzioni libere di avvertimento sotto strict / warnings) e nessuno dei due è uno slouch ed entrambi sono probabilmente molto più veloci dell'originale soluzione di script di shell.
Jonathan Leffler

Sulla mia matrice originale da 2.2GB, la soluzione perl è leggermente più veloce di awk - 350.103s contro 369.410s stavo usando perl 5.8.8 64bit
Federico Giorgi

1
@ zx8754 quel numero massimo di campi si applica solo a un vecchio awk non POSIX. Forse il nome incredibilmente sfortunatamente "nawk". Non si applica a gawk o altri awk moderni.
Ed Morton

47

Un'altra opzione è usare rs:

rs -c' ' -C' ' -T

-ccambia il separatore della colonna di input, -Ccambia il separatore della colonna di output e -Ttraspone righe e colonne. Non utilizzare al -tposto di -T, perché utilizza un numero calcolato automaticamente di righe e colonne che di solito non è corretto. rs, che prende il nome dalla funzione reshape in APL, viene fornito con BSD e OS X, ma dovrebbe essere disponibile dai gestori di pacchetti su altre piattaforme.

Una seconda opzione è usare Ruby:

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

Una terza opzione è usare jq:

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

jq -R .stampa ogni riga di input come una stringa letterale JSON, -s( --slurp) crea un array per le righe di input dopo aver analizzato ogni riga come JSON e -r( --raw-output) restituisce il contenuto delle stringhe invece delle stringhe letterali JSON. L' /operatore è sovraccarico per dividere le stringhe.


3
Non avevo familiarità con rs- grazie per il puntatore! (Il collegamento è a Debian; l'upstream sembra essere mirbsd.org/MirOS/dist/mir/rs )
tripleee

2
@lalebarde Almeno nell'implementazione di rsciò viene fornito con OS X, -cda solo imposta il separatore della colonna di input su una scheda.
nisetama

2
@lalebarde, prova la citazione ANSI-C di bash per ottenere un carattere di tabulazione:$'\t'
glenn jackman

3
Questo è un caso estremo, ma per un file molto grande con molte righe come TTC TTA TTC TTC TTT, l'esecuzione rs -c' ' -C' ' -T < rows.seq > cols.seqrs: no memory: Cannot allocate memory. Questo è un sistema che esegue FreeBSD 11.0-RELEASE con 32 GB di ram. Quindi, la mia ipotesi è che rsmetta tutto nella RAM, il che è buono per la velocità, ma non per i dati di grandi dimensioni.
jrm

1
jq ha utilizzato 21 GB di RAM su un file da 766 MB. L'ho ucciso dopo 40 minuti senza alcun output.
Glubbdrubb

30

Una soluzione 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

Quanto sopra si basa su quanto segue:

import sys

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

Questo codice presuppone che ogni riga abbia lo stesso numero di colonne (non viene eseguito alcun riempimento).


3
Un piccolo problema qui: sostituire l.split()con l.strip().split()(Python 2.7), altrimenti l'ultima riga dell'output è paralizzata. Funziona per separatori di colonna arbitrari, usa l.strip().split(sep)e sep.join(c)se il tuo separatore è memorizzato in variabile sep.
krlmlr

21

il progetto transpose su sourceforge è un programma C simile a coreutil proprio per questo.

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

Grazie per il collegamento. Tuttavia, richiede troppa memoria, quando si tratta di matrici / file di grandi dimensioni.
tommy.carstensen

ha argomenti per blockize e fieldize: prova a modificare gli argomenti -be -f.
pecore volanti l'

La dimensione predefinita del blocco (--block o -b) è 10kb e la dimensione del campo predefinita (--fieldmax o -f) è 64, quindi non può essere così. Provai. Grazie per il suggerimento però.
tommy.carstensen

1
Ha funzionato bene con un CSV di dimensioni 2 GB.
discipulus

2
Per un file matrice con dimensioni di circa 11k per 5k, ho trovato transpose.c ~ 7 volte più veloce e ~ 5 volte più efficiente in termini di memoria rispetto alla prima soluzione awk di ghostdog74. Inoltre, ho scoperto che il codice awk "non utilizza quasi memoria" da ghostdog74 non funzionava correttamente. Inoltre, fai attenzione al flag --limit nel programma transpose.c, che per impostazione predefinita limita l'output alla dimensione 1k per 1k.
ncemami

16

Pure BASH, nessun processo aggiuntivo. Un bel esercizio:

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

Questo ha funzionato per il mio file, anche se è interessante notare che stampa un elenco di directory per la prima riga della tabella. Non so abbastanza BASH per capire perché.
bugloaf

@bugloaf il tuo tavolo ha un * nell'angolo.
Ciao 71

2
@ Bugloaf: la citazione corretta delle variabili dovrebbe impedire che:printf "%s\t" "${array[$COUNTER]}"
In pausa fino a nuovo avviso.

16

Dai un'occhiata al datamash GNU che può essere usato come datamash transpose. Una versione futura supporterà anche la tabulazione incrociata (tabelle pivot)


9

Ecco uno script Perl moderatamente solido per fare il lavoro. Esistono molte analogie strutturali con la awksoluzione di @ 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 la dimensione dei dati del campione, la differenza di prestazioni tra perl e awk era trascurabile (1 millisecondo su 7 totali). Con un set di dati più ampio (matrice 100x100, voci di 6-8 caratteri ciascuna), perl ha leggermente sovraperformato awk - 0,026 s contro 0,042. Nessuno dei due è probabile che sia un problema.


Tempi rappresentativi per Perl 5.10.1 (32 bit) vs awk (versione 20040207 quando viene fornito '-V') vs gawk 3.1.7 (32 bit) su MacOS X 10.5.8 su un file contenente 10.000 righe con 5 colonne per linea:

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: 

Nota che gawk è molto più veloce di awk su questa macchina, ma comunque più lento di perl. Chiaramente, il tuo chilometraggio varierà.


sul mio sistema, gawk supera perl. puoi vedere i miei risultati nel mio post modificato
ghostdog74

4
Conclusioni raccolte: piattaforma diversa, versione software diversa, risultati diversi.
ghostdog74

6

Se hai scinstallato, puoi fare:

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

4
Notare che questo supporta un numero limitato di righe perché scnomina le sue colonne come uno o una combinazione di due caratteri. Il limite è 26 + 26^2 = 702.
Thor


5

Supponendo che tutte le tue righe abbiano lo stesso numero di campi, questo programma awk risolve il problema:

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

In parole, quando si esegue il ciclo sulle righe, per ogni campo fcresce una stringa separata da ":" col[f]contenente gli elementi di quel campo. Dopo aver terminato con tutte le righe, stampa ciascuna di quelle stringhe in una riga separata. È quindi possibile sostituire ':' per il separatore che si desidera (ad esempio, uno spazio) convogliando l'output attraverso tr ':' ' '.

Esempio:

$ 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

Il datamash GNU è perfettamente adatto a questo problema con una sola riga di codice e dimensioni del file potenzialmente arbitrariamente grandi!

datamash -W transpose infile > outfile

3

Una soluzione perl hacker può essere così. È bello perché non carica tutto il file in memoria, stampa file temporanei intermedi e quindi utilizza la pasta meravigliosa

#!/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'utilizzo di incolla e file temporanei sono solo operazioni extra non necessarie. puoi semplicemente manipolare all'interno della memoria stessa, ad esempio array / hash
ghostdog74

2
Sì, ma non significherebbe tenere tutto in memoria? I file con cui ho a che fare hanno una dimensione di circa 2-20 GB.
Federico Giorgi

3

L'unico miglioramento che posso vedere nel tuo esempio è l'uso di awk che ridurrà il numero di processi eseguiti e la quantità di dati che vengono convogliati tra di loro:

/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 questo piccolo awkframmento per questo 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

Questo carica semplicemente tutti i dati in un array bidimensionale a[line,column]e poi li stampa indietro come a[column,line], in modo che trasponga l'input dato.

Questo deve tenere traccia della maxquantità totale di colonne del file iniziale, in modo che venga utilizzato come numero di righe da stampare.


2

Ho usato la soluzione di fgm (grazie fgm!), Ma dovevo eliminare i caratteri di tabulazione alla fine di ogni riga, quindi ho modificato lo script in questo modo:

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

Stavo solo cercando una simile trasposizione bash ma con supporto per il riempimento. Ecco lo script che ho scritto sulla base della soluzione di fgm, che sembra funzionare. Se può essere d'aiuto ...

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

Stavo cercando una soluzione per trasporre qualsiasi tipo di matrice (nxn o mxn) con qualsiasi tipo di dati (numeri o dati) e ho ottenuto la seguente soluzione:

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 vuoi solo prendere una singola riga (delimitata da virgole) $ N da un file e trasformarla in una colonna:

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

2

Non molto elegante, ma questo comando "a riga singola" risolve rapidamente il problema:

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

Qui cols è il numero di colonne, dove puoi sostituire 4 con head -n 1 input | wc -w.


2

Un'altra awksoluzione e input limitato con la dimensione della memoria che hai.

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

Questo unisce ogni posizione dello stesso numero di file in insieme e ENDstampa il risultato che sarebbe la prima riga nella prima colonna, la seconda riga nella seconda colonna, ecc.

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

2

Alcuni * nix standard utilizzano one-liner, non sono necessari file temporanei. NB: l'OP voleva una soluzione efficiente , (cioè più veloce), e le risposte migliori sono solitamente più veloci di questa risposta. Queste battute sono per coloro a cui piacciono gli strumenti software * nix , per qualsiasi motivo. In rari casi ( ad esempio, IO e memoria scarsi), questi frammenti possono effettivamente essere più veloci di alcune delle risposte principali.

Chiama il file di input foo .

  1. Se sappiamo che foo ha quattro colonne:

    for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done
  2. Se non sappiamo quante colonne ha foo :

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

    xargsha un limite di dimensione e quindi renderebbe incompleto il lavoro con un file lungo. Quale limite di dimensione dipende dal sistema, ad esempio:

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

    Lunghezza massima del comando che potremmo effettivamente usare: 2088944

  3. tr& echo:

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

    ... o se il numero di colonne è sconosciuto:

    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'utilizzo set, che come xargs, ha limitazioni basate sulle dimensioni della riga di comando simili:

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

2
Sarebbero tutti ordini di grandezza più lenti di una soluzione awk o perl e fragili. Leggi unix.stackexchange.com/questions/169716/… .
Ed Morton

@EdMorton, grazie, introduzione qualificata della mia risposta per affrontare i tuoi problemi di velocità. Re "fragile": non 3) , né gli altri quando il programmatore sa che i dati sono sicuri per una data tecnica; e il codice shell compatibile con POSIX non è uno standard più stabile di perl ?
agc

scusate, idk molto su perl. In questo caso lo strumento da utilizzare sarebbe awk. cut, head, echo, Ecc non sono più POSIX codice shell compatibile che uno awkscript è - sono tutti di serie su ogni installazione UNIX. Semplicemente non c'è motivo di usare un set di strumenti che in combinazione richiedono di stare attenti al contenuto del file di input e alla directory da cui si esegue lo script quando si può semplicemente usare awk e il risultato finale è più veloce oltre che più robusto .
Ed Morton

Per favore, non sono anti- awk , ma le condizioni variano. Motivo n. 1: for f in cut head xargs seq awk ; do wc -c $(which $f) ; done quando l'archiviazione è troppo lenta o l'IO è troppo basso, interpreti più grandi peggiorano le cose, indipendentemente da quanto sarebbero bravi in ​​circostanze più ideali. Motivo n. 2: awk , (o quasi tutti i linguaggi), soffre anche di una curva di apprendimento più ripida rispetto a una piccola utility progettata per fare bene una cosa. Quando il tempo di esecuzione è più economico delle ore di lavoro del programmatore, la facile codifica con "strumenti software" consente di risparmiare denaro.
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

un'altra versione con set eval


Leggi unix.stackexchange.com/questions/169716/… per comprendere alcuni, ma non tutti, i problemi con quella soluzione.
Ed Morton

1

Un'altra variante di bash

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

copione

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

Produzione

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

0

Ecco una soluzione Haskell. Quando compilato con -O2, gira leggermente più veloce di awk di ghostdog e leggermente più lento del c python di Stephan avvolto in modo sottile sulla mia macchina per le righe di input ripetute "Hello world". Sfortunatamente il supporto di GHC per il passaggio del codice della riga di comando è inesistente per quanto ne so, quindi dovrai scriverlo su un file da solo. Troncerà le righe alla lunghezza della riga più corta.

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

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

0

Una soluzione awk che archivia l'intero array in 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

Ma possiamo "esplorare" il file tante volte quante sono necessarie le righe di output:

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

Quale (per un conteggio basso di righe di output è più veloce del codice precedente).


0

Ecco una riga di Bash basata sulla semplice conversione di ciascuna riga in una colonna e sul pasteloro insieme:

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 un tmp1file in modo che non sia vuoto.

  2. legge ogni riga e la trasforma in una colonna usando tr

  3. incolla la nuova colonna nel tmp1file

  4. le copie risultano di nuovo in tmp1.

PS: volevo davvero usare i descrittori io ma non sono riuscito a farli funzionare.


Assicurati di impostare una sveglia se hai intenzione di eseguirla su un file di grandi dimensioni. Leggi unix.stackexchange.com/questions/169716/… per comprendere alcuni, ma non tutti, i problemi con questo approccio.
Ed Morton

0

Un oneliner che usa 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

Ho usato di seguito due script per eseguire operazioni simili in precedenza. Il primo è in awk che è molto più veloce del secondo che è in bash "puro". Potresti essere in grado di adattarlo alla tua applicazione.

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.