如何在Bash中将heredoc值分配给变量?

534

我有一个包含引号的多行字符串:

abc'asdf"
$(dont-execute-this)
foo"bar"''

在Bash中,我如何使用heredoc将其分配给变量?

我需要保留换行符。

我不想转义字符串中的字符,那样很麻烦...


@JohnM - 我刚刚尝试了使用单引号 'EOF' 和在内容中使用转义换行符 \ 的 heredoc 赋值:如果第二行有 cd 命令,我会得到 ".sh: line X: cd: command not found";但是如果我双引号 "EOF";那么 bash 变量 ${A} 不会被保留为字符串(它们会被扩展);但是,换行符 被保留 - 并且,我没有问题在第二行运行带有 cd 的命令(_并且 'EOF' 和 "EOF" 似乎也与 eval 配合良好,用于运行存储在字符串变量中的一组命令_)。干杯! - sdaau
1
...并且补充我的先前评论:如果在双引号“EOF”变量中调用eval $VAR,bash注释“#”将导致其余脚本被注释掉,因为此处$VAR将被视为单行;为了能够在多行脚本中使用bash“#”注释,请在eval调用中也对变量进行双引号包裹:eval "$VAR" - sdaau
@sdaau:我在使用eval方法时遇到了问题,但由于它是某个包中的一部分,该包会对其配置文件中定义的某些变量进行eval操作,因此我没有追踪到问题所在。错误信息为:“/usr/lib/network/network: eval: line 153: syntax error: unexpected end of file”。我只是转而使用了另一种解决方案。 - Golar Ramblar
有时候确实需要使用Here文档,但如果你只是想在静态字符串中插入换行符,请参考https://dev59.com/KXA75IYBdhLWcg3w9-Sx。 - tripleee
14个回答

709

您可以通过以下方式避免无用的cat并更好地处理引号不匹配:

$ read -r -d '' VAR <<'EOF'
abc'asdf"
$(dont-execute-this)
foo"bar"''
EOF

如果在输出变量时不使用引号,换行符会被丢失。使用引号可以保留它们:

$ echo "$VAR"
abc'asdf"
$(dont-execute-this)
foo"bar"''

如果您想在源代码中使用缩进以提高可读性,请在小于号后面使用破折号。缩进必须仅使用制表符(不要使用空格)。
$ read -r -d '' VAR <<-'EOF'
    abc'asdf"
    $(dont-execute-this)
    foo"bar"''
    EOF
$ echo "$VAR"
abc'asdf"
$(dont-execute-this)
foo"bar"''

如果你想保留结果变量内容中的制表符,你需要从IFS中删除制表符。Here文档的终端标记(EOF)不能缩进。

$ IFS='' read -r -d '' VAR <<'EOF'
    abc'asdf"
    $(dont-execute-this)
    foo"bar"''
EOF
$ echo "$VAR"
    abc'asdf"
    $(dont-execute-this)
    foo"bar"''

在命令行中,您可以通过按下 Ctrl-V Tab 来插入制表符。如果您正在使用编辑器,根据不同的编辑器,这也可能有效,或者您可能需要关闭自动将制表符转换为空格的功能。


199
值得一提的是,如果在您的脚本中使用了 set -o errexit(也称为 set -e),并且当您使用 read 命令时,它会在读取到 EOF 时返回一个非零返回码,从而终止您的脚本。请注意不要修改原意,尽可能地使翻译通俗易懂。 - Mark Byers
20
这是我从@MarkByers的信息中翻译出来的:这就是我从不使用set -e并且总是建议不要使用它的原因之一。最好使用适当的错误处理机制,而不是使用set -etrap 是你的朋友,还有其他朋友,比如 else|| - Dennis Williamson
25
在这种情况下,避免使用 cat 值得吗?使用 cat 将一个 heredoc 赋值给一个变量是一个众所周知的习语。在我看来,使用 read 可能会使事情变得更加难以理解,而收益微不足道。 - Gregory Pakosz
10
这是因为在d和空字符串之间没有空格;在bash-rd''缩写为-rd之前,read从未看到其参数,因此VAR被视为-d的参数。 - chepner
8
在这种格式下,read会返回一个非零的退出代码。这使得在启用错误检查的脚本(例如set -e)中使用此方法并不理想。 - Swiss
显示剩余30条评论

401

使用 $() 将 cat 命令的输出赋值给变量,如下所示:

