671

I want to write some pre-defined texts to a file with the following:

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

echo -e $text > filename

I'm expecting something like this:

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

But got this:

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

I'm positive that there is no space after each \n, but how does the extra space come out?

Mateusz Piotrowski
  • 8,029
  • 10
  • 53
  • 79
cizixs
  • 12,931
  • 6
  • 48
  • 60
  • 2
    I'm not sure but.. how if you just typed `text="this is line one\nthis is line two\nthis is line three"` in the same one line..? (without any enter) – Yohanes Khosiawan 许先汉 May 29 '14 at 08:49
  • 7
    Remove the `\n` on each line, you have already hit newline to move to the new line – Mark Setchell May 29 '14 at 08:50
  • You already given `\n`.So why you put next line in new line? Simply `text="this is line one\nthis is line two\nthis is line three"` – Jayesh Bhoi May 29 '14 at 08:52
  • 1
    Removing the `\n` at the end of each line causes the output to all run together on a single line. – Jonathan Hartley Oct 02 '15 at 17:50
  • 36
    Aha: Putting double quotes around the `"$text"` in the echo line is crucial. Without them, none of the newlines (both literal and '\n') work. With them, they all do. – Jonathan Hartley Oct 02 '15 at 18:18
  • Always quote your bash variables. ALWAYS... unless you know why it should not be quoted. – Always Asking Dec 19 '19 at 06:47
  • There is a white space after your \n characters I guess that's the reason you get that output. Try to delete these whitespaces. – ZE0TRON Jan 21 '21 at 10:57
  • With a great-deal of effort, study, and experimenting over the last couple of years, I was able to write what I consider to be a very [clean-looking, easy-to-read, almost "Pythonic" answer that I am very proud of here](https://stackoverflow.com/a/71169983/4561887), and which also answers your question of `but how does the extra space come out?` (this may be the only answer that does that). – Gabriel Staples Feb 18 '22 at 08:00
  • See also: [How to output a multiline string in Bash?](https://stackoverflow.com/q/10969953/4561887) – Gabriel Staples Feb 18 '22 at 08:08

14 Answers14

997

Heredoc sounds more convenient for this purpose. It is used to send multiple commands to a command interpreter program like ex or cat

cat << EndOfMessage
This is line 1.
This is line 2.
Line 3.
EndOfMessage

The string after << indicates where to stop.

To send these lines to a file, use:

cat > $FILE <<- EOM
Line 1.
Line 2.
EOM

You could also store these lines to a variable:

read -r -d '' VAR << EOM
This is line 1.
This is line 2.
Line 3.
EOM

This stores the lines to the variable named VAR.

When printing, remember the quotes around the variable otherwise you won't see the newline characters.

echo "$VAR"

Even better, you can use indentation to make it stand out more in your code. This time just add a - after << to stop the tabs from appearing.

read -r -d '' VAR <<- EOM
    This is line 1.
    This is line 2.
    Line 3.
EOM

But then you must use tabs, not spaces, for indentation in your code.

alphadog
  • 10,473
  • 2
  • 13
  • 19
351

If you're trying to get the string into a variable, another easy way is something like this:

USAGE=$(cat <<-END
    This is line one.
    This is line two.
    This is line three.
END

)

If you indent your string with tabs (i.e., '\t'), the indentation will be stripped out. If you indent with spaces, the indentation will be left in.

NOTE: It is significant that the last closing parenthesis is on another line. The END text must appear on a line by itself.

Krishnaraj
  • 2,360
  • 1
  • 32
  • 55
Andrew Miner
  • 5,587
  • 2
  • 22
  • 26
  • 3
    Is is that significant? Works on my mac with `)` on the same line. I think it's because what goes between `$(` and `)` is gonna execute in its own universe, and the actual command won't see ')' anyway. I wonder if it works for others too. – deej Jan 17 '17 at 17:40
  • It's possible that different shells will interpret things differently. In the [bash documentation](http://www.tldp.org/LDP/abs/html/here-docs.html) it doesn't seem to say anything one way or the other, but all the examples have it on its own line. – Andrew Miner Jan 18 '17 at 16:08
  • Fun thing is, I found you answer when trying to set value for USAGE variable too. So your answer matches exactly. :) – Bunyk Mar 22 '18 at 13:52
  • 3
    @deej From the [POSIX spec](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_07_04): _The here-document … continues until there is a line containing only the delimiter and a _ – Fornost May 12 '18 at 18:56
  • 1
    @Fornost It does not contradict what I said – deej May 12 '18 at 21:34
  • 1
    You say that the actual command won't see ')', and I agree. However, the actual command will see if there is a newline or no newline after END. Bash gives me a warning: _here-document delimited by end-of-file_. – Fornost May 15 '18 at 18:25
  • 1
    @Fornost typically that means you have a space instead of a tab if you are using the `<<-END` style. Run `shellcheck` against your script to see if it spots any issues. – dragon788 Sep 26 '18 at 21:59
  • 6
    It's important to note the difference between `echo $USAGE` vs `echo "$USAGE"` when using this. – Kashyap Jul 03 '20 at 21:41
  • 1
    Both I and others on this thread have successfully used this approach on the Mac when using the `bash` shell. I suspect your problem may be that newer Macs don't use the `bash` shell by default anymore (they use `zsh`), and the syntax may be different on that shell. – Andrew Miner Jul 07 '20 at 20:57
  • It's good but unfortunately removes comma from string within it – muhammad tayyab Oct 27 '20 at 14:49
  • I happen to write sql inside this. Tuns out that if you write * inside it will expand the file system. Select * from table turned into select dir1, dir2, dir3 from table. – The Fool Nov 16 '20 at 03:43
  • For some reason by bash (either bash or tcsh) shows all the lines appended. EDIT: Ok, remember to print it with echo "$USAGE" – Kiteloopdesign Mar 23 '21 at 11:36
  • As the END needs to be first on a line of it's own, it kind of defeats the whole purpose. Then you cant indent the whole statement, like inside an IF statement. – Daniele Testa Aug 21 '21 at 07:36
  • This broke the syntax highlighting of my text editor – perrocallcenter Jul 20 '23 at 23:51
