91

The issue:

  1. I need to assign a variable a value that is decently long.
  2. All the lines of my script must be under a certain number of columns.

So, I am trying to assign it using more than one line.

It's simple to do without indents:

VAR="This displays without \
any issues."
echo "${VAR}"

Result:

This displays without any issues.

However with indents:

    VAR="This displays with \
    extra spaces."
    echo "${VAR}"

Result:

This displays with      extra spaces.

How can I elegantly assign it without these spaces?

рüффп
  • 1,707
JamesL
  • 1,270
  • 1
  • 14
  • 19

10 Answers10

53

Here the issue is that you are surrounding the variable with double quotes (""). Remove it and things will work fine.

    VAR="This displays with \
    extra spaces."
    echo ${VAR}

Output

 This displays with extra spaces.

Here the issue is that double quoting a variable preserves all white space characters. This can be used in case if you explicitly need it.

For example,

$ echo "Hello     World    ........ ...            ...."

will print

Hello     World    ........ ...            ....

And on removing quotes, its different

$ echo Hello     World    ........ ...            ....
Hello World ........ ... ....

Here the Bash removes extra spaces in the text because in the first case the entire text is taken as a "single" argument and thus preserving extra spaces. But in the second case echo command receives the text as 5 arguments.

Quoting a variable will also be helpful while passing arguments to commands.

In the below command, echo only gets single argument as "Hello World"

$ variable="Hello World"
$ echo "$variable"

But in case of the below scenario echo gets two arguments as Hello and World

$ variable="Hello World"
$ echo $variable
Kannan Mohan
  • 3,231
  • Most answers worked well, but this was the simplest. Thanks! – JamesL Oct 24 '14 at 19:05
  • 25
    @Sman865 - please believe me when I tell you that this is actually almost definitely the most complicated answer offered here, and it is also wrong in most respects - especially in its opening declaration. Any issue surrounding value assignment cannot be in any way related to its later expansion - that's just backwards. I'm sorry Kannan, but this answer is both wrong and wrong-headed. Field-splitting expansions on $IFS is a powerful and universal tool - but code written in this way can never produce reliable results of any kind. – mikeserv Oct 25 '14 at 11:32
  • 4
    Failing to use quotes when expanding a variable causes problems sooner or later, starting with filename expansion (any of * ? []). – mr.spuratic Oct 27 '14 at 11:09
  • I agree that variables need to be enclosed inside a double quotes to prevent it from bash expansion. But for special cases we can avoid it to prevent complication of code. – Kannan Mohan Oct 27 '14 at 14:37
  • 2
    I agree with @mikeserv, this is a very bad advice if taken at face value. This will break if used without the consequences. E.g., if the variable is passed as an argument to a command, you don't want each word to be broken up as a separate argument. – haridsv Aug 10 '16 at 11:14
40

The solutions given by esuoxu and Mickaël Bucas are the common and more portable ways of doing this.

Here are a few bash solutions (some of which should also work in other shells, like zsh). Firstly with the += append operator (which works in a slightly different way for each of an integer variable, a regular variable and an array).

text="Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod "
text+="tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, "
text+="quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea ..." 

If you want newlines (or other whitespace/escapes) in the text, use $'' quoting instead:

text=$'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod\n'
text+=$'...'

Next, using printf -v to assign a formatted value to a variable

printf -v text "%s" "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed " \
                    "do eiusmod empor incididunt ut labore et dolore magna aliqua. "\
                    "Ut enim ad minim veniam ..."

The trick here is that there are more arguments than format specifiers, so unlike most printf functions, the bash one reuses the format string until it runs out. You can put a \n within the format string, or use $'', (or both) to deal with whitespace.

Next, using an array:

text=("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod "
      "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, "
      "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea ..." )

You can also use += to build the text up line by line (note the ()):

text+=("post script")

Here though, you must remember to "flatten" the array if you want the entire text content in one go

echo "$text"      # only outputs index [0], the first line
echo "${text[*]}" # output complete text (joined by first character of IFS)

(integer indexed arrays are implicitly sorted, unlike associative arrays) This gives you slightly more flexibility since you can manipulate lines and even slice and dice if needed.

Finally, using read or readarray and a "here-document":

read -r -d '' text <<-"EOT"
        Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
        tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 
        quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea ...
EOT

readarray -t textarray <<-"EOT"
        Lorem [...]
EOT  

The here-document form of <<- means all leading hard tabs are removed from the input, so you must use tabs to indent your text. Quotes around "EOT" prevent shell expansion features, so the input is used verbatim. With read it uses NUL byte delimited input, so that it will read the newline delimited text in one go. With readarray (aka mapfile, available since bash-4.0) it reads into an array, and -t strips newlines on each line.

