Eine effiziente Möglichkeit, eine Datei in Bash zu transponieren


110

Ich habe eine riesige tabulatorgetrennte Datei, die so formatiert ist

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

Ich möchte es auf effiziente Weise nur mit Bash-Befehlen transponieren (ich könnte dazu ein Perl-Skript mit etwa zehn Zeilen schreiben, aber es sollte langsamer ausgeführt werden als die nativen Bash-Funktionen). Die Ausgabe sollte also so aussehen

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

Ich dachte an eine solche Lösung

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

Aber es ist langsam und scheint nicht die effizienteste Lösung zu sein. Ich habe in diesem Beitrag eine Lösung für vi gesehen , aber sie ist immer noch zu langsam. Irgendwelche Gedanken / Vorschläge / geniale Ideen? :-)


12
Was lässt Sie denken, dass es ein Bash-Skript geben würde, das schneller sein wird als ein Perl-Skript? Dies ist genau die Art von Problem, in der sich Perl auszeichnet
Mark Pim,

1
@mark, wenn es eine reine Bash ist, könnte es zu schnell sein, als all diese geschnittenen / sed usw. Werkzeuge miteinander zu verketten. Wenn Sie jedoch "bash" wie beim Kombinieren von Werkzeugen definieren, ist das Schreiben eines awk-Skripts mit der Perl-Textverarbeitung vergleichbar.
Ghostdog74

Fügen Sie eine weitere hinzu, um nicht zu verstehen, wie langsam Perl hier sein würde. Langsam den Code schreiben? Langsam auszuführen? Ich mag Perl wirklich nicht, aber es zeichnet sich bei dieser Art von Aufgabe aus.
Corey Porter

Wenn Ihre Spalten / Felder eine feste Größe / Breite haben, können Sie die Python-Dateisuche verwenden, um das Einlesen Ihrer Datei in den Speicher zu vermeiden. Haben Sie feste Spalten- / Feldgrößen / -breiten?
Tommy.carstensen

2
Jeder, der glaubt, ein Shell-Skript wäre schneller als awk oder perl, muss unix.stackexchange.com/questions/169716/… lesen, damit er verstehen kann, warum dies nicht der Fall ist.
Ed Morton

Antworten:


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

Ausgabe

$ 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

Leistung gegen Perl-Lösung von Jonathan in einer 10000-Zeilen-Datei

$ 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 von Ed Morton (@ ghostdog74 kann jederzeit gelöscht werden, wenn Sie dies ablehnen).

Vielleicht hilft diese Version mit einigen expliziteren Variablennamen dabei, einige der folgenden Fragen zu beantworten und allgemein zu klären, was das Skript tut. Es werden auch Tabulatoren als Trennzeichen verwendet, nach denen das OP ursprünglich gefragt hatte, damit leere Felder verarbeitet werden können, und es verschönert zufällig die Ausgabe für diesen speziellen Fall ein wenig.

$ 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

Die oben genannten Lösungen funktionieren in jedem awk (außer natürlich alten, kaputten awk - dort YMMV).

Die oben genannten Lösungen lesen jedoch die gesamte Datei in den Speicher. Wenn die Eingabedateien dafür zu groß sind, können Sie dies tun:

$ 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

Dies verwendet fast keinen Speicher, liest jedoch die Eingabedatei einmal pro Anzahl von Feldern in einer Zeile, sodass sie viel langsamer ist als die Version, die die gesamte Datei in den Speicher liest. Es geht auch davon die Anzahl der Felder in jeder Zeile das gleiche ist und verwendet GNU awk für ENDFILEund ARGINDaber jede awk kann auf das gleiche mit Tests durchführen FNR==1und END.


Und jetzt auch mit Zeilen- und Spaltenbeschriftungen umgehen?
Jonathan Leffler

OK - du hast recht; Ihre Beispieldaten stimmen nicht mit den Beispieldaten der Frage überein, aber Ihr Code funktioniert einwandfrei mit den Beispieldaten der Frage und gibt die erforderliche Ausgabe aus (Leerzeichen oder Tabulatorabstand geben oder nehmen). Hauptsächlich mein Fehler.
Jonathan Leffler

