views:

840

answers:

5

Suppose file1 looks like this:

bye bye
hello
thank you

And file2 looks like this:

chao
hola
gracias

The desired output is this:

bye bye chao
hello hola
thank you gracias

I myself have already come up with five different approaches to solve this problem. But I think there must be more ways, probably more concise and more elegant ways, and I hope I can learn more cool stuff :)

The following is what I have tried so far, based on what I've learnt from the many solutions of my previous problems. Also, I'm trying to sort of digest or internalize the knowledge I've acquired from the Llama book.

Code 1:

#!perl
use autodie;
use warnings;
use strict;

open my $file1,'<','c:/file1.txt';
open my $file2,'<','c:/file2.txt';

while(defined(my $line1 = <$file1>)
        and defined(my $line2 = <$file2>)){
    die "Files are different sizes!\n" unless eof(file1) == eof(file2);
    $line1 .= $line2;
    $line1 =~ s/\n/ /;
    print "$line1 \n";
}

Code 2:

#!perl
use autodie;
use warnings;
use strict;

open my $file1,'<','c:/file1.txt';
my @file1 = <$file1>;

open my $file2,'<','c:/file2.txt';
my @file2 =<$file2>;

for (my $n=0; $n<=$#file1; $n++) {
    $file1[$n] .=$file2[$n];
    $file1[$n]=~s/\n/ /;
    print $file1[$n];
}

Code 3:

#!perl
use autodie;
use warnings;
use strict;

open my $file1,'<','c:/file1.txt';
open my $file2,'<','c:/file2.txt';

my %hash;

while(defined(my $line1 = <$file1>)
      and defined(my $line2 = <$file2>)) {
  chomp $line1;
  chomp $line2;
  my ($key, $val) = ($line1,$line2);
  $hash{$key} = $val;
}
print map { "$_ $hash{$_}\n" } sort keys %hash;

Code 4:

#!perl
use autodie;
use warnings;
use strict;

open my $file1,'<','c:/file1.txt';
open my $file2,'<','c:/file2.txt';

while(defined(my $line1 = <$file1>)
      and defined(my $line2 = <$file2>)) {
  $line1 =~ s/(.+)/$1 $line2/;
  print $line1;
}

Code 5:

#!perl
use autodie;
use warnings;
use strict;

open my $file1,'<','c:/file1.txt';
my @file1 =<$file1>;

open my $file2,'<','c:/file2.txt';
my @file2 =<$file2>;

while ((@file1) && (@file2)){ 
    my $m = shift (@file1);
    chomp($m);

    my $n = shift (@file2);
    chomp($n);

    $m .=" ".$n;
    print "$m \n";
}

I have tried something like this:

foreach $file1 (@file2) && foreach $file2 (@file2) {...}

But Perl gave me a syntactic error warning. I was frustrated. But can we run two foreach loops simultaneously?

Thanks, as always, for any comments, suggestions and of course the generous code sharing :)

+2  A: 

An easy one with minimal error checking:

#!/usr/bin/perl -w

use strict;

open FILE1, '<file1.txt';
open FILE2, '<file2.txt';

while (defined(my $one = <FILE1>) or defined(my $twotemp = <FILE2>)){
    my $two = $twotemp ? $twotemp : <FILE2>;
    chomp $one if ($one);
    chomp $two if ($two);
    print ''.($one ? "$one " : '').($two ? $two : '')."\n";
}

And no, you can't run two loops simultaneous within the same thread, you'd have to fork, but that would not be guaranteed to run synchronously.

Pascal
Without testing, I can recognize there is the ternary operator I was expecting. Ah, this is cool. I myself made several attemtps to use the ternary operator to do the job but with no luck. Thanks alot :)
Mike
Tested now. It works great! Thanks again!
Mike
Does not work if the input line is the number zero. Use `defined` in the conditional operators.
Rob Kennedy
Yeah, Rob, true. (Ok, I said that it concatenates for every line of the first file, so this behavior would be somewhat correct ;) ).But that's what I meant with minimal error checking - that there should be more error checking.
Pascal
Edit: Ok, adjusted so that it now reads to the end of both files, no matter how long they are.
Pascal
No, I mean if a line is the digit zero, "0", then your script will not include that character in the output. I'm not talking about nul characters or empty lines, just the plain old number character at the top of your keyboard. I'm also not talking about files of different lengths or about error-checking.
Rob Kennedy
Oh, right, I see! Never ran into this problem, but you're obviously right. I should get used to using `defined()` from now on, thanks.
Pascal
+2  A: 

An easier alternative to your Code 5 which allows for an arbitrary number of lines and does not care if files have different numbers of lines (hat tip @FM):

#!/usr/bin/perl

use strict; use warnings;

use File::Slurp;
use List::AllUtils qw( each_arrayref );

my @lines = map [ read_file $_ ], @ARGV;

my $it = each_arrayref @lines;

while ( my @lines = grep { defined and chomp and length } $it->() ) {
    print join(' ', @lines), "\n";
}

And, without using any external modules:

#!perl
use autodie; use warnings; use strict;

my ($file1, $file2) = @ARGV;

open my $file1_h,'<', $file1;
my @file1 = grep { chomp; length } <$file1_h>;

open my $file2_h,'<', $file2;
my @file2 =  grep { chomp; length } <$file2_h>;

my $n_lines = @file1 > @file2 ? @file1 : @file2;

