如何将 HEREDOC 文本放入 shell 脚本变量中?

如何将 HEREDOC 文本放入 shell 脚本变量中?

我正在尝试以 POSIX 兼容的方式将 HEREDOC 文本放入 shell 脚本变量中。我尝试像这样:

#!/bin/sh

NEWLINE="
"

read_heredoc2() {
  while IFS="$NEWLINE" read -r read_heredoc_line; do
    echo "${read_heredoc_line}"
  done
}

read_heredoc2_result="$(read_heredoc2 <<'HEREDOC'

                        _                            _ _
                       | |                          | (_)
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |
            |___/|_|



HEREDOC
)"

echo "${read_heredoc2_result}"

这产生了以下错误:

                        _                            _ _
                       | |                          | (_)
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _  | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |
            |___/|_|

以下方法有效,但我不喜欢使用随机输出变量它是多么笨拙:

#!/bin/sh

NEWLINE="
"

read_heredoc1() {
  read_heredoc_first=1
  read_heredoc_result=""
  while IFS="$NEWLINE" read -r read_heredoc_line; do
    if [ ${read_heredoc_first} -eq 1 ]; then
      read_heredoc_result="${read_heredoc_line}"
      read_heredoc_first=0
    else
      read_heredoc_result="${read_heredoc_result}${NEWLINE}${read_heredoc_line}"
    fi
  done
}

read_heredoc1 <<'HEREDOC'

                        _                            _ _            
                       | |                          | (_)           
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___ 
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |                                                
            |___/|_|                                                



HEREDOC

echo "${read_heredoc_result}"

正确输出:

                        _                            _ _            
                       | |                          | (_)           
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___ 
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |                                                
            |___/|_|                                                

有任何想法吗?

答案1

问题是,在重击中,内部$( ... )转义(和其他)序列被解析,即使定界符本身没有它们。由于\转义了换行符,您会得到一条双线。您所看到的实际上是 Bash 中的解析问题 - 其他 shell 不会这样做。反引号在旧版本中也可能是一个问题。我有确认这是 Bash 中的一个错误,并将在以后的版本中修复。

您至少可以大大简化您的功能:

func() {
    res=$(cat)
}
func <<'HEREDOC'
...
HEREDOC

如果您想选择输出变量,可以对其进行参数化:

func() {
    eval "$1"'=$(cat)'
}
func res<<'HEREDOC'
...
HEREDOC

或者是一个相当丑陋的没有eval

{ res=$(cat) ; } <<'HEREDOC'
...
HEREDOC

需要{}的是 ,而不是(),以便变量之后仍然可用。

根据您执行此操作的频率以及目的,您可能更喜欢其中一个或另一个选项。最后一个是一次性的最简洁的。


如果您能够使用zsh,您的原始命令替换+heredoc将按原样工作,但您也可以进一步折叠所有这些:

x=$(<<'EOT'
...
EOT
)

Bash 不支持这一点,我认为任何其他 shell 也不会遇到您遇到的问题。

答案2

关于OP解决方案:

  • 如果允许使用某些常量变量,则不需要 eval 来分配变量。

  • 还可以实现调用接收 HEREDOC 的函数的一般结构。

在所有(合理的)shell 中都可以解决这两个问题的解决方案是这样的:

#!/bin/bash
nl="
"

read_heredoc(){
    var=""
    while IFS="$nl" read -r line; do
        var="$var$line$nl"
    done 
}


read_heredoc <<'HEREDOC'

                        _                            _ _
                       | |                          | (_)
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |
            |___/|_|

HEREDOC

read_heredoc2_result="$str"

printf '%s' "${read_heredoc2_result}"

原问题的解决方案。

自 bash 2.04(以及最近的 zsh、lksh、mksh)以来一直有效的解决方案。
请参阅下面的更便携的 (POSIX) 版本。

#!/bin/bash
read_heredoc() {
    IFS='' read -d '' -r var <<'HEREDOC'

                        _                            _ _
                       | |                          | (_)
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |
            |___/|_|



HEREDOC

}

read_heredoc
echo "$var"

核心命令

IFS='' read -d '' -r var <<'HEREDOC'

工作原理如下:

  1. 该词HEREDOC被(单)引用以避免对后续文本进行任何扩展。
  2. “here doc”内容在标准输入中以<<.
  3. 该选项-d ''强制read读取“此处文档”的全部内容。
  4. -r选项避免解释反斜杠引用的字符。
  5. 核心命令类似于read var.
  6. 最后一个细节是IFS='',这将避免读取删除默认 IFS 中的前导或尾随字符:spacetabnewline

在 ksh 中,该选项的 null 值-d ''不起作用。
作为解决方法,如果文本没有“回车符”,则 a-d $'\r'有效($'\r'当然,如果将 a 添加到每行末尾)。


添加的(在评论中)要求是生成符合 POSIX 的解决方案。

POSIX

扩展这个想法,使其仅使用 POSIX 选项运行。
这意味着主要没有-dfor read。这会强制读取每一行。
这反过来又迫使需要一次捕获一条线。
然后,要构建var尾随新行,必须添加(因为读取将其删除)。

#!/bin/sh

nl='
'

read_heredoc() {
    unset var
    while IFS="$nl" read -r line; do
        var="$var$line$nl"
    done <<\HEREDOC

                        _                            _ _
                       | |                          | (_)
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \ 
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/ 
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___| 
             __/ | | 
            |___/|_| 



HEREDOC

}

read_heredoc
printf '%s' "$var"

这在所有合理的 shell 中都有效(并且已经过测试)。

答案3

为了支持尾随换行符,我结合了 @MichaelHomer 的答案和我原来的解决方案。我没有使用 @EliahKagan 指出的链接中建议的解决方法,因为第一个使用神奇字符串,而后两个不符合 POSIX 标准。

#!/bin/sh

NEWLINE="
"

read_heredoc() {
  read_heredoc_result=""
  while IFS="${NEWLINE}" read -r read_heredoc_line; do
    read_heredoc_result="${read_heredoc_result}${read_heredoc_line}${NEWLINE}"
  done
  eval $1'=${read_heredoc_result}'
}

read_heredoc heredoc_str <<'HEREDOC'

                        _                            _ _
                       | |                          | (_)
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |
            |___/|_|




HEREDOC

echo "${heredoc_str}"

答案4

无用的使用 cat (引号 \ 和 `):

myplaceonline="
                       _                            _ _            
 _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | (_)_ __   ___ 
| '_ \` _ \\| | | | '_ \\| |/ _\` |/ __/ _ \\/ _ \\| '_ \\| | | '_ \\ / _ \\
| | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
|_| |_| |_|\\__, | .__/|_|\\__,_|\\___\\___|\\___/|_| |_|_|_|_| |_|\\___|
       |___/|_
"

或者不加引号:

myplaceonline="$(figlet myplaceonline)"

相关内容