Why is [ "$(ls *.jpg | wc -l)" = 0 ] always false unless the quotes are removed?

Viewed 56

Given that there are no jpg files in my current working directory, why do the two scripts jpg1.sh and jpg2.sh give different results? How to understand the difference? Note that the only difference between the scripts is whether to have double quotes around the $() command substitution.

$ cat jpg1.sh
#!/bin/bash
if [ "$(ls *.jpg | wc -l)" = 0 ]; then
    echo "yes"
else
    echo "no"
fi

$ ./jpg1.sh
ls: *.jpg: No such file or directory
no

$ cat jpg2.sh
#!/bin/bash
if [ $(ls *.jpg | wc -l) = 0 ]; then
    echo "yes"
else
    echo "no"
fi

$ ./jpg2.sh
ls: *.jpg: No such file or directory
yes

Follow-up

I testify that the ticked answer is to the point -- wc -l has some extra white spaces in its return value. After adding set -x to both scripts, the difference surfaces for itself.

$ ./jpg1.sh
++ wc -l
++ ls '*.jpg'
ls: *.jpg: No such file or directory
+ '[' '       0' = 0 ']'
+ echo no
no

$ ./jpg2.sh
++ wc -l
++ ls '*.jpg'
ls: *.jpg: No such file or directory
+ '[' 0 = 0 ']'
+ echo yes
yes

By the way my system is macOS Catalina.

1 Answers

On some systems this is false because wc -l pads its answer with whitespace, and the string you're comparing against -- 0 -- doesn't contain any whitespace at all. When you use quotes, the exact output is compared; when you leave them off, the output is split into individual words based on whitespace, and each of those words is expanded as a glob, before it's put on the [ command line. To determine whether you're on such a system, add set -x to your script to enable trace-level logging so you can see the exact values [ is being asked to compare.

In present circumstances, the entire problem is trivially avoided: There's no reason to use either ls or wc for this purpose.

#!/usr/bin/env bash

shopt -s nullglob
jpegs=( *.jpg )
if (( ${#jpegs[@]} == 0 )); then
  echo "No JPEG files were found"
else
  echo "Exactly ${#jpegs[@]} JPEG files were found"
fi
Related