2

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.

aafulei
  • 2,085
  • 12
  • 27
  • 5
    ...that said, `ls | wc` isn't good practice regardless. See [BashFAQ #4: *How can I check whether a directory is empty or not? How do I check for any *.mpg files, or count how many there are?*](http://mywiki.wooledge.org/BashFAQ/004) for more reliable alternatives. Also relevant is [Why you shouldn't parse the output of `ls`](https://mywiki.wooledge.org/ParsingLs). – Charles Duffy Oct 19 '20 at 14:43
  • 2
    ...there's some relevant discussion in [`wc` on OS X return includes spaces](https://stackoverflow.com/questions/30927590/wc-on-osx-return-includes-spaces) – Charles Duffy Oct 19 '20 at 14:58

1 Answers1

8

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
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Thank you for sharing nice answer sir. You mentioned `On some systems this is false because wc -l pads its answer with whitespace` when I do `wc -l < Input_file` it prints number of lines with no spaces in starting(may be Linux comes in criteria of not printing spaces before). So is statement true for both command styles(`wc -l Input_file` OR `wc -l < Input_file`) or its nothing related to way of passing Input_file, thank you. – RavinderSingh13 Oct 19 '20 at 14:56
  • …that said, square braces are for string comparisons. Since the output of `wc` is expected to be numeric, one might also consider an arithmetic expression such as `if (( $(ls *.jpg | wc -l) == 0 ))`. One would be wrong, but arguably _less wrong_. But this answer is even less wrong. – kojiro Oct 19 '20 at 14:58
  • 1
    @kojiro even sticking with the original `[`...`]`, you could use `-eq` to do a numeric instead of string comparison. But as Charles noted in his comment on the question, the entire approach of using `ls | wc` is fundamentally flawed. Too many variables make it unreliable, and when you're using a shell glob `ls` is basically turned into just an expensive `echo` anyway. – Mark Reed Oct 19 '20 at 15:01
  • 1
    It doesn't matter for a short standalone script like this, but FWIW, since `nullglob` is a global option, you may want to set it temporarily and then restore its previous setting, which you could so with something like `reset=$(shopt -p nullglob); shopt -s nullglob` and then `$reset` at the end. – Mark Reed Oct 19 '20 at 15:08
  • _nod_. I might `eval "$reset"` there so we continue to work if a future version of `shopt -p` includes quotes in its output, but it's a good suggestion. – Charles Duffy Oct 19 '20 at 15:09
  • @MarkReed, ...the other thing about a bare `$reset` is that it depends on IFS being at its default value to work correctly. – Charles Duffy Oct 20 '20 at 00:03