Indenting heredocs with spaces Indenting heredocs with spaces bash bash

Indenting heredocs with spaces


(If you are using bash 4, scroll to the end for what I think is the best combination of pure shell and readability.)

For shell scripts, using tabs is not a matter of preference or style; it's how the language is defined.

usage () {⟶# Lines between EOF are each indented with the same number of tabs# Spaces can follow the tabs for in-document indentation⟶cat <<-EOF⟶⟶Hello, this is a cool program.⟶⟶This should get unindented.⟶⟶This code should stay indented:⟶⟶    something() {⟶⟶        echo It works, yo!;⟶⟶    }⟶⟶That's all.⟶EOF}

Another option is to avoid a here document altogether, at the cost of having to use more quotes and line continuations:

usage () {    printf '%s\n' \        "Hello, this is a cool program." \        "This should get unindented." \        "This code should stay indented:" \        "    something() {" \        "        echo It works, yo!" \        "    }" \        "That's all."}

If you are willing to forego POSIX compatibility, you can use an array to avoid the explicit line continuations:

usage () {    message=(        "Hello, this is a cool program."        "This should get unindented."        "This code should stay indented:"        "    something() {"        "        echo It works, yo!"        "    }"        "That's all."    )    printf '%s\n' "${message[@]}"}

The following uses a here document again, but this time with bash 4's readarray command to populate an array. Parameter expansion takes care of removing a fixed number of spaces from the beginning of each lie.

usage () {    # No tabs necessary!    readarray message <<'    EOF'        Hello, this is a cool program.        This should get unindented.        This code should stay indented:            something() {                echo It works, yo!;            }        That's all.    EOF    # Each line is indented an extra 8 spaces, so strip them    printf '%s' "${message[@]#        }"}

One last variation: you can use an extended pattern to simplify the parameter expansion. Instead of having to count how many spaces are used for indentation, simply end the indentation with a chosen non-space character, then match the fixed prefix. I use :. (The space followingthe colon is for readability; it can be dropped with a minor change to the prefix pattern.)

(Also, as an aside, one drawback to your very nice trick of using a here-doc delimiter that starts with whitespace is that it prevents you from performing expansions inside the here-doc. If you wanted to do so, you'd have to either leave the delimiter unindented, or make one minor exception to your no-tab rule and use <<-EOF and a tab-indented closing delimiter.)

usage () {    # No tabs necessary!    closing="That's all"    readarray message <<EOF       : Hello, this is a cool program.       : This should get unindented.       : This code should stay indented:       :      something() {       :          echo It works, yo!;       :      }       : $closingEOF    shopt -s extglob    printf '%s' "${message[@]#+( ): }"    shopt -u extglob}


geta() {  local _ref=$1  local -a _lines  local _i  local _leading_whitespace  local _len  IFS=$'\n' read -rd '' -a _lines ||:  _leading_whitespace=${_lines[0]%%[^[:space:]]*}  _len=${#_leading_whitespace}  for _i in "${!_lines[@]}"; do    printf -v "$_ref"[$_i] '%s' "${_lines[$_i]:$_len}"  done}gets() {  local _ref=$1  local -a _result  local IFS  geta _result  IFS=$'\n'  printf -v "$_ref" '%s' "${_result[*]}"}

This is a slightly different approach which requires Bash 4.1 due to printf's assigning to array elements. (for prior versions, substitute the geta function below). It deals with arbitrary leading whitespace, not just a predetermined amount.

The first function, geta, reads from stdin, strips leading whitespace and returns the result in the array whose name was passed in.

The second, gets, does the same thing as geta but returns a single string with newlines intact (except the last).

If you pass in the name of an existing variable to geta, make sure it is already empty.

Invoke geta like so:

$ geta hello <<'EOS'>    hello>    there>EOS$ declare -p hellodeclare -a hello='([0]="hello" [1]="there")'

gets:

$ unset -v hello$ gets hello <<'EOS'>     hello>     there> EOS$ declare -p hellodeclare -- hello="hellothere"

This approach should work for any combination of leading whitespace characters, so long as they are the same characters for all subsequent lines. The function strips the same number of characters from the front of each line, based on the number of leading whitespace characters in the first line.

The reason all the variables start with underscore is to minimize the chance of a name collision with the passed array name. You might want to rewrite this to prefix them with something even less likely to collide.

To use in OP's function:

gets usage_message <<'EOS'    Hello, this is a cool program.    This should get unindented.    This code should stay indented:        something() {            echo It works, yo!;        }    That's all.EOSusage() {    printf '%s\n' "$usage_message"}

As mentioned, for Bash older than 4.1:

geta() {  local _ref=$1  local -a _lines  local _i  local _leading_whitespace  local _len  IFS=$'\n' read -rd '' -a _lines ||:  _leading_whitespace=${_lines[0]%%[^[:space:]]*}  _len=${#_leading_whitespace}  for _i in "${!_lines[@]}"; do    eval "$(printf '%s+=( "%s" )' "$_ref" "${_lines[$_i]:$_len}")"  done}