VAR=$(
cat <<'END_HEREDOC'
abc'asdf"
$(dont-execute-this)
foo"bar"''
END_HEREDOC
)

# this will echo variable with new lines intact
echo "$VAR"
# this will echo variable without new lines (changed to space character)
echo $VAR

确保用单引号来定界起始的 END_HEREDOC。 这将防止 heredoc 的内容被扩展,因此 dont-execute-this 将不会被执行。

请注意,结束 heredoc 定界符 END_HEREDOC 必须单独占据一行(因此结束括号 ) 在下一行)。

感谢 @ephemient 的回答。


62
这是最易读的解决方案,至少对我而言是如此。它将变量名称放在页面的最左侧,而不是嵌入到读取命令中。 - Clayton Stanley
24
注意:为保留换行符,变量必须加引号。请使用 echo "$VAR" 而不是 echo $VAR - sevko
8
这句话的意思是:“在使用ash和OpenWRT时,由于read不支持-d选项,这个方法非常有用。” - David Ehrmann
4
如果在heredoc中有一个未成对的反引号,由于我无法理解的原因,会导致出现“意外的EOF”错误。 - Radon Rosborough
6
它防止变量扩展。 - miken32
显示剩余12条评论

103

这是Dennis方法的一种变体,在脚本中看起来更加优雅。

函数定义:

define(){ IFS='\n' read -r -d '' ${1} || true; }

使用方法:

define VAR <<'EOF'
abc'asdf"
$(dont-execute-this)
foo"bar"''
EOF

echo "$VAR"

享受

附言:为不支持 read -d 的Shell制作了一个'read loop'版本。应该能够与 set -eu非成对的反引号一起使用,但测试还不够充分:

