4

I recently tried to use the <<- operator for heredocs in bash to keep indentation in my bash functions:

cat <<- EOF
    Hello World
EOF

However it turns out this only strips literal tab characters before the strings, which is of no use to me since:

  1. Some code editors replace tabs with spaces right away. So when I save this text in nvim or vscode it will still keep the indentation
  2. the EOF still needs to be written left-bound, I can't indent it.

I also don't want to strip all whitespace, only the ones that match up to the height of the EOF delimiter. So this would be my use case:

#!/bin/bash

log_something() {
    cat <<- EOF
    This is not indented in the final document
        This is indented by 4 spaces
    EOF
}

log_something

Is something like this possible in modern bash?

12
  • Inserting tabs in vscode: stackoverflow.com/q/45566785/1030675 Commented May 21 at 19:18
  • @choroba That's rather unpractical since I can't enforce this on all devs in our team Commented May 22 at 4:15
  • 1
    the EOF still needs to be written left-bound, I can't indent it. Strange. From bash manual: all leading tab characters are stripped from input lines and the line containing delimiter Commented May 22 at 4:44
  • @RenaudPacalet that's the problem. The delimiter is space in my setup, so indenting with 4 spaces of course yields an error. Commented May 22 at 11:06
  • Code editors do what you tell them to. Either adjust your editor's settings, or figure out how your editor allows you to enter a literal tab character instead of some number of spaces. Commented May 22 at 16:57

4 Answers 4

6

This is more of a hack than a recommendable solution, but you could simply make the termination string already contain the indenting spaces itself, i.e. using EOF, and replace the plain cat with a cut -c5- to un-indent the content block:

#!/bin/bash

log_something() {
    cut -c5- <<- "    EOF"
    This is not indented in the final document
        This is indented by 4 spaces
    EOF
}

log_something
This is not indented in the final document
    This is indented by 4 spaces
Sign up to request clarification or add additional context in comments.

3 Comments

Looks pretty recommendable to me :-). FWIW I'd personally indent the text within the here doc by an additional 4 spaces and use cut -d9- instead of cut -d5- so it's clear to a future reader that that text is within another construct, the here-doc, just like you'd indent text inside an if-then ... fi construct for legibility, and so you don't get tripped up if your text extremely coincidentally contains a line that just says EOF (or whatever you use as a delimiter).
Had a little chuckle while reading this, but I think it works for me now :) I hope at some point the maintainers of bash will come up with something better
When you want the shell variables in your block to be expanded, the quotes will block that. For literal text it is nice.
2

Function definitions themselves can include input redirections. You can write

log_something() {
    cat
} << EOF
This is not indented in the final document
    This is indented by 4 spaces
EOF

and cat inherits its standard input from the function itself.

(Note that these redirections attached to the definition cannot be overridden by a call to the function, though. log_something <<< "ha ha" still outputs your two lines of text, not ha ha.)

If you needed different inputs for different commands, you can use different file descriptors. For example,

foo () {
  cat <&3
  echo "==="
  cat <&2
} 2<<EOF2 3<<EOF3
second cat
EOF2
first cat
EOF3

Then

$ foo
first cat
===
second cat

This takes advantage of the fact that you can start multiple here documents (each associated with a different file descriptor) at the same time and complete them in starting order one after the other. Inside the function, each cat redirects its standard input from the appropriate here document's file descriptor.

5 Comments

i learned a few things here, didn't know about multiple heredocs! But i somehow fail to understand how this solves the original problem
I read the original question as being concerned about the syntax-related indentation. Since there is no reason to indent a function definition itself, there’s no reason to indent a here document atttached to the function.
Why should there be no reason to indent a function definition? E.g. why does everybody do it then?
glades - by function definition @chepner means the opening foo () { and closing } lines, not the body of the function that goes between them. I'm not sure if I've ever seen the lines @chepner is referring to indented, which is their point.
I was thinking in terms of bash having no nested functions. You can define a function inside another function (if I remember correctly), but it’s not scoped in anyway; the function is still global as if you had defined it in the global scope in the first place. Though I suppose you might want to define a function inside another the body of an if statement and indent it there, so this isn’t entirely foolproof.
1

@WalterA raised a good point about not being able to have variables expand inside a here document that uses quoted delimiters. You could use a here string instead of a here doc and post-process it with awk instead of cut if you need that functionality:

$ cat tst.sh
#!/bin/bash

log_something() {
    awk 'NR>2{print substr(p,9)} {p=$0}' <<< "
        This is not $RANDOM indented in the final document
            This is indented by 4 spaces
    "
}

log_something

$ ./tst.sh
This is not 26489 indented in the final document
    This is indented by 4 spaces

Consider also a function like fmt_log below that will figure out how much indenting your text block has and then adapt to that rather than you having to hard-code it as being 4 blanks:

$ cat tst.sh
#!/bin/bash

fmt_log() {
    awk '
        { rec[NR] = $0 }
        END {
            for ( i=2; i<NR; i++ ) {
                match(rec[i], /^[[:space:]]+/)
                if ( (indent == "") || (RLENGTH < indent) ) {
                    indent = RLENGTH
                }
            }
            for ( i=2; i<NR; i++ ) {
                print substr(rec[i], indent+1)
            }
        }
    ' <<< "$*"
}

log_something() {
    fmt_log "
        This is not $RANDOM indented in the final document
            This is indented by 4 spaces
    "
}

log_something_else() {
    if true; then
        if true; then
            fmt_log "
                This is also not $RANDOM indented in the final document
                    This is indented by 4 spaces
            "
        fi
    fi
}

log_something

echo "======="

log_something_else

$ ./tst.sh
This is not 1385 indented in the final document
    This is indented by 4 spaces
=======
This is also not 27651 indented in the final document
    This is indented by 4 spaces

3 Comments

that's quite a complex solution for such a sinple problem :/
I don't think the first script is complex, it's just replacing cut -c5- <<- " EOF" with awk 'NR>2{print substr(p,9)} {p=$0}' <<< " so you can let variables expand, and so far we haven't seen any other solution to that problem. The second one is a more complex solution as it's solving the more complex problem of being able to use any indent in multiple locations of your script(s) without needing to write different and tightly coupled but mostly duplicated code to handle each indent.
while we're on this subject I find it mind-bogglingly surreal that OpenGroup still steadfastly refuse to mandate here-strings in POSIX sh in 2025 even though they mandate here-docs, which is far more complex.
0

I usually just write a log function with printf.

$: log(){ printf '%s\n' "$@"; }

$: log "This is not indented in the final document
    This is indented by 4 spaces"
This is not indented in the final document
    This is indented by 4 spaces

$: log "This is not indented in the final document" "    This is indented by 4 spaces"
This is not indented in the final document
    This is indented by 4 spaces

You can of course build in extra functionality and formatting as needed.

$: log(){ printf '%(%F %T)T:\n' -1; printf '%s\n' "$@"; }
$: log "This is not indented in the final document" "    This is indented by 4 spaces"
2025-05-26 09:32:49:
This is not indented in the final document
    This is indented by 4 spaces

I do use here-docs now and then, but for me it's almost universally when I need to quote some quoted quotes, which I try to avoid.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.