121

If there's a "First World Problems" for scripting, this would be it.

I have the following code in a script I'm updating:

if [ $diffLines -eq 1 ]; then
        dateLastChanged=$(stat --format '%y' /.bbdata | awk '{print $1" "$2}' | sed 's/\.[0-9]*//g')

        mailx -r "Systems and Operations <sysadmin@[redacted].edu>" -s "Warning Stale BB Data" jadavis6@[redacted].edu <<EOI
        Last Change: $dateLastChanged

        This is an automated warning of stale data for the UNC-G Blackboard Snapshot process.
EOI

else
        echo "$diffLines have changed"
fi

The script sends email without issues, but the mailx command is nested within an if statement so I appear to be left with two choices:

  1. Put EOI on a new line and break indentation patterns or
  2. Keep with indentation but use something like an echo statement to get mailx to suck up my email.

I'm open to alternatives to heredoc, but if there's a way to get around this it's my preferred syntax.

Bratchley
  • 16,824
  • 14
  • 67
  • 103
  • It’s very rare that you need to combine awk and sed in the same command.  Here. you could do stat … | awk '{sub(/\.[0-9]*/, ""); print $1, $2}', thus doing the substitution in awk and eliminating the sed.  (Use gsub if there is a possibility of multiple occurrences of the pattern.) – G-Man Says 'Reinstate Monica' Mar 24 '22 at 18:32

10 Answers10

187

You can change the here-doc operator to <<-.  You can then indent both the here-doc and the delimiter with tabs:

#! /bin/bash
cat <<-EOF
    indented
    EOF
echo Done

Note that you must use tabs, not spaces, to indent both the here-doc and the delimiter. This means the above example won't work copied (Stack Exchange replaces tabs with spaces). If you put any quotes around the first EOF delimiter, then parameter expansion, command substitution, and arithmetic expansion will not be in effect.

choroba
  • 47,233
  • Cool, that fixes the indent problem but now it's not expanding the variable I'm trying to put in there ($dateLastChanged) if I do the hypen+quotes thing in your example, but if I take the hyphen and quotes out and put EOI on a new line it starts expanding it again. – Bratchley May 20 '13 at 18:17
  • 2
    @JoelDavis: Just remove the quotes, keep the hyphen. – choroba May 20 '13 at 18:37
  • 19
    Being forced to use tabs is very annoying. Is there a good way around it? – con-f-use Nov 19 '15 at 18:25
  • 3
    @con-f-use: You can try something like cat << EOF | sed 's/^ *//' and so on. – choroba Nov 19 '15 at 19:33
  • 10
    Or even better: cat <<- EOF | awk 'NR==1 && match($0, /^ +/){n=RLENGTH} {print substr($0, n+1)}'. This removes the amount of preceding spaces in the first line from every line in the here document (thanks to anubhava). – con-f-use Nov 21 '15 at 10:40
  • 3
    Actually, the only line that needs a is the final "EOF" line. The rest of the lines can use spaces. (at least in Bash v4... not sure of earlier.) – Cometsong Feb 12 '18 at 18:29
  • 1
    @Cometsong: It doesn't remove the leading whitespace for me on bash 4.3.42. – choroba Feb 12 '18 at 21:01
  • @choroba - I just realized I'd added in the awk space-removal before I added that comment! Only the final line needs to be Tab'd using that... :) – Cometsong Feb 12 '18 at 21:09
  • To clarify, the reason you need to use <<- (instead of <<) is because, as explained here, it strips leading tabs, but not spaces, in the output. Thus, to ensure your here doc has a nice readable indentation in your script, but not when it's actually written out, you need to indent your here doc with tabs (instead of spaces). – Seth Sep 07 '22 at 20:10
  • Similarly, to build upon this answer, let's say you wanted the content within your here doc to also have nested indentation -- i.e. you have the outermost level of indentation, which is just for making your script readable and which you want to be stripped, and also one/more inner levels of indentation that you want to be preserved when your here doc is actually written out. In that case, you would have to use tabs for the outermost level of indentation, and spaces for the remaining inner levels of indentation. – Seth Sep 07 '22 at 20:11
26

If you don't need command substitution and parameter expansion inside your here-document, you can avoid using tabs by adding the leading spaces to the delimiter:

$     cat << '    EOF'
>         indented
>     EOF
        indented
$     cat << '    EOF' | sed -r 's/^ {8}//'
>         unindented
>     EOF
unindented

I couldn't figure out a way to use this trick and keep parameter expansion, though.

