2009-10-25 12 views
12

Come è possibile utilizzare shell one-liner e strumenti GNU comuni per concatenare le righe in due file come nel prodotto cartesiano? Qual è il modo più succinto, bello e "linuxy"?Prodotto cartesiano di due file (come insiemi di righe) in GNU/Linux

Per esempio, se ho due file:

$ cat file1 
a 
b 
$ cat file2 
c 
d 
e 

Il risultato dovrebbe essere

a, c 
a, d 
a, e 
b, c 
b, d 
b, e 
+0

Oh no, è trasformato in una competizione ... –

+1

@C. Ross, non è stato così. Avevo un preciso ed espresso criterio di non usare perl, python, ecc. E il resto era solo una solita battaglia per la mantenibilità. semplicità e chiarezza. –

risposta

13

Ecco script di shell per farlo

while read a; do while read b; do echo "$a, $b"; done < file2; done < file1 

Anche se questo sarà piuttosto lento. Non riesco a pensare a nessuna logica precompilata per realizzare questo. Il prossimo passo per la velocità sarebbe quello di fare quanto sopra in awk/perl.

awk 'NR==FNR { a[$0]; next } { for (i in a) print i",", $0 }' file1 file2 

Hmm, che ne dici di questa soluzione hacky per utilizzare la logica precompilata?

paste -d, <(sed -n "$(yes 'p;' | head -n $(wc -l < file2))" file1) \ 
      <(cat $(yes 'file2' | head -n $(wc -l < file1))) 
+2

@Pixelbeat: la tua prima versione deve invertire l'ordine di 'file1' e' file2'. (Ovvero, dovrebbe essere "fatto" Telemachus

+3

@Telemachus, l'ordine è irrilevante: se dico" prodotto cartesiano ", lo dico davvero *. –

+0

@HiteshPatel Credo che questo possa esserti utile L'unico cambiamento che dovrai fare funzionare correttamente mentre leggi una risposta di tipo 'è aggiungere l'argomento' -r', rendendolo 'while read -ra; do while read -rb; do', dal momento che il contenuto ha letteralmente backslash. (@pixelbeat, potresti voler modificare detti argomenti nella risposta corretta) –

2

Edit: Oops ... Mi dispiace, ho pensato che questo è stato taggato python ...

Se si dispone di Python 2.6:

from itertools import product 
print('\n'.join((', '.join(elt) for elt in (product(*((line.strip() for line in fh) for fh in (open('file1','r'), open('file2','r')))))))) 

a, c 
a, d 
a, e 
b, c 
b, d 
b, e 

Se si dispone di python pre-2.6:

def product(*args, **kwds): 
    ''' 
    Source: http://docs.python.org/library/itertools.html#itertools.product 
    ''' 
    # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy 
    # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111 
    pools = map(tuple, args) * kwds.get('repeat', 1) 
    result = [[]] 
    for pool in pools: 
     result = [x+[y] for x in result for y in pool] 
    for prod in result: 
     yield tuple(prod) 
print('\n'.join((', '.join(elt) for elt in (product(*((line.strip() for line in fh) for fh in (open('file1','r'), open('file2','r')))))))) 
+0

Funzionerebbe, ma python non è quello che stavo chiedendo. –

1

Soluzione 1:

perl -e '{use File::Slurp; @f1 = read_file("file1"); @f2 = read_file("file2"); map { chomp; $v1 = $_; map { print "$v1,$_"; } @f2 } @f1;}'

+0

Perché hai usato 'map' qui? Quelli dovrebbero essere "for' loops". –

+0

@Kinopiko: Non ti eri solo lamentato della "polizia della lingua" su una discussione diversa? – Telemachus

+0

L'unica cosa che mi piace usare più delle mappe è Espressioni regolari. :) – DVK

6

Il modo meccanico per farlo in guscio, non utilizzando Perl o Python, è:

while read line1 
do 
    while read line2 
    do echo "$line1, $line2" 
    done < file2 
done < file1 

Il comando join volte può essere utilizzato per queste operazioni - tuttavia, non sono chiaro che può fare un prodotto cartesiano come un caso degenerato.

Un passo avanti rispetto al doppio anello sarebbe:

while read line1 
do 
    sed "s/^/$line1, /" file2 
done < file1 
+0

Preferirei la prima soluzione perché non assomiglia ai file sono sostanzialmente diversi. –

+0