define(){ o=; while IFS="\n" read -r a; do o="$o$a"'
'; done; eval "$1=\$o"; }

1
这似乎只是表面上的工作。定义函数将返回状态1,我不太确定需要纠正什么。 - fny
2
这个解决方案适用于设置了 set -e,而所选答案则不适用。这似乎是因为 http://unix.stackexchange.com/a/265151/20650 - ffledgling
4
@fny,回传状态早已修复。 - ttt
3
ShellCheck SC2141 建议应该是 define(){ IFS=$'\n' ... (添加了 $)。 - luckman212
1
IFS='\n' 并不是你想要的(它将 IFS 设置为一个字面上的 n,并删除反斜杠)。你可以通过在引号之间添加一个字面上的换行符来解决这个问题,或者在 Bash 中使用先前评论中建议的 IFS=$'\n' - tripleee
显示剩余5条评论

55
VAR=<<END
abc
END

不工作是因为你正在将stdin重定向到一个不关心它的地方,即该赋值语句。

export A=`cat <<END
sdfsdf
sdfsdf
sdfsfds
END
` ; echo $A

这段代码可以运行,但其中有一个反引号可能会导致问题。此外,你应该避免使用反引号,最好使用命令替换符号$(..)

export A=$(cat <<END
sdfsdf
sdfsdf
sdfsfds
END
) ; echo $A

3
使用$(cat <<'END')替代即可接近成功。最后一个换行符将不会成为变量的一部分,但其余部分将被保留。 - ephemient
2
@Darren:啊哈!我已经注意到换行符的问题了,使用引号将输出变量括起来确实解决了这个问题。谢谢! - WestCoastProjects
1
@l0st3d 我会省略反引号的提及。 - Brad Koch
1
你不需要在 $(...) 中使用 cat,因为 << 已经将此处文档发送到 stdout。所以 export A=$(<<END ... END) 同样可以正常工作,并且少了一个进程运行。 - Spoike
3
有趣的是,由于第一个例子的特殊性,在必要时你可以将其用作类似于这样的临时注释块:REM=<< 'REM' ... 在此处添加注释... REM。或者更简洁地说,: << 'REM' ...。其中“REM”可以是像“NOTES”或“SCRATCHPAD”等内容。 - Beejor
显示剩余2条评论

49

仍然没有保留换行符的解决方案。

这并不是真的-你可能会被echo的行为所误导:

echo $VAR # 去除换行符

echo "$VAR" # 保留换行符


9
真正的问题在于如何引用变量。如果没有加引号,它们会被视为不同的参数,以空格为分隔符插入,而如果加了引号,则整个变量内容将被视为一个参数处理。 - Czipperz

24

Neil的回答的基础上,你通常根本不需要一个var,你可以像使用变量一样使用函数,并且它比内联或基于read的解决方案更容易阅读。

$ complex_message() {
  cat <<'EOF'
abc'asdf"
$(dont-execute-this)
foo"bar"''
EOF
}

$ echo "This is a $(complex_message)"
This is a abc'asdf"
$(dont-execute-this)
foo"bar"''

5
这个解决方案非常棒,依我之见是最优雅的。 - noamtm
这里有很多选择。 - Cymatical

16
一个数组是一个变量,所以在这种情况下,mapfile将起作用。
mapfile y <<'z'
abc'asdf"
$(dont-execute-this)
foo"bar"''
z

然后你可以像这样打印

printf %s "${y[@]}"

这里肯定有使用案例。 - Cymatical
更好的版本:https://dev59.com/DnM_5IYBdhLWcg3w6X5e#73370199 - Fredrick Brennan

8

我无法相信我是第一个发布这个的人。

@Erman和@Zombo接近了,但是mapfile不仅仅读取数组...

考虑这个例子:

#!/bin/bash
mapfile -d '' EXAMPLE << 'EOF'
Hello
こんにちは
今晩は
小夜なら
EOF
echo -n "$EXAMPLE"

Yielding:

你好
こんにちは
今晩は
如果是夜深了

$'' 是给 mapfile 的分隔符,它永远不会出现,它的含义是“未被分隔”。

因此,没有必要使用无用的 cat 并且不需要重组数组而产生额外开销。

此外,您将获得以下好处:

$ echo $EXAMPLE
Hello こんにちは 今晩は 小夜なら

使用 @Zombo 的方法无法获得以下内容:

mapfile y <<'z'
abc'asdf"
$(dont-execute-this)
foo"bar"''
z
echo $y
如果通过 head -c -1 命令运行,也可以以不影响性能的方式摆脱最后一个换行符:
abc'asdf"

Bonus

如果你通过 head -c -1 命令运行,还可以以不影响性能的方式摆脱最后一个换行符:

unset EXAMPLE
mapfile -d '' EXAMPLE < <(head -c -1 << EOF
Hello
こんにちは
今晩は
小夜なら
EOF
)
printf "%q" "$EXAMPLE"

$'Hello\nこんにちは\n今晩は\n小夜なら'

mapfile -d 需要 Bash 4.4 或更高版本。 - Martin
哦,没错。如果 shell 版本早于 bash 4.4(2016 年 9 月 17 日),那么这确实行不通。 - Fredrick Brennan
2
苹果只提供自2006年的Bash 3.2版本 :) - Martin
对于使用苹果电脑的人来说,这是一个好消息。;-) - Fredrick Brennan

3

将heredoc值分配给变量

VAR="$(cat <<'VAREOF'
abc'asdf"
$(dont-execute-this)
foo"bar"''
VAREOF
)"

用作命令参数

echo "$(cat <<'SQLEOF'
xxx''xxx'xxx'xx  123123    123123
abc'asdf"
$(dont-execute-this)
foo"bar"''
SQLEOF
)"

当我尝试第一种方法时,似乎在行之间没有行终止符。这可能是我的Linux机器上的某种配置问题? - Kemin Zhou
1
这可能意味着当您回显变量时,您没有在其周围加引号...尝试像这样:echo "$VAR" - Brad Parks

2
感谢dimo414的答案,这展示了他的优秀解决方案如何工作,并且展示了您可以轻松地在文本中使用引号和变量:

示例输出

$ ./test.sh

The text from the example function is:
  Welcome dev: Would you "like" to know how many 'files' there are in /tmp?

  There are "      38" files in /tmp, according to the "wc" command

test.sh

#!/bin/bash

function text1()
{
  COUNT=$(\ls /tmp | wc -l)
cat <<EOF

  $1 Would you "like" to know how many 'files' there are in /tmp?

  There are "$COUNT" files in /tmp, according to the "wc" command

EOF
}

function main()
{
  OUT=$(text1 "Welcome dev:")
  echo "The text from the example function is: $OUT"
}

main

看到文本中的未匹配引号会很有趣,可以看看它如何处理。也许像这样 别惊慌,有“$COUNT”个文件,这样撇号/单引号会让事情变得有趣。 - dragon788

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接