itsadok
  • 733
  • 2
    To me, this is the only answer which solves the indenting problem without using spaces. shell-check will find any indent changes which con't match the spaces in the quoted string. Use double quotes for parameter expansion? – Tom Hale Jun 26 '18 at 17:18
  • 2
    Nice. You can pipe thru envsubst to replace environ variables with their values. – Red Pill Aug 18 '22 at 22:39
  • The only downside to this issue that IDEs like VSCODE think that the HEREDOC is never closed for styling any coding following it.

    Before: https://imgur.com/2xWrPSH After: https://imgur.com/2xjvwt2

    – NorseGaud Jul 31 '23 at 16:53
7

Try this:

sed 's/^ *//' >> ~/Desktop/text.txt << EOF
    Load time-out reached and nothing to resume.
    $(date +%T) - Transmission-daemon exiting.
EOF
robz
  • 71
  • 1
    You can't have differently-indented lines within the heredoc is this case. (This matters if e.g. the contents is a script.) – ivan_pozdeev Aug 08 '19 at 09:45
2

The other method would be herestrings:

    mail_content="Last Change: $dateLastChanged

    This is an automated warning of stale data for the UNC-G Blackboard Snapshot process."
    mailx -r "Systems and Operations <sysadmin@[redacted].edu>" -s "Warning Stale BB Data" jadavis6@[redacted].edu <<<"$mail_content"
muru
  • 72,889
2

This answer is GNU-Bash-specific.

The trick is that we use the <<< one-word here-doc offered by Bash, and we make that a multi-line item.

We also avoid a UUoC: we don't need a cat process to feed input to sed:

$ sed '1d;s/^    //' <<<"
    {
       TERM=$TERM
    }
    bye"

Output shows leading four-space indentation removed, and $TERM expanded:

{
   TERM=xterm-256color
}
bye

the 1d command in sed is to delete the first blank line, which exists because our quoted literal starts with a newline after the opening quote.

Of course, in the real script for which this is indented—pardon me, intendeded—we would line up the braces with the sed command, which would be indented inside a loop or conditional.

If we start each line of the datum with a delimiter, then a simple sed substitution will delete a variable amount of indentation, so that the block can be freely moved around between indentation levels:

