New line in bash variables

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?

Asked By: Wizard79

||

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 '%sn' "$LS"
Answered By: cuonglm

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 '%sn' "$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?

The aforementioned

printf '%sn' "$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 "$LSn"
total 0
-rw-r--r--  1 domain  domain  0 Nov  5 06:12

$ printf '%sn' "$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\nnline1.n') printf '%sn' "$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.

Answered By: cnst
Categories: Answers Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.