144

I need to write some complex xml to a variable inside a bash script. The xml needs to be readable inside the bash script as this is where the xml fragment will live, it's not being read from another file or source.

So my question is this if I have a long string which I want to be human readable inside my bash script what is the best way to go about it?

Ideally I want:

  • to not have to escape any of the characters
  • have it break across multiple lines making it human readable
  • keep it's indentation

Can this be done with EOF or something, could anyone give me an example?

e.g.

String = <<EOF <?xml version="1.0" encoding='UTF-8'?> <painting> <img src="madonna.jpg" alt='Foligno Madonna, by Raphael'/> <caption>This is Raphael's "Foligno" Madonna, painted in <date>1511</date>-<date>1512</date>.</caption> </painting> EOF 
2
  • I'm willing to bet that you're just going to dump that data into a stream again. Why store it in a variable when you could make things more complex and use streams? Commented Sep 26, 2013 at 22:57
  • see this too: Multi-line string with extra space (preserved indentation) Commented Jun 10, 2017 at 16:51

5 Answers 5

175

This will put your text into your variable without needing to escape the quotes. It will also handle unbalanced quotes (apostrophes, i.e. '). Putting quotes around the sentinel (EOF) prevents the text from undergoing parameter expansion. The -d'' causes it to read multiple lines (ignore newlines). read is a Bash built-in so it doesn't require calling an external command such as cat.

IFS='' read -r -d '' String <<"EOF" <?xml version="1.0" encoding='UTF-8'?> <painting> <img src="madonna.jpg" alt='Foligno Madonna, by Raphael'/> <caption>This is Raphael's "Foligno" Madonna, painted in <date>1511</date>-<date>1512</date>.</caption> </painting> EOF 
24
  • 6
    cat is an external command. Not using it saves doing that. Plus, some have the philosophy that if you're using cat with fewer than two arguments "Ur doin' it wrong" (which is distinct from "useless use of cat"). Commented Oct 9, 2009 at 0:03
  • 15
    and never ever indent second EOF.... (multiple table to head bangs involved) Commented May 18, 2012 at 19:40
  • 12
    I tried to use the above statement while set -e. It seems read always returns non-zero. You can thick this behaviour by using ! read -d ....... Commented Nov 8, 2012 at 10:56
  • 5
    @DennisWilliamson: "Proper" error handling in shell is prohibitively tedious. set -e is imperfect and lays the occasional trap, but makes scripts far more reliable. Commented Nov 19, 2012 at 22:42
  • 14
    And if you are using this multi-line String variable to write to a file, put the variable around "QUOTES" like echo "${String}" > /tmp/multiline_file.txt or echo "${String}" | tee /tmp/multiline_file.txt. Took me more than an hour to find that. Commented Apr 20, 2014 at 16:16
44

You've been almost there. Either you use cat for the assembly of your string or you quote the whole string (in which case you'd have to escape the quotes inside your string):

#!/bin/sh VAR1=$(cat <<EOF <?xml version="1.0" encoding='UTF-8'?> <painting> <img src="madonna.jpg" alt='Foligno Madonna, by Raphael'/> <caption>This is Raphael's "Foligno" Madonna, painted in <date>1511</date>-<date>1512</date>.</caption> </painting> EOF ) VAR2="<?xml version=\"1.0\" encoding='UTF-8'?> <painting> <img src=\"madonna.jpg\" alt='Foligno Madonna, by Raphael'/> <caption>This is Raphael's \"Foligno\" Madonna, painted in <date>1511</date>-<date>1512</date>.</caption> </painting>" echo "${VAR1}" echo "${VAR2}" 
5
  • 1
    Unfortunately, the apostrophe in "Raphael's" makes the first one not work. Commented Oct 8, 2009 at 12:37
  • Both assignments work for me eventually. The single quote in VAR1 should not be a problem (at least not for bash). Maybe you have been misled by the syntax highlighting? Commented Oct 8, 2009 at 21:13
  • 2
    It works in a script, but not at a Bash prompt. Sorry for not being clearer. Commented Oct 9, 2009 at 6:08
  • 3
    It is better to quote EOF as 'EOF' or "EOF", otherwise shell variables will be parsed. Commented May 6, 2018 at 13:43
  • 2
    Be sure to echo "$VAR1" with quotes to inspect your work or newlines will not print. Commented Sep 24, 2020 at 3:04
22
#!/bin/sh VAR1=`cat <<EOF <?xml version="1.0" encoding='UTF-8'?> <painting> <img src="madonna.jpg" alt='Foligno Madonna, by Raphael'/> <caption>This is Raphael's "Foligno" Madonna, painted in <date>1511</date>-<date>1512</date>.</caption> </painting> EOF ` echo "VAR1: ${VAR1}" 

This should work fine within Bourne shell environment

4
  • 4
    +1 this solution allow variable substitution like ${foo} Commented Sep 27, 2012 at 16:54
  • Upside: sh-compatible. Downside: backticks are deprecated/discouraged in bash. Now if I had to choose between sh and bash... Commented Sep 26, 2013 at 22:55
  • 2
    since when are backticks deprecated/discouraged? just curious Commented Apr 21, 2018 at 1:50
  • @AlexanderMills shellcheck.net/wiki/SC2006 Commented Mar 13, 2024 at 5:43
6

Yet another way to do the same...

I like to use variables and special <<- who drop tabulation at begin of each lines to permit script indentation:

#!/bin/bash mapfile Pattern <<-eof <?xml version="1.0" encoding='UTF-8'?> <painting> <img src="%s" alt='%s'/> <caption>%s, painted in <date>%s</date>-<date>%s</date>.</caption> </painting> eof while IFS=";" read file alt caption start end ;do printf "${Pattern[*]}" "$file" "$alt" "$caption" "$start" "$end" done <<-eof madonna.jpg;Foligno Madonna, by Raphael;This is Raphael's "Foligno" Madonna;1511;1512 eof 

warning: there is no blank space before eof but only tabulation.

<?xml version="1.0" encoding='UTF-8'?> <painting> <img src="madonna.jpg" alt='Foligno Madonna, by Raphael'/> <caption>This is Raphael's "Foligno" Madonna, painted in <date>1511</date>-<date>1512</date>.</caption> </painting> 
Some explanations:
  • mapfile read entire here document in an array.
  • the syntaxe "${Pattern[*]}" do cast this array into a string.
  • I use IFS=";" because there is no ; in required strings
  • The syntaxe while IFS=";" read file ... prevent IFS to be modified for the rest of the script. In this, only read do use the modified IFS.
  • no fork.
2
  • Note that mapfile requires Bash 4 or higher. And the syntax "${Pattern[*]}" casts the array into a string when in quotes (as shown in the example code). Commented Jul 22, 2013 at 23:54
  • Yes, bash 4 was very new when this question was asked. Commented Jul 23, 2013 at 2:31
3

There are too many corner cases in many of the other answers.

To be absolutely sure there are no issues with spaces, tabs, IFS etc., a better approach is to use the "heredoc" construct, but encode the contents of the heredoc using uuencode as explained here:

https://stackoverflow.com/questions/6896025/#11379627.

1
  • "a better approach is to..q. encode the contents of the heredoc..." - better, for some other question. maybe. not this one though. sentence 2 is relevant. Commented Sep 16, 2024 at 15:06

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.