2
\$\begingroup\$

I am trying to extend the JSON format (for private use) to include an include statement. This could be used to include other .json files into a master file, for example.

#! /usr/bin/perl

use warnings;
use strict;
use File::Slurp 'read_file';

my $str=parseFile('t.json');

sub parseFile {
  my ($fn)=@_;

  my $str=read_file($fn);
  my $res='';
  my $i=0;
  while (1) {
    if ($str=~/include\s+"(.*?)"/) {
      $res=$res.$`.parseFile("include/$1.json");
      $str=$';
    } else {
      $res=$res.$str;
      last;
    }
  }
  return $res;
}

It seems to be working for some simple test cases. But is it too complicated or is it too simple?

\$\endgroup\$
4
\$\begingroup\$

The $` and $' are very cryptic, I had to look them up in man perlvar to find that they contain whatever was before and after the match. It also turns out that they are discouraged due to performance considerations. Either way the while block really smells.

A simpler approach is to use s///ge to replace the matched pattern with the output of a function call, like this:

sub parseFile {
  my ($fn) = @_;
  my $str = read_file($fn);
  $str =~ s/include\s+"(.*?)"/parseFile("include\/$1.json")/ge;
  return $str;
}

Also consider adding some error handling. For example, what should happen if the file doesn't exist? Ignore / warn / raise error?

It might be a good idea to make parseFile handle multiple parameters:

sub parseFile {
    foreach my $fn (@_) {
        my $str = read_file($fn);
        $str =~ s/include\s+"(.*?)"/parseFile("include\/$1.json")/ge;
        return $str;
    }
}

Maybe you don't really need to handle multiple files, but a side effect of this approach is that it automatically handles the case gracefully if somebody incorrectly calls the method without any parameters.

Finally, notice that I used spaces around operators like = and =~ to improve readability. I borrow this practice from other programming languages where this is a standard, and let's face it, Perl scripts need all the readability aid they can get...

UPDATE

@amon added some excellent points in his comment:

  • Notice that in the replacement pattern s/include\s+"(.*?)"/parseFile(".\/$1")/ge; I had to escape the delimiter /. Another way is changing the delimiter so I don't have to escape anything: s{include\s+"(.*?)"}{parseFile("include/$1")}ge;

  • My multi-argument suggestion is kinda broken, because of the return statement: it will only process the first file. It would be better to use Carp::confess "parseFile expects one file name or file handle" if not @_ == 1, and the caller could still map { parseFile($_) } @files if really required

\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks @janos for the comments! I forgot about the e modifier; seems like you can make good use of it here :) Now the code looks quite elegant. \$\endgroup\$ – Håkon Hægland Aug 27 '14 at 13:29
  • 1
    \$\begingroup\$ A review-review: ① If you have to escape a delimiter in Perl, switch the delimiter. E.g. you could use s{}{} to avoid escaping that forward slash. ② Your multi-argument variant doesn't work, because it will return the first result. In programming, there is little space for “degrading gracefully”. I recommend Carp::confess "parseFile expects one file name or file handle" if not @_ == 1 instead – a caller could still map { parseFile($_) } @files if really required. ③ Otherwise, good points → +1 \$\endgroup\$ – amon Aug 27 '14 at 14:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.