125

echo adds spaces between the arguments passed to it. $text is subject to variable expansion and word splitting, so your echo command is equivalent to:

echo -e "this" "is" "line" "one\n" "this" "is" "line" "two\n"  ...

You can see that a space will be added before "this". You can either remove the newline characters, and quote $text to preserve the newlines:

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

echo "$text" > filename

Or you could use printf, which is more robust and portable than echo:

printf "%s\n" "this is line one" "this is line two" "this is line three" > filename

In bash, which supports brace expansion, you could even do:

printf "%s\n" "this is line "{one,two,three} > filename
Josh Jolly
  • 11,258
  • 2
  • 39
  • 55
  • 2
    Thanks for explaining why mod sees extra space when using “echo” command. This answer also is working well when using in bash script (as oppose to interactive shell session). Feel like this is the real answer and should be an accepted answer. – onelaview Oct 18 '18 at 00:20
  • 2
    This should be accepted answer. All other answers provided tons of interesting *other* ways to solve OP's pain, but technically the question was "how does the extra space come out" and no one addressed this :-) !!! Thank you, Josh for making -1 mystery! – Dmitry Shevkoplyas Jun 14 '19 at 02:22
71

in a bash script the following works:

#!/bin/sh

text="this is line one\nthis is line two\nthis is line three"
echo -e $text > filename

alternatively:

text="this is line one
this is line two
this is line three"
echo "$text" > filename

cat filename gives:

this is line one
this is line two
this is line three
Chris Maes
  • 35,025
  • 12
  • 111
  • 136
48

I've found more solutions since I wanted to have every line properly indented:

  1. You may use echo:

    echo    "this is line one"   \
        "\n""this is line two"   \
        "\n""this is line three" \
        > filename
    

    It does not work if you put "\n" just before \ on the end of a line.

  2. Alternatively, you can use printf for better portability (I happened to have a lot of problems with echo):

    printf '%s\n' \
        "this is line one"   \
        "this is line two"   \
        "this is line three" \
        > filename
    
  3. Yet another solution might be:

    text=''
    text="${text}this is line one\n"
    text="${text}this is line two\n"
    text="${text}this is line three\n"
    printf "%b" "$text" > filename
    

    or

    text=''
    text+="this is line one\n"
    text+="this is line two\n"
    text+="this is line three\n"
    printf "%b" "$text" > filename
    
  4. Another solution is achieved by mixing printf and sed.

    if something
    then
        printf '%s' '
        this is line one
        this is line two
        this is line three
        ' | sed '1d;$d;s/^    //g'
    fi
    

    It is not easy to refactor code formatted like this as you hardcode the indentation level into the code.

  5. It is possible to use a helper function and some variable substitution tricks:

    unset text
    _() { text="${text}${text+
    }${*}"; }
    # That's an empty line which demonstrates the reasoning behind 
    # the usage of "+" instead of ":+" in the variable substitution 
    # above.
    _ ""
    _ "this is line one"
    _ "this is line two"
    _ "this is line three"
    unset -f _
    printf '%s' "$text"
    
Mateusz Piotrowski
  • 8,029
  • 10
  • 53
  • 79
  • 3b is my preferred solution when wanting to preserve code indentation and string indentation independently, at least when I cannot use 2 in variable assignment, and is more readable than 3a. When I don't care I just keep the quoted string open over the several lines. – Pysis Apr 28 '19 at 16:04
  • Very nice answer especially 3b – Sumit Trehan Sep 10 '19 at 11:34
  • 1
    "It does not work if you put "\n" just before \ on the end of a line." - is a **great tip** when using echo. – Noam Manos May 26 '20 at 15:57
  • 1
    printf saved me in a bare-bones docker container and no need for \n was really nice – Slate Nov 03 '20 at 16:41
