http://qs321.pair.com?node_id=11125451

Bod has asked for the wisdom of the Perl Monks concerning the following question:

When I created an account here some five weeks ago, little did I realise just how much varied learning I would receive in such a short time...so I am asking for advice on an issue that has had me scratching my head many times over the years. How best to lay out code when quite a bit of text output is required, such as when dynamically creating a webpage, inside an indented block.

In the main body of the code I usually use an interpolating heredoc with any runtime variations defined in variables ahead of printing it all out.

my $login_text = $user_number?'logout':'login'; print<<"END_HTML"; <div> ...part of webpage... <input type="button" name="log" value="$login_text" onClick="doSomethi +ng();"> ...more of webpage... </div> END_HTML
That works and looks fine for a block of procedural code but I run into difficulties when I want to put something similar into an indented block for any reason. It could be a subroutine that is called to display a largely different page based on the query string or a significant block of content that is only shown under some conditions.
if (isAdmin($user_number)) { print ...some extra content... }
Heredocs don't work so well in these circumstances. I am using Perl 5.16 so don't get to use the print<<~"END_HTML"; syntax introduced in Perl 5.26.

This leaves a few option.
The one that most of my legacy code has is to simply put every line in a separate print statement

if (isAdmin($user_number)) { print "<table>\n"; print "<tr>\n"; print "<td class=\"someclass\" style=\"text-align:center\">Some Co +ntent</td>\n"; print "</tr><tr>\n<td class=\"someClass\">Restricted</td>\n" if $u +ser_number == 20; print "</tr>\n"; print "</table>"; }
Not very pretty and quite difficult to follow as it becomes more involved, especially as more and more HTML gets added over time. So a slight improvement that I used for a short time is with qq to save having to escape the quotation marks.
print qq[<td class="someclass" style="text-align:center">Some Conte +nt</td>\n]; print qq[</tr><tr>\n<td class="someClass">Restricted</td>\n] if $us +er_number == 20;
Slightly better - but still not very nice...

I have tried having a subroutine to strip out leading spaces but this has the disadvantage of always stripping leading spaces even when they are wanted! In this format it also strips out blank lines although this is not too tricky to solve.

#!/usr/bin/perl use strict; print "Content-type: text/plain\n\n"; print "Test\n\n"; sub indent { my $text = shift; $text =~ s/^\s+//gm; return $text; } if (1) { print indent(<<"END_TEXT"); Here is some test text with plenty of space at the start END_TEXT } exit 0;
This still requires END_TEXT to be written without an indent.

Many times I have searched for a solution and found several references to the issue but nothing offering a 'proper' solution. The topic of indentation in some form or another crops up periodically in all sorts of forms including Mandatory indenting which was interesting despite not being directly relevant.

Other than upgrading to Perl 5.26 or later, is there an elegant solution to laying out code to print a lot of text in an indented block?

Replies are listed 'Best First'.
Re: Here documents in blocks
by hippo (Bishop) on Dec 19, 2020 at 16:36 UTC

    It's not very neat, but you could alter indent() to take an optional second argument which is the maximum amount of leading space to trim. eg:

    #!/usr/bin/perl use strict; print "Content-type: text/plain\n\n"; print "Test\n\n"; sub indent { my ($text, $cols) = @_; $cols //= 1000; $text =~ s/^\s{0,$cols}//gm; return $text; } if (1) { print indent(<<"END_TEXT", 1); Here is some test text with plenty of space and an indent here at the start END_TEXT } exit 0;

    Note that you'll need to be careful with tabs v spaces. The value of 1 for $cols here is fine for a tab. You'll see that with that level, the indent on line 4 of the paragraph is preserved.

    PS. indent is probably better named outdent since that is effectively what it is doing.


    🦛

