Bash – Shell wildcards and dot files

bashshellwildcards

Why do the following two codes work as expected:

ls -d .[!.]?*
echo [D]*

But the following 2 don't:

ls -d [.][!.]?*
echo [D]

In the first command, I get an error:

ls: cannot access [.][!.]*: No such file or directory

When I am trying to get the same listing ls -d .[!.]?*. And for the second, the output is:

[D]

When I'm expecting an error proclamation along the lines of No such file or directory. What am I missing? What exactly makes an expression a wildcard, if those wildcard elements shown in the second set of examples above don't cut it?

Clarification (Also in comments):

The wildcard [D]* doesn't only output D, it also outputs Desktop, Downloads … etc. However, I also tried echo [D] when I had a file named D and when I didn't. The output worked when the file D was there, but I also got the output [D] when it wasn't. I don't get why. Why did the presence of the file in the directory change the expression [D] from being a wild card to not?

Best Answer

By default, bash (the shell) doesn't expand a filename beginning with a dot (considered a hidden file) unless the dot is explicitly specified. Since [.] is a pattern, rather than a literal character, it doesn't trigger the display of hidden files and so ls -d [.][!.]?* doesn't match anything.

You can change this default behavior by running shopt -s dotglob, which sets the option to always includes hidden files when expanding filename patterns (known as "globbing").

Example (the dollar sign at the beginning of the line indicates the shell prompt; don't type it in):

$ touch .foo
$ ls -d .[!.]?*
.foo
$ ls -d [.][!.]?*
ls: cannot access [.][!.]?*: No such file or directory
$ shopt -s dotglob
$ ls -d [.][!.]?*
.foo

The issue with echo [D] is that expansion doesn't happen if no filename is matched. Since you didn't have a file named D when you tried it, echo was given, and echoed, what you literally entered.

An example of pathname expansion only occurring when a filename matches:

$ touch apple
$ echo a*
apple
$ rm apple
$ echo a*
a*

If you delete all of the files which match [D]*, then echo [D]* will give you [D]*.

Related Question