How to maintain proper column spaces while printing rows of a text file in perl

Viewed 368

I am trying to edit few lines of my file and create new file with all modifications

My lines are not in paragraph format, it looks something like this below,

 R1   X XA  0i  1i   H 0i  
 R2   X XA  1i  1i   H 0i  
 R3   X XA  1i  1i   H 0i  
 R4   X XA  1i  1i   H 0i  
 R5   X XA  1i  1i   H 0i  
 R6   X XA  0i  0i   X 0i  

They clearly don't have equal spacing between each item in row, but they are aligned column basis. I wand to edit R3 3rd item from XA to XAHBB but unable to maintain the column alignment

my $cur_line_num = 1;
while(<Sourcefile>){
            my @RowEdit = split (" ",$_);
               if($RowEdit[0]=~ m/^R3$/s){
                  $RowEdit[2]="RowEdit[2]HBB"
               }
            my $curr_line = join(" ", @RowEdit)
            print $newfile "curr_line\n";
          $cur_line_num++;

}
print "$cur_line_num\n";

My current output is as shown below

 R1 X XA 0i 1i H 0i  
 R2 X XA 1i 1i H 0i  
 R3 X XAHBB 1i 1i H 0i  
 R4 X XA 1i 1i H 0i  
 R5 X XA 1i 1i H 0i  
 R6 X XA 0i 0i X 0i

Where as my expected output should look something like this below

 R1   X XA    0i  1i   H 0i  
 R2   X XA    1i  1i   H 0i  
 R3   X XAHBB 1i  1i   H 0i  
 R4   X XA    1i  1i   H 0i  
 R5   X XA    1i  1i   H 0i  
 R6   X XA    0i  0i   X 0i

How to maintain column alignment while editing a file?

3 Answers

That's what Text::Table is for:

#!/usr/bin/perl
use warnings;
use strict;

use Text::Table;

my $table = 'Text::Table'->new;

while (<DATA>) {
    my @RowEdit = split ' ';
    if ($RowEdit[0] eq 'R3') {
        $RowEdit[2] .= 'HBB';
    }
    $table->add(@RowEdit);
}
print $table;
print $. + 1, "\n";

__DATA__
R1   X XA  0i  1i   H 0i
R2   X XA  1i  1i   H 0i
R3   X XA  1i  1i   H 0i
R4   X XA  1i  1i   H 0i
R5   X XA  1i  1i   H 0i
R6   X XA  0i  0i   X 0i

Also note all the other small changes I made:

  • No need to use a new variable to count lines. Perl already has $..
  • /s changes the behaviour of . in a regex. It makes no sense to use it in a regex that doesn't contain a dot. Moreover, eq can be used for string equiality if there's only one string that matches the regex - it's easier to read and faster to run.
  • split uses $_ as the second argument if none is provided. Typing $_ anywhere outside grep or map is a code smell - either it's something worth a name, or could be written without it.
  • To append a string, you can use the .= operator.

You can do this with printf(). But you need to make two passes over the data (as there's no way to know what the widest column width are until you have seen every data record).

Something like this seems to do the trick.

#!/usr/bin/perl

use strict;
use warnings;

my @widths; # Store the max widths for each column
my @data;   # Store the actual data

while (<DATA>) {
  my @row;

  # Use the first line to initialise the @widths array
  if ($. == 1) {
    @row = /(\S+\s*)/g;
    @widths = map { length() - 1} @row; # Subtract 1 for gutter
    @row = map { s/\s+$//; $_ } @row;   # Remove trailing whitespace
  } else {
    @row = split;
  }

  # Split the data
  my @row = split;
  # Store the split data
  push @data, \@row;
  # Make the (optional) transformation
  $row[2] .= 'HBB' if $row[0] eq 'R3';

  # Look at each column in this row of data and
  # compare it to the widest data that we've previously
  # seen in that column.
  for my $i (0 .. $#row) {
    $widths[$i] = length $row[$i]
      if length $row[$i] > ($widths[$i] // 0);
  }
}

# Create a printf output format using the column
# widths we've stored in @widths
my $fmt = join ' ', map { "%-${_}s" } @widths;

# Use printf to display each line of data.
printf "$fmt\n", @$_ for @data;

__DATA__
R1   X XA  0i  1i   H 0i
R2   X XA  1i  1i   H 0i
R3   X XA  1i  1i   H 0i
R4   X XA  1i  1i   H 0i
R5   X XA  1i  1i   H 0i
R6   X XA  0i  0i   X 0i

The output is:

R1   X XA    0i  1i   H 0i
R2   X XA    1i  1i   H 0i
R3   X XAHBB 1i  1i   H 0i
R4   X XA    1i  1i   H 0i
R5   X XA    1i  1i   H 0i
R6   X XA    0i  0i   X 0i

But I think the Text::Table solution is better :-)

As this is one of the tasks Perl was originally written for (Practical Extraction and Reporting Language), then for completeness also good ol' plain Perl formats should be mentioned, see man perlform. E.g.

format STDOUT =
@<<<<< @ @<<<<, ....
$col1, $col2, $col3, ...
.

for my ... (...) {
   # set $col1 .... and then
   write
}
Related