mr.spuratic
  • 9,901
  • 1
    The read option with here document is very nice! Useful to embed python scripts and execute with python -c. I used to do script=$(cat <here document>) before, but read -r -d '' script <here document> is much better. – haridsv Aug 10 '16 at 11:44
20

There is a special heredoc syntax that removes tabs at the beginning of all lines : "<<-" (notice the dash added)

http://tldp.org/LDP/abs/html/here-docs.html

Example 19-4. Multi-line message, with tabs suppressed

You can use it like this :

v="$(cat <<-EOF
    A
        B
    C
EOF
)"
echo "$v"

Result :

A
B
C

It works only with tabs, not spaces.

Mickaël Bucas
  • 301
  • 1
  • 2
  • Here on ubuntu, the reverse is true -- spaces work, not tabs. \t works great though if you need tabs. Just use echo -e "$v" rather than echo "$v" to enable backslash characters – willscripted Sep 16 '15 at 12:09
8

This how I suggest you should do it, and I will explain why, but first I want to talk about something else...

set -- 'Arg 1: Line 1.' \
       'Arg 2: Line 2.' \
       'and so on for'  \
       'as long as you might like.'
var="$*"

A lot of the other proffered solutions here seem to suggest that you can somehow affect a shell variable's contents by altering your methods of expanding it. I can assure you this is not the case.

    string="some stuff here \
            some more stuff here."
    echo $string ${#string} 
    echo "$string" "${#string}"

OUTPUT

some stuff here some more stuff here. 53
some stuff here                 some more stuff here. 53

What you see above is first a field-split expansion, then a report on the byte-count for the expansion's source variable, then a quote-delimited expansion, and the same byte-count. While the output may differ the contents of the shell variable $string never changes at all except upon assignment.

What's more, if you do not understand why this is, you're bound to encounter some very nasty surprises sooner than later. Let's try that again, but in slightly different conditions.

    IFS=sf
    echo $string ${#string} 
    echo "$string" "${#string}"

Same $string - different environment.

OUTPUT

 ome  tu   here                  ome more  tu   here. 53
some stuff here                 some more stuff here. 53

Field splitting occurs based on the field delimiters defined in $IFS. There are two kinds of delimiters - $IFS whitespace and $IFS anything else. By default $IFS is assigned the value space tab newline - which are the three possible $IFS whitespace values. It is easily changed, though, as you can see above, and can have drastic effects on field-split expansions.

$IFS whitespace will elide by sequence to a single field - and this is why echoing an expansion containing any sequence of spaces when $IFS contains a space will evaluate to only a single space - because echo concatenates its arguments on spaces. But any non-whitespace values will not elide in the same way, and each occurring delimiter always gets a field unto itself - as can be seen in the stuff expansion above.

This is not the worst of it. Consider this other $string.

IFS=$space$tab$newline
cd emptydir
    string=" * * * \
             * * * "
    echo $string ${#string}
    echo "$string" "${#string}"    

OUTPUT

* * * * * * 30
 * * *                  * * *  30

Looks ok, right? Well, let's alter the environment again.

    touch file1 file2 file3 file4 file5
    echo $string ${#string}
    echo "$string" "${#string}"    

OUTPUT

file1 file2 file3 file4 file5 file1 file2 file3 file4 file5 file1 file2 file3 file4 file5 file1 file2 file3 file4 file5 file1 file2 file3 file4 file5 file1 file2 file3 file4 file5 30
 * * *                  * * *  30

Woah.

By default the shell will expand filename globs if it can match them. This occurs after parameter expansion and field-splitting in its parse-order and so any unquoted string is vulnerable in this way. You can toggle this behavior off with set -f if you like, but any POSIX-compatible shell will always glob by default.

This is the kind of stuff you're up against when you drop quotes on expansions to suit your indentation preferences. And even so, in every case, regardless of its expansion behavior, the actual value for $string is always still whatever it was when you last assigned it. So let's get back to the first thing.

set -- 'Arg 1: Line 1.' \
       'Arg 2: Line 2.' \
       'and so on for'  \
       'as long as you might like.'
var="$*"
echo "$var" "${#var}"

OUTPUT

Arg 1: Line 1. Arg 2: Line 2. and so on for as long as you might like. 70

I believe this is a far saner way to adapt shell syntax to your indentation preferences. What I'm doing above is assigning each individual string to a positional parameter - which can each be referenced by number like $1 or ${33} - and then assigning their concatenated values to $var using the special shell parameter $*.

This approach is not immune to $IFS, even so. Still, I consider its relationship to $IFS an added benefit in this respect. Consider:

IFS=\ ;space_split="$*"
IFS=/; slash_split="$*";IFS='
';new_line_split="$*"

echo "$space_split"
echo "$slash_split"
echo "$new_line_split"

OUTPUT

Arg 1: Line 1. Arg 2: Line 2. and so on for as long as you might like.
Arg 1: Line 1./Arg 2: Line 2./and so on for/as long as you might like.
Arg 1: Line 1.
Arg 2: Line 2.
and so on for
as long as you might like.

As you can see, $* concatenates each arg in "$@" on the first byte in $IFS. So saving its value while $IFS is differently assigned gets different field delimiters for each saved value. What you see above is the literal value for each variable, by the way. If you wanted no delimiter at all you would do:

IFS=;delimitless="$*"
echo "$delimitless" "${#delimitless}"

OUTPUT

Arg 1: Line 1.Arg 2: Line 2.and so on foras long as you might like. 67
mikeserv
  • 58,310
6

Let the shell eat the unwanted linefeeds and following spaces:

$ cat weird.sh 
#!/bin/sh

        var1="A weird(?) $(
             )multi line $(
             )text idea. $(
             )PID=$$"

        var2='You can '$(
            )'avoid expansion '$(
            )'too: PID=$$'

        var3='Or mix it: '$(
            )'To insert the PID use $$. '$(
            )"It expands to e.g. $$."

        echo "$var1"
        echo "$var2"
        echo "$var3"
$ sh weird.sh 
A weird(?) multi line text idea. PID=13960
You can avoid expansion too: PID=$$
Or mix it: To insert the PID use $$. It expands to e.g. 13960.

So it is possible... but sure it is a matter of taste to like or dislike this solution...

4

Maybe you can try this.

          echo "Test" \
               "Test2" \
               "Test3"
esuoxu
  • 41
  • 1
2

You may want to try :

echo $VAR | tr -s " "

or

myArr=($VAL)
VAL=${myArr[@]}
echo "$VAL"

and also you can check this out.

Vivian Maya
  • 262
  • 2
  • 8
2

Use a Bash Substitution Expansion

If you're using Bash, you can use a substitution expansion. For example:

$ echo "${VAR//  /}"
This displays with extra spaces.
CodeGnome
  • 7,820
1

This is a variant for setting path-like variables:

set -- "${MYDIR}/usr/local/lib" \
      :"${MYDIR}/usr/lib" \
      :"${MYDIR}/lib" \
       "${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
    export LD_LIBRARY_PATH="$*"
    LD_LIBRARY_PATH=$(sed 's/ :/:/g' <<< $LD_LIBRARY_PATH)

Using set overwrites $@, which can be saved and used later as follows:

ARGV=("$@")
exec foo "${ARGV[@]}"

The LD_LIBRARY_PATH=$(sed 's/ :/:/g' <<< $LD_LIBRARY_PATH) line eliminates spaces before colons as well as possible trailing whitespace. If only eliminating trailing spaces, use LD_LIBRARY_PATH=${LD_LIBRARY_PATH%% } instead.

This entire approach is a variant on mikeserv's excellent answer.

rsanden
  • 111
0

Don't be afraid of whitespace characters. Just remove them before you print your multiline text.

$ cat ./t.sh
#!/bin/bash

NEED_HELP=1
if [[ $NEED_HELP -eq 1 ]]; then

    lucky_number=$((1 + RANDOM % 10 + 10))

    read -r -d '' text <<'    EOF'
    NAME says hello

    Look at this helpful text:

                                                 * -**
                                 Eyjafjallajokull        Eyjafjallajokull
                            Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull
                        Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull
                    Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull
                Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull
            Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull
        Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull
    Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull

    Don't go away, please rate your experience... It'll just take two minutes.

    EOF
    text=$(echo "$text" | sed -r 's!^\s{4}!!')
    text=$(echo "$text" | sed -r "s!\bNAME\b!$0!") # Bash: text=${text//NAME/$0}
    text=$(echo "$text" | sed -r "s!\btwo\b!$lucky_number!")

    echo "$text"

fi

Output:

$ ./t.sh
./t.sh says hello

Look at this helpful text:

                                             * -**
                             Eyjafjallajokull        Eyjafjallajokull
                        Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull
                    Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull
                Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull
            Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull
        Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull
    Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull
Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull Eyjafjallajokull

Don't go away, please rate your experience... It'll just take 16 minutes.

No need to use a <<- heredoc and break your 4-space indentation with tab characters.

c0xc
  • 121