bash

How quotes in bash loop's command affect control flow?


Seeing something that is making me question my sanity:

#!/bin/env bash
set -e
for d in '/tmp/somedir/*'; do               
    for f in "${d}/*pub"; do
        echo $f;
    done
done

it returns as expected

/tmp/somedir/AAA/fileaa.pub /tmp/somedir/BBB/filebb.pub

But, if I change echo $f to echo "$f" or echo "${f}" I get:

/tmp/somedir/*/*pub

And I am having a hard time understanding why. For starters, it is a single item, so it is not something that is only affecting the echo line.

using GNU bash, version 5.2.37(1)-release (x86_64-pc-linux-gnu)

here's the shopts on that host

assoc_expand_once       off
cdable_vars     off
cdspell         off
checkhash       off
checkjobs       off
checkwinsize    on
cmdhist         on
compat31        off
compat32        off
compat40        off
compat41        off
compat42        off
compat43        off
compat44        off
complete_fullquote      on
direxpand       off
dirspell        off
dotglob         off
execfail        off
expand_aliases  on
extdebug        off
extglob         on
extquote        on
failglob        off
force_fignore   on
globasciiranges on
globskipdots    on
globstar        off
gnu_errfmt      off
histappend      on
histreedit      off
histverify      off
hostcomplete    off
huponexit       off
inherit_errexit off
interactive_comments    on
lastpipe        off
lithist         off
localvar_inherit        off
localvar_unset  off
login_shell     off
mailwarn        off
no_empty_cmd_completion off
nocaseglob      off
nocasematch     off
noexpand_translation    off
nullglob        off
patsub_replacement      on
progcomp        on
progcomp_alias  off
promptvars      on
restricted_shell        off
shift_verbose   off
sourcepath      on
varredir_close  off
xpg_echo        off

Solution

  • #!/bin/env bash
    set -e
    for d in '/tmp/somedir/*'; do # `*` and `?` should not be quoted unless they exist in the filenames
        for f in "${d}/*pub"; do
            echo $f; # Here, the variable `f` is actually `/tmp/somedir/*/*pub`
                     # Without double quotes, Bash will expand the glob at this point
                     # So, you will see all matching files.
                     # With double quotes, you will see the original value of var `f`
        done
    done
    

    After correcting the script:

    #!/bin/env bash
    set -e
    for d in /tmp/somedir/*; do               
        for f in "${d}"/*pub; do # only quote the variable
            echo "$f"; # you will get each filename
        done
    done
    

    Normally, we should NOT quote * or ? in the loop. This way, Bash will expand them to match each filename, and the for loop will behave as expected.