Probabilmente (la prima soluzione) sarebbe sostanzialmente più lenta - ma sarebbe anche immune ai caratteri dispari (come le barre) nei dati. Sistemare le cose in modo tale che non sia un problema è un po 'più fasullo, ea quel punto si inizia a pensare di usare Perl o Python dopo tutto. –

+0

@Pavel - grazie per l'assistenza editoriale. –

4

Edit:

DVK tentativo s' mi ha ispirato a fare questo con eval:

script='1{x;d};${H;x;s/\n/\,/g;p;q};H' 
eval "echo {$(sed -n $script file1)}\,\ {$(sed -n $script file2)}$'\n'"|sed 's/^ //' 

O uno script più semplice sed:

script=':a;N;${s/\n/,/g;b};ba' 

che si desidera utilizzare senza l'interruttore -n.

che dà:

a, c 
a, d 
a, e 
b, c 
b, d 
b, e 

risposta originale:

In Bash, si può fare questo. Essa non legge da file, ma è un trucco:

$ echo {a,b}\,\ {c,d,e}$'\n' 
a, c 
a, d 
a, e 
b, c 
b, d 
b, e 

Più semplicemente:

$ echo {a,b}{c,d,e} 
ac ad ae bc bd be 
+0

bello. ma di sicuro non vorrei mantenere questo script. :) – ghostdog74

+0

Davvero delizioso, ma non mantenibile. :) –

1
awk 'FNR==NR{ a[++d]=$1; next} 
{ 
    for (i=1;i<=d;i++){ 
    print $1","a[i] 
    } 
}' file2 file1 

# ./shell.sh 
a,c 
a,d 
a,e 
b,c 
b,d 
b,e 
1

OK, questa è la derivazione della soluzione di Dennis Williamson sopra da quando ha notato che il suo lo fa non leggere dal file:

$ echo {`cat a | tr "\012" ","`}\,\ {`cat b | tr "\012" ","`}$'\n' 
a, c 
a, d 
a, e 
b, c 
b, d 
b, e 
+1

Questo è ciò che mi dà: '{a, b,}, {c, d, e,}' come stringa letterale. –

1

Una soluzione con join, awk e il processo s ubstitution:

join <(xargs -I_ echo 1 _ < setA) <(xargs -I_ echo 1 _ < setB) 
    | awk '{ printf("%s, %s\n", $2, $3) }' 
+0

Qual è il contenuto del file "a"? Uno di questi dovrebbe essere un file diverso? L'AWK potrebbe essere probabilmente sostituito da 'cut -f2- -d '''. –

+0

Il file "a" contiene il set. Possono essere diversi se lo si desidera. Lo correggerò! – yassin

+0

@Dennis, 'cut' è probabilmente migliore, poiché funziona anche se' setB' contiene linee con spazi bianchi. –

6

non ho intenzione di far finta questo è abbastanza, ma ...

join -t, -j 9999 -o 2.1,1.1 /tmp/file1 /tmp/file2 

(aggiornato grazie a Iwan Aucamp sotto)

- join (coreutils GNU) 8.4

+0

è possibile eliminare l'uso del taglio aggiungendo -o '2.1.1.1' (o in qualsiasi modo ti piaccia) –

3

una funzione ricorsiva BASH generica potrebbe essere qualcosa di simile:

foreachline() { 

    _foreachline() { 

     if [ $# -lt 2 ]; then 
      printf "$1\n" 
      return 
     fi 

     local prefix=$1 
     local file=$2 
     shift 2 

     while read line; do 
      _foreachline "$prefix$line, " $* 
     done <$file 
    } 

    _foreachline "" $* 
} 

foreachline file1 file2 file3 

Saluti.

+2

Questa soluzione è unica tra queste soluzioni in quanto risolve il caso più generale di un insieme arbitrario di operazioni di prodotto cartesiane. –

+0

L'uso di '$ *' piuttosto che '" $ @ "' è sfortunato, comunque; significa che qualsiasi '" * "' come argomento verrà sostituito con un elenco di nomi di file, ad esempio. –

4

Non ci sarà una virgola per separare, ma utilizzando solo join:

$ join -j 2 file1 file2 
a c 
a d 
a e 
b c 
b d 
b e 
+0

'join -j 2 -o '1.1 2.1' -t ',' file1 file2' – Marcus

+0

@Marcus, potrebbe essere utile sottolineare che se si esegue il downgrade a un singolo separatore, ad esempio' -t, ', sarà anche lavorare con un numero di implementazioni di join non GNU. A parte la clausola dell'OP, la comunità più ampia apprezza la portabilità delle risposte. Non tutti eseguiamo Linux. :) – ghoti

Problemi correlati