Search code examples
bashquoting

Why does echo "$out" split output onto multiple lines, if quotes suppress word-splitting?


I have very simple directory with "directory1" and "file2" in it. After

out=`ls`

I want to print my variable: echo $out gives:

directory1   file2

but echo "$out" gives:

directory1
file2

so using quotes gives me output with each record on separate line. As we know ls command prints output using single line for all files/dirs (if line is big enough to contain output) so I expected that using double quotes prevents my shell from splitting words to separate lines while ommitting quotes would split them. Pls tell me: why using quotes (used for prevent word-splitting) suddenly splits output ?


Solution

  • On Behavior Of ls

    ls only prints multiple filenames on a single line by default when output is to a TTY. When output is to a pipeline, a file, or similar, then the default is to print one line to a file.

    Quoting from the POSIX standard for ls, with emphasis added:

    The default format shall be to list one entry per line to standard output; the exceptions are to terminals or when one of the -C, -m, or -x options is specified. If the output is to a terminal, the format is implementation-defined.


    Literal Question (Re: Quoting)

    It's the very act of splitting your command into separate arguments that causes it to be put on one line! Natively, your value spans multiple lines, so echoing it unmodified (without any splitting) prints it precisely that manner.

    The result of your command is something like:

     out='directory1
     file2'
    

    When you run echo "$out", that exact content is printed. When you run echo $out, by contrast, the behavior is akin to:

    echo "directory1" "file2"
    

    ...in that the string is split into two elements, each passed as completely different argument to echo, for echo to deal with as it sees fit -- in this case, printing both those arguments on the same line.


    On Side Effects Of Word Splitting

    Word-splitting may look like it does what you want here, but that's often not the case! Consider some particular issues:

    • Word-splitting expands glob expressions: If a filename contains a * surrounded by whitespace, that * will be replaced with a list of files in the current directory, leading to duplicate results.
    • Word-splitting doesn't honor quotes or escaping: If a filename contains whitespace, that internal whitespace can't be distinguished from whitespace separating multiple names. This is closely related to the issues described in BashFAQ #50.

    On Reading Directories

    See Why you shouldn't parse the output of ls. In short -- in your example of out=`ls`, the out variable (being a string) isn't able to store all possible filenames in a useful, parsable manner.

    Consider, for instance, a file created as such:

    touch $'hello\nworld"three words here"'
    

    ...that filename contains spaces and newlines, and word-splitting won't correctly detect it as a single name in the output from ls. However, you can store and process it in an array:

    # create an array of filenames
    names=( * )
    if ! [[ -e $names || -L $names ]]; then  # this tests only the FIRST name
      echo "No names matched" >&2            # ...but that's good enough.
    else
      echo "Found ${#files[@]} files" # print number of filenames
      printf '- %q\n' "${names[@]}"
    fi