14

The following is my preferred way to assign a multi-line string to a variable (I think it looks nice).

read -r -d '' my_variable << \
_______________________________________________________________________________

String1
String2
String3
...
StringN
_______________________________________________________________________________

The number of underscores is the same (here 80) in both cases.

Frank-Rene Schäfer
  • 3,182
  • 27
  • 51
7

Hard to read:

This looks way too bash-like :) (hard to read) for my general taste:

cat << EndOfMessage
This is line 1.
This is line 2.
Line 3.
EndOfMessage

Better, easier-to-read:

Let's get something a little more Pythonic (this is still bash):

text="this is line one
      this is line two
      this is line three\n"
dedent text
printf "$text"             # print to screen
printf "$text" > file.txt  # print to a file

Ah...that's better. :) It reminds me of Python's textwrap.dedent() function which I use here.

Here is what the magic dedent function looks like:

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

Sample output to screen:

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

WithOUT calling dedent text first`, the output would look like this:

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

The variable text is passed to dedent by reference, so that what is modified inside the function affects the variable outside the function.

For more details and explanation and references, see my other answer here: Equivalent of python's textwrap dedent in bash

Problem with your original attempt

OP's quote (with my emphasis added):

I'm positive that there is no space after each \n, but how does the extra space come out?

Your original attempt was this:

text="this is line one\n
this is line two\n
this is line three"
echo -e $text

...but your output had an extra space before the 2nd and 3rd line. Why?

By deduction and experimentation, my conclusion is that echo converts the actual newline at the end of the line (the one you got when you actually pressed Enter) into a space. That space therefore shows up before the line just after each \n in the text.

So, the solution is to escape the real newline at the end of each line by putting a backslash \ at the end of any line within the quotes of your string, like this:

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

echo -e "$text"

Do NOT put a space before those trailing backslashes (like this: text="this is line one\n \) or that space will go right back into your output and cause you the same problem with the extra space!

OR, just use my technique with the dedent function above, which also has the added functionality of being able to indent with your code to look really pretty and nice and readable.

Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
6

I came hear looking for this answer but also wanted to pipe it to another command. The given answer is correct but if anyone wants to pipe it, you need to pipe it before the multi-line string like this

echo | tee /tmp/pipetest << EndOfMessage
This is line 1.
This is line 2.
Line 3.
EndOfMessage

This will allow you to have a multi line string but also put it in the stdin of a subsequent command.

Frank Bryce
  • 8,076
  • 4
  • 38
  • 56
2

In bash and dash (the ones I tested, should work for yours), you can use some Python-style triple-quote strings:

echo """Hello
World!"""

This outputs:

Hello
World!
1

There are many ways to do it. For me, piping the indented string into sed works nicely.

printf_strip_indent() {
   printf "%s" "$1" | sed "s/^\s*//g" 
}

printf_strip_indent "this is line one
this is line two
this is line three" > "file.txt"

This answer was based on Mateusz Piotrowski's answer but refined a bit.

Beni Trainor
  • 346
  • 1
  • 11
0

it will work if you put it as below:

AA='first line
\nsecond line 
\nthird line'
echo $AA
output:
first line
second line
third line
luk
  • 17
  • 1
0

Just to mention a simple one-line concatenation as it can be useful sometimes.

# for bash

v=" guga "$'\n'"   puga "

# Just for an example.
v2="bar "$'\n'"   foo "$'\n'"$v"

# Let's simplify the previous version of $v2.
n=$'\n'
v3="bar ${n}   foo ${n}$v"

echo "$v3" 

You'll get something like this

bar 
   foo 
 guga 
   puga 

All leading and ending white spaces will be preserved right for

echo "$v3" > filename
it3xl
  • 2,372
  • 27
  • 37
0

Or keeping text indented with whitespaces:

#!/bin/sh

sed 's/^[[:blank:]]*//' >filename <<EOF
    this is line one
    this is line two
    this is line three
EOF

Same but using a varible:

#!/bin/sh

text="$(sed 's/^[[:blank:]]*//' << whatever
    this is line one
    this is line two
    this is line three
)"

echo "$text" > filename

;-)

ish-west
  • 81
  • 2
0

As per @alphado's answer, and subsequent comments, this is what worked for me and gives 0 exit status.

read -r -d '\0' VAR <<- EOM
    This is line 1.
    This is line 2.
    Line 3.
\0
EOM
  • <<- to ignore leading tabs
  • set \0 as explicit "end marker"
  • this also substitutes variables between the EOMs, so use \$ to print verbatim.

(Just putting this as a separate answer because it got hard to decipher this between all the comments, and did not want to change too much in original answer)

AmanicA
  • 4,659
  • 1
  • 34
  • 49