Interessante Timings - Ich bin damit einverstanden, dass Sie in awk einen Leistungsvorteil sehen. Ich habe MacOS X 10.5.8 verwendet, das 'gawk' nicht verwendet. und ich habe Perl 5.10.1 (32-Bit-Build) verwendet. Ich erfahre, dass Ihre Daten 10000 Zeilen mit 4 Spalten pro Zeile waren? Wie auch immer, es spielt keine große Rolle; Sowohl awk als auch perl sind praktikable Lösungen (und die awk-Lösung ist ordentlicher - die "definierten" Überprüfungen in meinem Perl sind erforderlich, um freie Läufe unter strengen / Warnungen zu warnen), und keiner ist ein Slouch, und beide sind wahrscheinlich viel schneller als das Original Shell-Skript-Lösung.
Jonathan Leffler

Auf meiner ursprünglichen 2,2-GB-Matrix ist die Perl-Lösung etwas schneller als awk - 350,103s gegenüber 369,410s. Ich habe Perl 5.8.8 64bit
Federico Giorgi

1
@ zx8754, dass die maximale Anzahl von Feldern nur für eine alte Nicht-POSIX-Awk gilt. Möglicherweise der unglaublich leider benannte "Nawk". Es gilt nicht für Gawk oder andere moderne Awks.
Ed Morton

46

Eine andere Option ist rs:

rs -c' ' -C' ' -T

-cÄndert das Trennzeichen für Eingabespalten, -Cändert das Trennzeichen für Ausgabespalten und -Ttransponiert Zeilen und Spalten. Verwenden Sie nicht -tanstelle von -T, da eine automatisch berechnete Anzahl von Zeilen und Spalten verwendet wird, die normalerweise nicht korrekt ist. rs, das nach der Umformungsfunktion in APL benannt ist, wird mit BSDs und OS X geliefert, sollte jedoch von Paketmanagern auf anderen Plattformen verfügbar sein.

Eine zweite Option ist die Verwendung von Ruby:

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

Eine dritte Option ist jq:

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

jq -R .druckt jede Eingabezeile als JSON-Zeichenfolgenliteral, -s( --slurp) erstellt ein Array für die Eingabezeilen, nachdem jede Zeile als JSON analysiert wurde, und -r( --raw-output) gibt den Inhalt von Zeichenfolgen anstelle von JSON-Zeichenfolgenliteralen aus. Der /Operator ist überladen, um Zeichenfolgen zu teilen.


3
Ich war nicht vertraut rs- danke für den Zeiger! (Der Link ist zu Debian; der Upstream scheint mirbsd.org/MirOS/dist/mir/rs zu sein )
Tripleee

2
@lalebarde Zumindest in der Implementierung rs, die mit OS X geliefert wird , wird -callein das Trennzeichen für die Eingabespalte auf eine Registerkarte gesetzt.
Nisetama

2
@lalebarde, versuchen Sie das ANSI-C-Zitat von bash , um ein Tabulatorzeichen zu erhalten:$'\t'
Glenn Jackman

3
Dies ist ein extremer Fall, aber für eine sehr große Datei mit vielen Zeilen wie TTC TTA TTC TTC TTT, rs -c' ' -C' ' -T < rows.seq > cols.seqgibt das Ausführen rs: no memory: Cannot allocate memory. Dies ist ein System, auf dem FreeBSD 11.0-RELEASE mit 32 GB RAM ausgeführt wird. Ich vermute also, dass rsalles im RAM gespeichert wird, was gut für die Geschwindigkeit ist, aber nicht für große Datenmengen.
jrm

1
jq verwendete 21 GB RAM für eine 766 MB-Datei. Ich habe es nach 40 Minuten ohne Ausgabe getötet.
Glubbdrubb

30

Eine Python-Lösung:

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

Das Obige basiert auf Folgendem:

import sys

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

Dieser Code setzt voraus, dass jede Zeile die gleiche Anzahl von Spalten hat (es wird kein Auffüllen durchgeführt).


3
Ein kleines Problem hier: Ersetzen l.split()durch l.strip().split()(Python 2.7), sonst ist die letzte Zeile der Ausgabe verkrüppelt. Funktioniert für beliebige Spaltentrennzeichen. Verwenden Sie l.strip().split(sep)und, sep.join(c)wenn Ihr Trennzeichen in einer Variablen gespeichert ist sep.
krlmlr

21

Das Transponierungsprojekt auf SourceForge ist genau dafür ein Coreutil-ähnliches C-Programm.

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