Re: Here documents in blocks
by Discipulus (Canon) on Dec 19, 2020 at 16:50 UTC
    Hello Bod,

    if I understand your question, prior of perl 5.26 you can use some trick to indent heredocs: substitution (but the token in not indented):

    ($var = <<"HEREDOC") =~ s/^\s+//gm; your text goes here HEREDOC

    But note you can also use "     HEREDOC" as token: see (and follow the white rabbit) here

    L*

    There are no rules, there are no thumbs..
    Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.

      In addition to the missing /r (already mentioned), that will remove blank lines. Use \h instead of \s.

      prior of perl 5.26 you can use some trick to indent heredocs: substitution

      That only works for variable assignment. If the same trick is attempted with a print statement, Perl complains that we Can't modify print in substitution (s///).

      print <<"HEREDOC" =~ s/^\s+//gm; # <- Doesn't work your text goes here HEREDOC

      I guess it could just be followed by print $var; but that continues the theme of inelegant solutions. I am sure there is a 'nice' solution to this...

        > If the same trick is attempted with a print statement, Perl complains that we Can't modify print in substitution (s///).

        That's why the /r flag was introduced. try s///r

        update

        print <<"HEREDOC" =~ s/^\s+//gmr; # <- works your text goes here HEREDOC __END__ your text goes here

        Cheers Rolf
        (addicted to the Perl Programming Language :)
        Wikisyntax for the Monastery

        Hello Bod,

        this works

        use strict; use warnings; print $_=<<" EOT"; indented as intended EOT exit;

        L*

        There are no rules, there are no thumbs..
        Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.
Re: Here documents in blocks
by BillKSmith (Monsignor) on Dec 19, 2020 at 19:50 UTC
    The book "Perl Best Practices" recommends factoring out such a heredoc into a subroutine. "The heredoc does compromise the indentation of the subroutine, but that's now a small and isolated section of the code..."
    Bill

      FWIW, all 255 Perl Best Practices (without the text from the PBP book) are available here. There are a number of items related to heredocs in the Values and Expressions chapter, the most relevant being item 42:

      Use a "theredoc" when a heredoc would compromise your indentation. This item offers two alternative ways to minimize compromising code indentation (which harms readability): i) factor out the heredoc into a predefined read-only constant at the file level; ii) factor out the heredoc inside a small subroutine (indentation is compromised but only in a small and isolated section of the code). Both methods do not indent the contents of the heredoc, so no error-prone tricks are required to adjust. Conway describes both methods as a "theredoc".

      The book "Perl Best Practices"

      Unfortunately my copy of that book is sat on a shelf in my office...I was last in my office back in February before the Pesky Pandemic turned the world upside down.

      Come to think of it, I filled up the biscuit tin the last time I was there!

        Speaking merely as a long time satisfied customer https://learning.oreilly.com/ (used to be called "Safari Books Online" back when I first subscribed) lets you get to electronic versions of all the ORA books as well as things from lots of other tech publishers as well as video courses. I've found it well worth what I pay them yearly that I don't spend on dead tree copies (then again I'm satisfied with reading things on my iPad).

        Alternately keep an eye out as Humble Bundle periodically will have ORA pdfs in a collection (e.g. this current one; but nothing (directly) perl related in this one though).

        The cake is a lie.
        The cake is a lie.
        The cake is a lie.

