Indenting heredocs with spaces [duplicate]

不问归期 提交于 2020-01-11 17:42:08

问题


For personal development and projects I work on, we use four spaces instead of tabs. However, I need to use a heredoc, and I can't do so without breaking the indention flow.

The only working way to do this I can think of would be this:

usage() {
    cat << '    EOF' | sed -e 's/^    //';
    Hello, this is a cool program.
    This should get unindented.
    This code should stay indented:
        something() {
            echo It works, yo!;
        }
    That's all.
    EOF
}

Is there a better way to do this?

Let me know if this belongs on the Unix/Linux Stack Exchange instead.


回答1:


(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 following the 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!;
       :      }
       : $closing
EOF
    shopt -s extglob
    printf '%s' "${message[@]#+( ): }"
    shopt -u extglob
}



回答2:


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 hello
declare -a hello='([0]="hello" [1]="there")'

gets:

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

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.
EOS

usage() {
    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
}


来源:https://stackoverflow.com/questions/33815600/indenting-heredocs-with-spaces

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!