449

How can I output a multipline string in Bash without using multiple echo calls like so:

echo "usage: up [--level <n>| -n <levels>][--help][--version]"
echo 
echo "Report bugs to: "
echo "up home page: "

I'm looking for a portable way to do this, using only Bash builtins.

5
  • 6
    If you're outputting a usage message in response to an incorrect invocation, you would normally send that message to standard error instead of standard output, with echo >&2 ...
    – Mark Reed
    Commented Jun 10, 2012 at 17:05
  • 3
    @MarkReed The usage message is output by typing --help (which should go to standard out). Commented Jun 12, 2012 at 10:00
  • 3
    For others who come along, more info about "here documents" is available: tldp.org/LDP/abs/html/here-docs.html Commented Dec 31, 2015 at 8:08
  • 1
    Check the printf-based solution from Gordon Davidson. Despite being in the shadow of the echo or cat based approaches, it seems to be much less of a kludge. Admittedly the `printf' syntax represent a bit of a learning curve, but I'd like to ear of other drawbacks (? compatibility, performance ? ...)
    – mjv
    Commented Apr 26, 2017 at 23:49
  • 1
    Related: stackoverflow.com/questions/23929235/… Commented Oct 22, 2017 at 10:03

13 Answers 13

508

Here documents are often used for this purpose.

cat << EOF
usage: up [--level <n>| -n <levels>][--help][--version]

Report bugs to: 
up home page:
EOF

They are supported in all Bourne-derived shells including all versions of Bash.

6
  • 8
    Yup - but cat isn't a built-in.
    – Mark Reed
    Commented Jun 10, 2012 at 17:15
  • 14
    @MarkReed: That's true, but it's always available (except possibly under unusual circumstances). Commented Jun 10, 2012 at 17:17
  • 12
    +1 Thx. I've ended up using read -d '' help <<- EOF ... to read the multiline string into a variable and then echoed the result. Commented Jun 10, 2012 at 21:11
  • 4
    can i save a HEREDOC to a variable?
    – chovy
    Commented Dec 14, 2015 at 1:03
  • 4
    Bloody genius, thanks @DennisWilliamson! ProTip: If you need to escape the content from things like backticks/etc, you can double quote the delimiter on the first line, like cat << "EOF". Commented May 3, 2022 at 21:17
288

or you can do this:

echo "usage: up [--level <n>| -n <levels>][--help][--version]

Report bugs to: 
up home page: "
3
  • 2
    @OliverWeiler: It will even work in Bourne shells such as Dash and the Heirloom Bourne Shell. Commented Jun 10, 2012 at 17:05
  • 19
    Not great if you need this in a function because you'll either need to 1) outdent the string all the way to the left of your file or 2) keep it indented to line up with the rest of your code but then it prints with the indents as well
    – s g
    Commented Oct 19, 2018 at 20:45
  • 4
    Works. Also note use ' (single quotes) instead of " (double quotes) if you don't want the interpretation of the string. If you want $something or something in the output instead of it getting replaced.
    – Eric
    Commented Nov 6, 2021 at 15:03
108

Inspired by the insightful answers on this page, I created a mixed approach, which I consider the simplest and more flexible one. What do you think?

First, I define the usage in a variable, which allows me to reuse it in different contexts. The format is very simple, almost WYSIWYG, without the need to add any control characters. This seems reasonably portable to me (I ran it on MacOS and Ubuntu)

__usage="
Usage: $(basename $0) [OPTIONS]

Options:
  -l, --level <n>              Something something something level
  -n, --nnnnn <levels>         Something something something n
  -h, --help                   Something something something help
  -v, --version                Something something something version
"

Then I can simply use it as

echo "$__usage"

or even better, when parsing parameters, I can just echo it there in a one-liner:

levelN=${2:?"--level: n is required!""${__usage}"}
6
  • 4
    this worked for me in a script where the above answer does not (without modification). Commented Sep 25, 2018 at 0:16
  • 7
    This is much cleaner than involving a bunch of characters like \t and \n which are hard to find in the text and expand to make the output much different than the string in the script
    – s g
    Commented Oct 19, 2018 at 20:53
  • 2
    For some reasons it prints everything on the same line for me :/ Commented Mar 13, 2020 at 15:36
  • 2
    If you use the solution from @jorge and find that everything is on the same line, make sure you enclose the variable in quotes: ``` echo $__usage ``` will print everything on one line whereas ``` echo "$__usage" ``` will retain the newlines. Commented Apr 9, 2020 at 17:12
  • 7
    @Nicolas: Using double quotes in echo "$__usage" was necessary for me. echo $__usage did not work.
    – mfg
    Commented Apr 10, 2020 at 10:15
73

Use -e option, then you can print new line character with \n in the string.

For example:

echo -e "This will be the first line \nand this will be on the second line"
5
  • 9
    Those man pages are for the system-supplied echo command, /bin/echo, which on Mac OS has no -e option. when you're using bash on those systems, its built-in echo command takes over. You can see this by explicitly typing /bin/echo whatever and observing the difference in behavior. To see the documentation for the built-in, type help echo.
    – Mark Reed
    Commented Jun 10, 2012 at 16:59
  • 1
    /bin/echo is often different from one OS to another and different from Bash's builtin echo. Commented Jun 10, 2012 at 17:01
  • @MarkReed: I'll try later, but thanks for the info. +1. I will just leave my answer here, since there are quite a lot of good discussion going on.
    – nhahtdh
    Commented Jun 10, 2012 at 17:01
  • 10
    echo -e is not portable -- for example, some implementations of echo will print the "-e" as part of the output. If you want portability, use printf instead. For example, /bin/echo on OS X 10.7.4 does this. IIRC the bash builtin echo was also weird under 10.5.0, but I don't remember the details any more. Commented Jun 10, 2012 at 17:02
  • 2
    echo -e has bitten me before... Definitely use printf or cat with a heredoc. The <<- variant of here docs are especially nice because you can strip leading indentation in the output, but indent for readability in the script
    – zbeekman
    Commented Jun 7, 2017 at 2:10
29

Since I recommended printf in a comment, I should probably give some examples of its usage (although for printing a usage message, I'd be more likely to use Dennis' or Chris' answers). printf is a bit more complex to use than echo. Its first argument is a format string, in which escapes (like \n) are always interpreted; it can also contain format directives starting with %, which control where and how any additional arguments are included in it. Here are two different approaches to using it for a usage message:

First, you could include the entire message in the format string:

printf "usage: up [--level <n>| -n <levels>][--help][--version]\n\nReport bugs to: \nup home page: \n"

Note that unlike echo, you must include the final newline explicitly. Also, if the message happens to contain any % characters, they would have to be written as %%. If you wanted to include the bugreport and homepage addresses, they can be added quite naturally:

printf "usage: up [--level <n>| -n <levels>][--help][--version]\n\nReport bugs to: %s\nup home page: %s\n" "$bugreport" "$homepage"

Second, you could just use the format string to make it print each additional argument on a separate line:

printf "%s\n" "usage: up [--level <n>| -n <levels>][--help][--version]" "" "Report bugs to: " "up home page: "

With this option, adding the bugreport and homepage addresses is fairly obvious:

printf "%s\n" "usage: up [--level <n>| -n <levels>][--help][--version]" "" "Report bugs to: $bugreport" "up home page: $homepage"
0
24

Also with indented source code you can use <<- (with a trailing dash) to ignore leading tabs (but not leading spaces).

For example this:

if [ some test ]; then
    cat <<- xx
        line1
        line2
xx
fi

Outputs indented text without the leading whitespace:

line1
line2
5
  • That did not work for me. What shell are you using?
    – four43
    Commented Jun 11, 2019 at 22:55
  • Did not work in bash 4.4.19 in Ubuntu. It did not remove the spacing before line1 and line2
    – four43
    Commented Jun 12, 2019 at 20:22
  • 1
    @four43, You were right. Does not work to remove leading spaces. However, does remove leading tabs. So I corrected my answer from tabs and spaces, to tabs and not spaces. Sorry for the mistake. I checked the manual and it clearly just says tabs are removed. Thanks for bringing this to my attention. Commented Jun 13, 2019 at 23:03
  • It works with bash and zsh on MacOS 11.4. Great tip!
    – Philipp
    Commented Jun 6, 2021 at 22:03
  • ProTip: Use double quotes around your delimiter to escape things, like "xx". Commented May 3, 2022 at 21:16
22

I usually go with the builtin read command which I think is more flexible and intuitive. It reads the contents of a line into a variable, and allows for word splitting that is tied to the special shell variable IFS. Refer to this blog or even the man page for more details.

read -r -d '' usage <<-EOF
    usage: up [--level <n>| -n <levels>][--help][--version] 

    Report bugs to: $report server
    up home page: $HOME
EOF
echo "$usage"
1
  • 2
    Best thing about this is that <<- (with the minus sign) ignores leading tab characters so that you can indent your message inside the code without indenting it when printed.
    – BUFU
    Commented Oct 25, 2021 at 8:24
7

One more thing, using printf with predefined variable (here: msg) as template.

msg="First line %s
Second line %s
Third line %s
"

one='additional message for the first line'
two='2'
tri='this is the last one'

printf "$msg" "$one" "$two" "$tri"

This ^^^ will print whole message with additional vars inserted instead of %s in provided order.

1
  • This works well in Makefiles Commented May 17, 2022 at 21:44
2
    You can write your
    text
        freely,
                   in a separate: 
                             ----file.

and then

echo "$(</pathto/your_multiline_file)"
1

Building on @Gabriel Staples's answer...

Create these two functions.

dedent() {
    local -n reference="$1"
    reference="$(echo "$reference" | sed 's/^[[:space:]]*//')"
}

log_multi() {
    dedent $1
    printf "$1"
}

# then log
log_multi "
from os import path
import os
def test_dummy():
    assert 1==1
"

1

Option 1: += concatenation operator

To maintain pretty indentation on a text line you want to take multiple lines in code, but not when printed, use the += concatenation operator, like this:

# function definition
some_func() {
    text="This is the first part of a really long string "
    text+="that I want to be able to write in multiple lines "
    text+="without having to worry about indentation. I also "
    text+="want it to print as a single line in this case."

    echo "$text"
}

# call the function
some_func

Example output:

This is the first part of a really long string that I want to be able to write in multiple lines without having to worry about indentation. I also want it to print as a single line in this case.

Note/going further: you could forcefully put line breaks in the text string above if you wanted to, using \n in the text, such as in echo -e "some text\n" or printf "%s\n" "some text".

Option 2: dedent function for natural text with line breaks

You can also do this Python-like trick:

dedent() {
    local -n reference="$1"
    reference="$(echo "$reference" | sed 's/^[[:space:]]*//')"
}

text="this is line one
      this is line two
      this is line three\n"

# `text` is passed by reference and gets dedented
dedent text

printf "$text"

Output withOUT calling dedent first:

this is line one
      this is line two
      this is line three

...and WITH calling dedent first (as shown above):

this is line one
this is line two
this is line three

For a full explanation, see where I've already written about this:

  1. Equivalent of python's textwrap dedent in bash
  2. Multi-line string with extra space (preserved indentation)

And of course, thanks to @Andreas Louv for showing me the sed part of that function here.

2
0

Here is how I've done:

function help_text {
  printf "\n\
Usage: ./cpanel-to-cc.sh [arguments] ... \n\
Examples: \n\
\t ./cpanel-to-cc.sh --client-id 123123 --api-key abc123def456 --domain example.com \n\
\t ./cpanel-to-cc.sh --client-id 123123 --tmp-dir /home/user/cpanel-to-cc \n\
\t ./cpanel-to-cc.sh --resync --domain example.com \n\
\t ./cpanel-to-cc.sh --purge \n\
\n\
Arguments: \n\
Option \t\t\t Long option \t\t\t Function \n\
 -c <id> \t\t --client-id <id> \t\t Specify the SiteHost Client ID \n\
 -k <key> \t\t --api-key <key> \t\t Specify the SiteHost API key with access to Cloud, Job and Server modules \n\
 -d <domain> \t\t --domain <domain> \t\t The cPanel domain to migrate. If not specified we try migrate all \n\
 -t <directory> \t --tmp-dir <directory> \t\t Directory to store temporary files and logs. Default is: $TMP_DIR \n\
 -v \t\t\t --verbose \t\t\t Print debugging/verbose information \n\
 -y \t\t\t --assume-yes \t\t\t Automatic yes to prompts. Assume \"yes\" as answer to all prompts \n\
 -r \t\t\t --resync \t\t\t Use credentials stored and copy data into Container already created. \n\
 -p \t\t\t --purge \t\t\t Remove any metadata stored on the the server. This removes any files in: $TMP_DIR \n\
 -h \t\t\t --help \t\t\t Display this help and exit \n\
 \n"
}
0
-1

Simply use three quotaion marks instead of one: """your multiline text here"""

3
  • Are you thinking of Python?
    – Chris
    Commented Apr 6 at 15:51
  • 1
    Thanks, but this is not a new answer to the question. The first two quotes are just an empty string, and then the third starts a new string. You could also use 5 quotes with the same effect. Or 7, or 9, or ...
    – rand'Chris
    Commented Apr 15 at 13:21
  • It is not about python it also works in shell and other programming languages. I even use it in adb shell of android and it works
    – SuTalk
    Commented Jun 8 at 20:15

Not the answer you're looking for? Browse other questions tagged or ask your own question.