for my $i (0 .. $n_lines - 1) {
    my ($line1, $line2) = map {
        defined $_ ? $_ : ''
    } $file1[$i], $file2[$i];
    print $line1, ' ', $line2, "\n";
}

If you want to concatenate only the lines that appear in both files:

#!perl
use autodie; use warnings; use strict;

my ($file1, $file2) = @ARGV;

open my $file1_h,'<', $file1;
my @file1 = grep { chomp; length } <$file1_h>;

open my $file2_h,'<', $file2;
my @file2 =  grep { chomp; length } <$file2_h>;

my $n_lines = @file1 < @file2 ? @file1 : @file2;

for my $i (0 .. $n_lines - 1) {
    print $file1[$i], ' ', $file2[$i], "\n";
}
Sinan Ünür
Tested failed. Perl says "Can't locate List/Allutils.pm in @INC". But I'll grab the module and test it again.
Mike
+1 for `read_file` and `each_array`. I thought about this approach too, but then noticed your answer at the last minute. A person could easily generalize this to handle any N of files as well.
FM
@Sinan, thanks! Module installed and tested the code again. it works great but there's a pesky problem. Perl gives me an error saying "can't locate perl58.dll". of course it can't locate perl58.dll because I'm now running Perl 5.10.1. How can I get rid of this false alarm without downgrading my Perl?
Mike
@Mike That seems like a separate question onto itself but it seems to me like either there the `perl.exe` from the previous installation is still in your path or there is some kind of file association issue. What does `ftype Perl` say?
Sinan Ünür
C:/ftype Perl gives me this: perl="C:\Perl\bin\perl.exe" "%1" %*
Mike
@Sinan, so for this itsybitsy job, we can use quite an impressive lot of Perl operators and functions. This is GOOD.
Mike
@Sinan, one thing, I think this line of code should be added to your code: @ARGV = ("c:/file1.txt","c:/file2.txt"); I suppose for beginners, @ARGV would be difficult. I learnt something about this stuff from hobbs' answer to my first question "How can I search multiple files for a string?". Before that, I knew nothing about it. But maybe for unix systems, things are quite different. I'm not sure.
Mike
@Mike how do you invoke your script? And, yes, there is variety, but I do not think any of the solutions other than @FM's should be used.
Sinan Ünür
@Sinan, how do I invoke my script? well, I write my code using Windows text editor and save it as .pl file and then drag the file to the Window Commandline Console. Then it works.
Mike
@Sinan, or maybe it's not the right way to run the script? I'm really not sure.
Mike
+9  A: 
use strict;
use warnings;
use autodie;

my @handles = map { open my $h, '<', $_; $h } @ARGV;

while (@handles){
    @handles = grep { ! eof $_ } @handles;
    my @lines = map { my $v = <$_>; chomp $v; $v } @handles;
    print join(' ', @lines), "\n";
}

close $_ for @handles;
FM
+1 Good use of `eof`.
Sinan Ünür
BTW, I find `my @handles = map { open my $h, '<', $_; $h } @ARGV;` preferable. You get rid of the `@files` array which you do not use elsewhere. I wish I could upvote your answer several times.
Sinan Ünür
@Sinan Thanks, that's a good idea (answer edited).
FM
@FM, this code of yours works great !
Mike
Looks like @ARGV = ("c:/f1.txt","c:/f2.txt"); is more concise than open my $file1,'<','c:/f1.txt'; open my $file2,'<','c:/f2.txt';
Mike
@Mike Here's another way to do it: `my %d; push @{$d{$ARGV}}, $_ while <>`. That will slurp all of the files into a hash of array references. Printing the output is similar to my answer, but instead of pruning the list of file handles as you go, you will want to delete hash keys as their lists become empty.
FM
@FM, thanks for the pointer :)
Mike
Very nice indeed!
Pascal
+6  A: 

The most elegant way doesn't involve perl at all:

paste -d' ' file1 file2
mouviciel
+1 Agreed, but I think the OP's purpose is to learn Perl by working such toy programs.
Sinan Ünür
@mouviciel, this doesn't look like perl. but I agree it IS concise :)
Mike
is there a working Perl one-liner like this? I'm wondering.
Mike
I don't know perl enough to write a one-liner for that task. Maybe something based on the answer of SanHolo?
mouviciel
@mouviciel: See http://stackoverflow.com/questions/1636755/how-many-different-ways-are-there-to-concatenate-two-files-line-by-line-using-per/1637438#1637438 And, please **don't use it**.
Sinan Ünür
In Perl: `system("paste -d' ' $file1 $file2")`
mobrule
+5  A: 
Sinan Ünür
This is not my intention to use this! Nice performance.
mouviciel
+1 with a friggin laser on its head!
DVK
@Sinan :) Thanks alot! This wicked one-liner works great: perl -le "$,=' ';@_=@ARGV;open $_,$_ for @_;print map{chomp($a =<$_>);$a} @_=grep{!eof $_ }@_ while @_" "c:/file1.txt" "c:/file2.txt".
Mike
@Sinan, this line code does not work: perl -e "@_=@ARGV;chomp $x[$.-1]{$ARGV}=$_ print qq{@$_{@_}\n} for @x" "c:/f1.txt" "c:/f2.txt". It gives me something like "Can't modify scalar chomp in scalar assignment at -e line 1, near "eof and" Execution of -e aborted due to compilation errors.
Mike
@Mike I needed parentheses around the chomp. Should be OK now.
Sinan Ünür