Danke für den Link. Beim Umgang mit großen Matrizen / Dateien ist jedoch zu viel Speicher erforderlich.
Tommy.carstensen

es Argumente für Blockgröße und Feldgröße hat: versuchen , die zwicken -bund -fArgumente.
fliegende Schafe

Die Standardblockgröße (--block oder -b) beträgt 10 KB und die Standardfeldgröße (--fieldmax oder -f) beträgt 64, das kann also nicht sein. Ich habe es versucht. Vielen Dank für den Vorschlag.
Tommy.carstensen

1
Funktionierte gut mit einer CSV-Größe von 2 GB.
Discipulus

2
Für eine Matrixdatei mit Abmessungen von ca. 11 x 5 KB stellte ich fest, dass transpose.c ~ 7x schneller und ~ 5x speichereffizienter ist als die erste awk-Lösung des Ghostdog74. Außerdem stellte ich fest, dass der awk-Code von ghostdog74 "verwendet fast keinen Speicher" nicht richtig funktionierte. Achten Sie auch auf das Flag --limit im Programm transpose.c, das die Ausgabe standardmäßig auf die Dimension 1k x 1k begrenzt.
Ncemami

16

Pure BASH, kein zusätzlicher Prozess. Eine schöne Übung:

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

Dies funktionierte für meine Datei, obwohl interessanterweise eine Verzeichnisliste für die erste Zeile der Tabelle ausgedruckt wird. Ich weiß nicht genug BASH, um herauszufinden, warum.
Bugloaf

@bugloaf Ihr Tisch hat ein * in der Ecke.
Hallo71

2
@bugloaf: Das korrekte Zitieren von Variablen sollte printf "%s\t" "${array[$COUNTER]}"
Folgendes

16

Schauen Sie sich GNU Datamash an, das wie verwendet werden kann datamash transpose. Eine zukünftige Version wird auch Kreuztabellen (Pivot-Tabellen) unterstützen.


9

Hier ist ein mäßig solides Perl-Skript, um die Arbeit zu erledigen. Es gibt viele strukturelle Analogien zur awkLösung von @ 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";
}

Bei der Größe der Probendaten war der Leistungsunterschied zwischen Perl und Awk vernachlässigbar (1 Millisekunde von insgesamt 7). Mit einem größeren Datensatz (100x100-Matrix, Einträge mit jeweils 6-8 Zeichen) übertraf Perl awk - 0,026s gegenüber 0,042s leicht. Beides dürfte kein Problem sein.


Repräsentative Timings für Perl 5.10.1 (32-Bit) vs awk (Version 20040207 bei Angabe von '-V') vs gawk 3.1.7 (32-Bit) unter MacOS X 10.5.8 in einer Datei mit 10.000 Zeilen mit 5 Spalten pro Linie:

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: 

Beachten Sie, dass Gawk auf diesem Computer viel schneller als Awk ist, aber immer noch langsamer als Perl. Ihr Kilometerstand wird natürlich variieren.


Auf meinem System übertrifft Gawk Perl. Sie können meine Ergebnisse in meinem bearbeiteten Beitrag sehen
ghostdog74

4
Schlussfolgerung: unterschiedliche Plattform, unterschiedliche Softwareversion, unterschiedliche Ergebnisse.
Ghostdog74

6

Wenn Sie scinstalliert haben, können Sie Folgendes tun:

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

4
Beachten Sie, dass dies eine begrenzte Anzahl von Zeilen unterstützt, da scdie Spalten als ein oder eine Kombination aus zwei Zeichen benannt werden. Die Grenze ist 26 + 26^2 = 702.
Thor


5

Angenommen, alle Ihre Zeilen haben die gleiche Anzahl von Feldern, löst dieses awk-Programm das Problem:

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

In Worten, wenn Sie die Zeilen durchlaufen, fwächst für jedes Feld eine durch ':' getrennte Zeichenfolge, col[f]die die Elemente dieses Felds enthält. Nachdem Sie mit allen Zeilen fertig sind, drucken Sie jede dieser Zeichenfolgen in einer separaten Zeile. Sie können dann das gewünschte Trennzeichen (z. B. ein Leerzeichen) durch ':' ersetzen, indem Sie die Ausgabe durchleitentr ':' ' ' .

Beispiel:

