bash 用空格缩进 heredocs

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/33815600/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-18 13:55:21  来源:igfitidea点击:

Indenting heredocs with spaces

bashindentationspacesheredoc

提问by IBPX

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.

对于我从事的个人发展和项目,我们使用四个空格而不是制表符。但是,我需要使用heredoc,并且在不破坏缩进流程的情况下我不能这样做。

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

让我知道这是否属于Unix/Linux Stack Exchange

回答by chepner

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

(如果你使用的是bash4,滚动到最后我认为是纯 shell 和可读性的最佳组合。)

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

对于 shell 脚本,使用选项卡不是偏好或风格的问题;这就是语言的定义方式。

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:

另一种选择是完全避免使用 here 文档,代价是必须使用更多引号和行延续:

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:

如果您愿意放弃 POSIX 兼容性,您可以使用数组来避免显式的行延续:

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 bash4's readarraycommand to populate an array. Parameter expansion takes care of removing a fixed number of spaces from the beginning of each lie.

下面再次使用 here 文档,但这次使用bash4 的readarray命令来填充数组。参数扩展负责从每个谎言的开头删除固定数量的空格。

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 <<-EOFand a tab-indented closing delimiter.)

(另外,顺便说一句,使用以空格开头的 here-doc 分隔符的绝妙技巧的一个缺点是它会阻止您在 here-doc 内执行扩展。如果您想这样做,您将有要么不缩进分隔符,要么对你的无制表符规则做一个小例外,并使用<<-EOF一个制表符缩进的结束分隔符。)

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
}

回答by Binary Phile

geta() {
  local _ref=
  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=
  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 getafunction below). It deals with arbitrary leading whitespace, not just a predetermined amount.

这是一种稍微不同的方法,由于 printf 分配给数组元素,因此需要 Bash 4.1。(对于以前的版本,替换geta下面的函数)。它处理任意的前导空白,而不仅仅是预定的数量。

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

第一个函数 ,geta从标准输入中读取,去除前导空格并在传入名称的数组中返回结果。

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

第二个, gets, 做同样的事情,geta但返回一个带有完整换行符的字符串(最后一个除外)。

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

如果您将现有变量的名称传递给geta,请确保它已经为空。

Invoke getalike so:

geta像这样调用:

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

gets:

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:

在 OP 的函数中使用:

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:

如前所述,对于 4.1 以上的 Bash:

geta() {
  local _ref=
  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
}