while command ; do
    if condition ; then
        variable=$(sed '1d;s/^.*|//' <<<"
                  |{
                  |   TERM=$TERM
                  |}
                  |bye
                  ")
    fi
done

One last idea is to put the indent magic into a variable which is used as a sort of macro:

# put in some common definitions library section
indent='sed 1d;s/^.*|//'

...

while command ; do if condition ; then variable=$($indent <<<" |{ | TERM=$TERM |} |bye ") fi done

We can improve this by writing a good old-fashioned function:

# put in some common definitions library section
ind()
{
   sed '1d;s/^.*|//' <<<$1
}

...

while command ; do if condition ; then variable=$(ind " |{ | TERM=$TERM |} |bye ") fi done

Then we have abstracted away the <<< entirely.

Kaz
  • 8,273
2

You could use a here-string (<<<) instead of a here-document (<<MARKER), which at least avoids the end-of-document marker not being intended.

if [[ true ]]; then
  # Sample indented block
  cat <<<'First line
  Second line
  '
fi

Output (note the trailing empty line):

First line
  Second line

You can combine this with other commands to strip indent. Here cut will output the fifth character onwards of each line. -c is character mode, and 5- is the range of characters to output.

if [[ true ]]; then
  # Sample indented block
  cut -c5- <<<'
    First line
    Second line
  '
fi

Output (note first and last line are empty):


First line
Second line

Excellent explanation of the difference at command line - What's the difference between <<, <<< and < < in bash? - Ask Ubuntu.

RobM
  • 502
1

Hmm... Seems like you could take better advantage of the --format argument here to use --printf instead and just pass the lot over a pipe. Also, your if...fi is a compound command - it can take a redirect which all contained commands will inherit, so maybe you don't need to nest the heredoc at all.

if      [ "$diffLines" = 1 ]
then    stat --printf "Last Change: %.19y\n\n$(cat)\n" /.bbdata |
        mailx   -r  "Systems and Operations <sysadmin@[redacted].edu>" \
                -s  "Warning Stale BB Data" 'jadavis6@[redacted].edu'
else    echo    "$diffLines have changed"
fi      <<\STALE
This is an automated warning of stale data for the UNC-G Blackboard Snapshot process.
STALE
mikeserv
  • 58,310
  • Yeah my previous revision said I didn't mind the sed/awk part. Part of my revision today was to take it out since it wasn't germane to the question. Either way it's six of one half a dozen of the other. – Bratchley Jan 27 '15 at 16:27
  • @Bratchley - damn. That last sentence is going to distract me for the rest of the day. – mikeserv Jan 27 '15 at 16:28
  • How do you mean? – Bratchley Jan 27 '15 at 16:29
  • 1
    @Bratchley - Looks like a riddle. – mikeserv Jan 27 '15 at 16:30
  • Ha. Not sure what country you're from but that's a common phrase in the States. Just means "Different approach to the same end." Your solution does get around heredoc though. – Bratchley Jan 27 '15 at 16:31
  • @Bratchley - I usually use em like that. It works for functions, too. with functions its better though because the redirects aren't evaluated till theyre called - so if you tack a heredoc onto the end of fn() { cat <&3; } 3<<INPUT you get a free, shell-evaled fd for every call to fn. You can stack as many as you want and reference them anywhere within the compound command - but theyre always evaled at the first call - before any commands in fn execute. Im from the states - but never heard that one. – mikeserv Jan 27 '15 at 16:36
1

Given that this is an aesthetic issue, here's an aesthetic solution: place some visual aid into the code which acts as a transition between the "proper" pre-heredoc indentation and temporary left-alignment of the heredoc block.

if [ $condition -eq 1 ]; then
              ︙
       some code
       some more code

____/

mailx -r "Systems and Operations <sysadmin@[redacted].edu>" -s "Warning Stale BB Data" jadavis6@[redacted].edu <<EOI Last Change: $dateLastChanged

This is an automated warning of stale data for the UNC-G Blackboard Snapshot process. EOI

____

\

   <i>even more code</i>
          ︙

else code ︙ fi

I find that this allows my brain to better perceive the left-aligned code as part of the indented code around it. When I'm writing in a code editor that indicates the level of indentation with a vertical line, the above visual aid appears to merge into the indicator for the previous indent level, making the transition feel even more intuitive. Not everyone will find this satisfying, of course, but there don't seem to be any universally satisfying answers for when you have space-indented code.

0

There's a workaround I use to cheat the "space stripping behaviour". It allows me to write the code with indents as I want it to be human readable too.

#!/bin/bash

T=echo -ne ''

TEST_CAT() { cat <<- _EOF Line without indent $T This is indented first line $T Line with double indent $T Again a line with double indent $T Another Line with double indent $T$T Note: That extra '$T' removes one indent $T This is again a single indent _EOF

}

TEST_CAT

mdk
  • 101
  • Your answer seems incomplete or erroneous. The code is not testable as is. It could be improved with additional supporting information. warning: Stack Exchange doesn' save tabs – Thibault LE PAUL Jul 02 '22 at 06:49
0

There have been quite many good answers to this question already. However the one thing that I would like to improve is that most answers either require the use of tabs, or they remove an arbitrary number of white spaces at the beginning of the indented code. The latter problem has even been raised as a comment by @ivan_pozdeev, but it has not been answered yet.

In my solution, I prefer variable expansion to work, so I do not quote the EOF as done by https://unix.stackexchange.com/a/436168/240734. Therefore, the ending EOF must not be indented. For me, that is an acceptable compromise. If you prefer the ending EOF to be indented too, then please see the other answers with their respective pro's and cons.

Also, I want to be able to use spaces for indenting instead of tabs, therefore the concept of https://unix.stackexchange.com/a/76483/240734 does not work for me.

My solution is the following:

# This 'if' statement is only added to showcase the indenting:
if true ; then
    # Use perl in the heredoc to remove only the first 4 spaces or
    # the first tab character in the heredoc. Adjust this to the
    # indenting that is used in your heredoc. Instead of perl, also
    # sed or awk can be used.
    cat << EOF | perl -pe 's@^(    |\t)@@g' > /some/file
    # This line is not indented in the created file.
    if true ; then
        # This line is indented by 4 spaces in the created file.
        echo "hello world"
    fi || exit 1
EOF
# Check if there heredoc file creation / redirection worked,
# and fail if it did not:
if test $? -ne 0 ; then
    exit 1
fi

fi

emmenlau
  • 111
  • Quoting EOF has nothing to do with whether the ending EOF has to be indented. The - after << doesn't affect variable expansion. – muru Sep 13 '23 at 11:40
  • @muru Quoting EOF (as https://unix.stackexchange.com/a/436168/240734 is doing) is one way to allow for indenting of EOF. But it comes at the price of loosing command substitution and parameter expansion. That is what I was referring to. Using the <<- operator is another way to allow for indenting of EOF. But it comes at the price of not being able to use spaces for indenting. I did not mention this because https://unix.stackexchange.com/a/76483/240734 already explains this idea nicely. People that can accept using tabs may prefer that answer over mine! – emmenlau Sep 13 '23 at 11:55
  • Ah, then you should probably use the awk variant in https://unix.stackexchange.com/a/198401/70524 instead. – muru Sep 13 '23 at 12:23
  • @muru the awk variant in unix.stackexchange.com/a/198401/70524 achieves mostly the same, but in my humble opinion it is way harder to read than my solution here. It makes use of helper function show that is superfluous to the problem, it also makes use of the expand function that converts tabs to spaces which may not be what users expect. All in all its great if that works for you, but my goal was to provide a solution that is more easy to reason about. – emmenlau Sep 13 '23 at 12:55