$ 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 ist mit nur einer Codezeile und möglicherweise beliebig großer Dateigröße perfekt für dieses Problem geeignet!

datamash -W transpose infile > outfile

3

Eine hackige Perl-Lösung kann so sein. Es ist schön, weil es nicht die gesamte Datei in den Speicher lädt, temporäre Zwischendateien druckt und dann die wundervolle Paste verwendet

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

Die Verwendung von Einfüge- und temporären Dateien ist nur ein zusätzlicher unnötiger Vorgang. Sie können einfach Manipulationen im Speicher selbst
vornehmen

2
Ja, aber würde das nicht bedeuten, alles im Gedächtnis zu behalten? Die Dateien, mit denen ich es zu tun habe, sind ungefähr 2-20 GB groß.
Federico Giorgi

3

Die einzige Verbesserung, die ich an Ihrem eigenen Beispiel sehen kann, ist die Verwendung von awk, wodurch die Anzahl der ausgeführten Prozesse und die Datenmenge, die zwischen ihnen geleitet wird, verringert werden:

/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

Normalerweise verwende ich dieses kleine awkSnippet für diese Anforderung:

  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

Dadurch werden nur alle Daten in ein zweidimensionales Array geladen a[line,column]und anschließend als gedruckta[column,line] , sodass die angegebene Eingabe transponiert wird.

Dies muss die maxAnzahl der Spalten in der ursprünglichen Datei verfolgen , damit sie als Anzahl der Zeilen verwendet werden kann, die zurückgedruckt werden sollen.


2

Ich habe die Lösung von fgm verwendet (danke fgm!), Musste aber die Tabulatorzeichen am Ende jeder Zeile entfernen, also habe ich das Skript folgendermaßen geändert:

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

Ich war nur auf der Suche nach einer ähnlichen Bash-Übertragung, aber mit Unterstützung für die Polsterung. Hier ist das Skript, das ich basierend auf der Lösung von fgm geschrieben habe und das zu funktionieren scheint. Wenn es hilfreich sein kann ...

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

Ich suchte nach einer Lösung, um jede Art von Matrix (nxn oder mxn) mit jeder Art von Daten (Zahlen oder Daten) zu transponieren und bekam die folgende Lösung:

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

Wenn Sie nur eine einzelne (durch Kommas getrennte) Zeile $ N aus einer Datei nehmen und in eine Spalte umwandeln möchten:

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

2

Nicht sehr elegant, aber dieser "einzeilige" Befehl löst das Problem schnell:

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

Hier ist cols die Anzahl der Spalten, in denen Sie 4 durch ersetzen können head -n 1 input | wc -w.


2

Eine andere awkLösung und begrenzte Eingabe mit der Größe des Speichers, den Sie haben.

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

Dies verbindet jede Position der gleichen abgelegten Nummer miteinander und ENDdruckt das Ergebnis aus, das die erste Zeile in der ersten Spalte, die zweite Zeile in der zweiten Spalte usw. wäre. Wird ausgegeben:

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

2

Einige * nix- Standards verwenden Einzeiler, es werden keine temporären Dateien benötigt. NB: Das OP wollte eine effiziente Lösung (dh schneller), und die Top-Antworten sind normalerweise schneller als diese Antwort. Diese Einzeiler sind für diejenigen gedacht, die * nix- Softwaretools aus welchen Gründen auch immer mögen . In seltenen Fällen ( z. B. knappe E / A und Speicher) können diese Snippets tatsächlich schneller sein als einige der wichtigsten Antworten.

Rufen Sie die Eingabedatei foo auf .

  1. Wenn wir wissen, dass foo vier Spalten hat:

    for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done
  2. Wenn wir nicht wissen, wie viele Spalten foo hat:

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

    xargshat eine Größenbeschränkung und würde daher unvollständige Arbeit mit einer langen Datei machen. Welche Größenbeschränkung ist systemabhängig, z.

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

    Maximale Befehlslänge, die wir tatsächlich verwenden könnten: 2088944

  3. tr& echo:

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

    ... oder wenn die Anzahl der Spalten unbekannt ist:

    n=$(head -n 1 foo | wc -w)
    for f in $(seq 1 $n); do 
        cut -d ' ' -f $f foo | tr '\n' ' ' ; echo
    done
  4. Unter Verwendung set, die wie xargs, hat ähnliche Kommandozeile Größe basierend Einschränkungen:

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

