10

I'm trying to store multiple lines in a bash variable, but it doesn't seem to work.

For example, if I list /bin one file per line and store it in $LS, then I pass $LS as stdin to wc, it always returns 1:

$ ls -1 /bin | wc -l
134
$ LS=$(ls -1 /bin); wc -l <<< $LS
1

If I try to output to screen, I get various results: echo prints all the lines on a single line, while printf prints only the first line:

#!/bin/bash
LS=$(ls -1 /bin)
echo $LS
printf $LS

So, a bash variable can contain multiple lines?

Wizard79
  • 245

3 Answers3

17

You need to double quote it (and you should double quote variables in most case):

echo "$LS"

But don't use echo to print variables content, using printf instead:

printf '%s\n' "$LS"
cuonglm
  • 153,898
  • 1
    Just to put a finer point on the overall answer: printf expects a format string to be its first parameter, which is why they didn't see what they expected. If they want to see newlines in the output from printf, arbtrarily split on spaces, printf "%s\n" $LS would do it. – Jeff Schaller Jul 09 '15 at 12:39
  • the %LS should be $LS, btw (I couldn't make such a short edit) – Jeff Schaller Jul 09 '15 at 12:40
  • Thanks! It of course works also with wc and other commands: wc -l <<< "$LS" – Wizard79 Jul 09 '15 at 12:43
  • 1
    @JeffSchaller: No, you shouldn't unquote variable in this case, just using printf '%s\n' "$LS" if you want to see newline. That's mis-typing, fixed it! – cuonglm Jul 09 '15 at 13:03
  • Why shouldn't you use echo to print variables ? – 123 Jul 09 '15 at 14:31
  • @User112638726 - see the linked article – Jeff Schaller Jul 09 '15 at 14:58
  • @JeffSchaller Oh, didn't even see the links! – 123 Jul 09 '15 at 15:07
  • i don't think this works -- in my case, i'm saving $(git diff), which has mentions of \n that I've added, which subsequently get interpreted as a newline by echo. – cnst Nov 05 '15 at 13:51
  • oh, so, not too surprisingly, the solution is to use printf instead of echo, as echo makes no distinction between actual newlines and \n literals. V=$(printf 'line1:test\\ntest\\n\nline2\n') ; printf '%s\n' "$V" – cnst Nov 05 '15 at 14:03
9

The newlines are in the variable. LS=$(ls -1) sets the variable LS to the output of ls -1 (which produces the same output as ls, by the way, except when the output goes to a terminal), minus trailing newlines.

The problem is that you're removing the newlines when you print out the value. In a shell script, $LS does not mean “the value of the variable LS”, it means “take the value of LS, split it into words according to IFS and interpret each word as a glob pattern”. To get the value of LS, you need to write "$LS", or more generally to put $LS between double quotes.

echo "$LS" prints the value of LS, except in some shells that interpret backslash characters, and except for a few values that begin with -.

printf "$LS" prints the value of LS as long as it doesn't contain any percent or backslash character and (with most implementations) doesn't start with -.

To print the value of LS exactly, use printf %s "$LS". If you want a newline at the end, use printf '%s\n' "$LS".

Note that $(ls) is not, in general, the list of files in the current directory. This only works when you have sufficiently tame file names. To get the list of file names (except dot files), you need to use a wildcard: *. The result is a list of strings, not a string, so you can't assign it to a string variable; you can use an array variable files=(*) in shells that support them (ksh93, bash, zsh).

For more information, see Why does my shell script choke on whitespace or other special characters?

0

The aforementioned

printf '%s\n' "$LS"

is the only correct solution.

Let me demonstrate that the other proposed solutions, both with echo and printf, simply do not work properly:

$ mkdir t
$ cd t
$ touch \\n
$ LS=$(ls -l)
$ echo "$LS"
total 0
-rw-r--r--  1 domain  domain  0 Nov  5 06:12

$ printf "$LS\n"
total 0
-rw-r--r--  1 domain  domain  0 Nov  5 06:12

$ printf '%s\n' "$LS"
total 0
-rw-r--r--  1 domain  domain  0 Nov  5 06:12 \n
$ ls -l
total 0
-rw-r--r--  1 domain  domain  0 Nov  5 06:12 \n
$ rm \\n
$ cd ..
$ rmdir t
$

(One can also test with V=$(printf 'line0:\\n\\n\nline1.\n') printf '%s\n' "$V" and variations.)

Whereas \n in the filenames may not be that common, store git diff into a variable, and your chances of encountering literal \n increase dramatically.

cnst
  • 3,283
  • Note that ksh and zsh also support print -r -- "$LS" and zsh also has echo -E - $LS. csh has echo $LS:q though that doesn't output the newline character if $LS is empty, and getting a multiline value into $LS in the first place is quite tricky in csh. – Stéphane Chazelas Nov 05 '15 at 14:33