Re: Here documents in blocks (updated)
by AnomalousMonk (Archbishop) on Dec 19, 2020 at 18:30 UTC

    Win8 Strawberry 5.30.3.1 (64) Sat 12/19/2020 13:18:14 C:\@Work\Perl\monks >perl -Mstrict -Mwarnings use 5.014; # for s/// with /r modifier print "Content-type: text/plain\n\n"; print "Test\n\n"; # sub undent { # works, but not as flexible # return $_[0] =~ s/^\s+//gmr; # } sub undent { my $text = shift; $text =~ s{ \A [^\n]* \n (\s*) }{}xms; my $dent = length $1; return $text =~ s{ ^ \s{0,$dent} }{}xmsgr; } if (1) { print undent " Here is some test text that can also be ragged with plenty of space at the start\n" } ^Z Content-type: text/plain Test Here is some test text that can also be ragged with plenty of space at the start
    However, this has the disadvantage that you have to manually stick a newline at the end to get a nice block ending. But I suppose you could tack that on automatically with a .= "\n" or some such.
    (Update: I realized after posting that I only tested with spaces as indentation characters, not tabs, which I never use anyway. I think the code would work with pure tab indents, but I haven't tested this. I doubt the code would work with a mixture of spaces and tabs used as indentation.)

    Update: Yes,
        return $text =~ s{ ^ \s{0,$dent} }{}xmsgr . "\n";
    seems to do it for the automatic newline text block end.


    Give a man a fish:  <%-{-{-{-<

Re: Here documents in blocks
by tybalt89 (Monsignor) on Dec 19, 2020 at 19:58 UTC

    Something like this ?

    #!/usr/bin/perl use strict; # https://perlmonks.org/?node_id=11125451 use warnings; sub indent { my $text = shift; $text =~ s/^\s//gm until $text =~ /^\S/m; return $text; } if (1) { print indent(<<""); Here is some test text with plenty of space and even an indented line at the start }

    Outputs:

    Here is some test text with plenty of space and even an indented line at the start
      > print indent(<<"");

      The empty line marker is dangerous, at least if your editor doesn't highlight trailing white-space (mine does)

      Cheers Rolf
      (addicted to the Perl Programming Language :)
      Wikisyntax for the Monastery

        It's just a tradeoff between danger and beauty :)

        Personally I put the HERE-IS block at the left hand edge, and skip the whole "indent" thing.
        (A what (indent) you see, is what you get.)

Re: Here documents in blocks
by kcott (Archbishop) on Dec 20, 2020 at 03:31 UTC

    G'day Bod,

    I had a look through your post and came up with quite a few solutions; although, as I read through the other posts, I found most had already been covered. However, this one hasn't been addressed:

    "... put every line in a separate print statement ..."

    The print function takes a list. You don't need a massive column of print statements: sometimes you might need a few; for the HTML table code you showed, you really only need one.

    I've reduced the HTML to bare bones text but the principle is the same. I've included a conditional line just as your code had.

    $ perl -e ' print qq{line1\n}, qq{line2\n}, ($ARGV[0] == 20 ? qq{line3\n} : ""), qq{line4\n}; ' 19 line1 line2 line4
    $ perl -e ' print qq{line1\n}, qq{line2\n}, ($ARGV[0] == 20 ? qq{line3\n} : ""), qq{line4\n}; ' 20 line1 line2 line3 line4

    I think you'll agree that code is a lot cleaner; easier to read; and easier to maintain.

    I suppose you have your reasons for using 5.16 — I'm stuck with that at $work too; but I use 5.32 at home. Thankfully, I work at home normally (nothing to do with the plague) so I can build utilities in 5.32 to automate a large part of my work; unfortunately, I need to use 5.16 for all legacy code. Anyway, the suggestions by others that I saw will work under 5.16 (templating systems, s///r, and so on).

    — Ken

      I think you'll agree that code is a lot cleaner; easier to read; and easier to maintain.

      I certainly do agree!
      Thanks Ken for this. Between this and other answers I think I can produce some much nicer code next time I am faced with this sort of problem.

      I suppose you have your reasons for using 5.16

      It's what is installed on the shared hosting...I have 5.32 on my laptop and even 5.28 on my mobile!

      The webhost are doing an upgrade over night at the beginning of January. They have not said what is being upgraded but I'm hoping that they will installing a later version of Perl although I won't be holding my breath.

      Like you, I normally work from home. My company doesn't have any offices so all my employees work remotely. We just have one small conference room for face to face meetings and I have an office next to that but unusually only visit about once a month and I have not been there since February due to the Pesky Pandemic which, here in the UK, seems to be getting worse rather than better - quite concerning as we are in the hospitality sector.

Re: Here documents in blocks
by marto (Cardinal) on Dec 19, 2020 at 16:45 UTC

    "Not very pretty and quite difficult to follow as it becomes more involved, especially as more and more HTML gets added over time. So a slight improvement..."

    With respect it isn't 1995 anymore. Using classes and inline styles in HTML? Not using a templating system or modern framework. FWIW Mojolicious has an excellent support for this out of the box.

      FWIW Mojolicious has an excellent support for this out of the box

      This issue is not limited to delivering webpages...that's just one examples
      There are many other times I find it necessary to print significant blocks of text within an indented block.

        Regardless..., if you're actually writing code like this at all you'd benefit from taking the time learning how not to have to do such things. Separation of data from code is a good first step.

Re: Here documents in blocks [chomp]
by kcott (Archbishop) on Dec 20, 2020 at 03:57 UTC

    Here's another trick I've used occasionally that involves chomp.

    The following code will leave a blank line at the end of <pre> blocks when the HTML is rendered:

    $ perl -e ' my $insert = <<EOF; line1 line2 line3 EOF print "<pre>$insert</pre>"; ' <pre>line1 line2 line3 </pre>

    chomp to the rescue:

    $ perl -e ' chomp(my $insert = <<EOF); line1 line2 line3 EOF print "<pre>$insert</pre>"; ' <pre>line1 line2 line3</pre>

    It can fix lots of annoying problems like that; not just <pre> blocks.

    — Ken

Re: Here documents in blocks [s///r chaining]
by kcott (Archbishop) on Dec 20, 2020 at 17:00 UTC

    Me again. I don't think I've ever written this many responses to a single OP. :-)

    Use of s///r has been mentioned in more than one post. I wondered if you knew that you could chain them. You can also chain y///r and mix them up. This type of extended construct is valid; although, I can't think of an immediate use for such a beast:

    $str =~ s///r =~ y///r =~ s///r =~ tr///r

    I saw where you'd written that you don't really have time to learn a completely new system. The following may be useful in its own right; however, it may get you halfway to writing a more formal template. When you do get around to looking at templating systems — and I do recommend you at least put that on your TODO list — you'll probably notice the similarities between placeholders like __TOKEN__ here and those used by templating systems, such as <% TOKEN %>.

    In the following code, the main processing, including the s///r chaining, is all at the front; the (messy) heredocs are written as theredocs and moved out of the way to the end of the code.

    $ perl -E ' my @users = ( { audience => "Management" }, { audience => "Employee" }, { audience => "Guest" }, ); generate_report($_) for @users; sub generate_report { my ($user) = @_; say main_template() =~ s/__AUDIENCE__/$user->{audience}/r =~ s/__MNGT_REP__/mngt_rep($user)/er =~ s/__EMP_REP__/emp_rep($user)/er =~ s/__GUEST_REP__/guest_rep($user)/er; } # Theredocs (out of the way) sub main_template { <<EOF <p> Report for __AUDIENCE__. </p> __MNGT_REP____EMP_REP____GUEST_REP__ EOF } sub mngt_rep { my ($viewer) = @_; return "" unless $viewer->{audience} eq "Management"; return <<EOF <div> ... Management Report ... </div> EOF } sub emp_rep { my ($viewer) = @_; return "" unless $viewer->{audience} eq "Employee"; return <<EOF <div> ... Employee Report ... </div> EOF } sub guest_rep { my ($viewer) = @_; return "" unless $viewer->{audience} eq "Guest"; return <<EOF <div> ... Guest Report ... </div> EOF } '

    When run, that outputs:

    <p> Report for Management. </p> <div> ... Management Report ... </div> <p> Report for Employee. </p> <div> ... Employee Report ... </div> <p> Report for Guest. </p> <div> ... Guest Report ... </div>

    — Ken

Re: Here documents in blocks
by Anonymous Monk on Dec 20, 2020 at 22:04 UTC
    I would strongly suggest that you consider using something like Template::Toolkit even though you might not be producing "a web page." These are very useful in creating a separation of concerns between what you want to include and exactly how you want the finished text to look.
      I would strongly suggest that you consider using something like Template::Toolkit

      I think that's my bedtime reading sorted then...