2
Das wären alles Größenordnungen langsamer als eine Awk- oder Perl-Lösung und zerbrechlich. Lesen Sie unix.stackexchange.com/questions/169716/… .
Ed Morton

@ EdMorton, danke, qualifiziertes Intro meiner Antwort, um Ihre Geschwindigkeitsbedenken auszuräumen. Zu "zerbrechlich": nicht 3) und auch nicht die anderen, wenn der Programmierer weiß, dass die Daten für eine bestimmte Technik sicher sind; und ist POSIX-kompatibler Shell-Code nicht ein stabilerer Standard als Perl ?
Agc

Entschuldigung, Idk viel über Perl. In diesem Fall wäre das zu verwendende Werkzeug awk. cut, head, echoEtc. sind nicht mehr POSIX kompatible Shell - Code als ein awkSkript ist - sie alle auf jeder UNIX - Installation Standard sind. Es gibt einfach keinen Grund, eine Reihe von Tools zu verwenden, bei denen Sie in Kombination vorsichtig mit dem Inhalt Ihrer Eingabedatei und dem Verzeichnis sein müssen, aus dem Sie das Skript ausführen, wenn Sie nur awk verwenden können und das Endergebnis schneller und robuster ist .
Ed Morton

Bitte, ich bin kein Anti- Awk , aber die Bedingungen variieren. Grund Nr. 1: for f in cut head xargs seq awk ; do wc -c $(which $f) ; done Wenn der Speicher zu langsam oder die E / A zu niedrig ist, verschlimmern größere Dolmetscher die Situation, egal wie gut sie unter idealeren Umständen sind. Grund Nr. 2: awk (oder fast jede Sprache) leidet auch unter einer steileren Lernkurve als ein kleines Dienstprogramm, das entwickelt wurde, um eine Sache gut zu machen. Wenn die Laufzeit billiger ist als die Arbeitsstunden des Programmierers, spart das einfache Codieren mit "Software-Tools" Geld.
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

eine andere Version mit set eval


Lesen Sie unix.stackexchange.com/questions/169716/… , um einige, aber nicht alle Probleme mit dieser Lösung zu verstehen.
Ed Morton

1

Eine weitere Bash-Variante

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

Skript

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

Ausgabe

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

0

Hier ist eine Haskell-Lösung. Wenn es mit -O2 kompiliert wird, läuft es etwas schneller als Ghostdogs Awk und etwas langsamer als Stephans dünn umwickelte C- Python auf meinem Computer für wiederholte "Hallo Welt" -Eingabezeilen. Leider gibt es, soweit ich das beurteilen kann, keine Unterstützung für die Übergabe von Befehlszeilencode durch GHC. Sie müssen diese daher selbst in eine Datei schreiben. Die Zeilen werden auf die Länge der kürzesten Zeile abgeschnitten.

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

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

0

Eine awk-Lösung, die das gesamte Array im Speicher speichert

    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

Wir können die Datei jedoch so oft "durchlaufen", wie Ausgabezeilen benötigt werden:

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

Welche (für eine geringe Anzahl von Ausgabezeilen ist schneller als der vorherige Code).


0

Hier ist ein Bash-Einzeiler, der darauf basiert, jede Zeile einfach in eine Spalte umzuwandeln und pastezusammenzufügen:

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. Erstellt eine tmp1Datei, damit sie nicht leer ist.

  2. liest jede Zeile und wandelt sie mit in eine Spalte um tr

  3. Fügt die neue Spalte in die tmp1Datei ein

  4. Kopien ergeben sich wieder in tmp1.

PS: Ich wollte unbedingt Io-Deskriptoren verwenden, konnte sie aber nicht zum Laufen bringen.


Stellen Sie sicher, dass Sie einen Wecker einstellen, wenn Sie diesen für eine große Datei ausführen möchten. Lesen Sie unix.stackexchange.com/questions/169716/… , um einige, aber nicht alle Probleme mit diesem Ansatz zu verstehen.
Ed Morton

0

Ein Oneliner mit 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

Ich habe unten zwei Skripte verwendet, um ähnliche Vorgänge auszuführen. Der erste ist in awk, was viel schneller ist als der zweite, der in "pure" bash ist. Möglicherweise können Sie es an Ihre eigene Anwendung anpassen.

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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.