Unexpected behaviour with echo [[:digit:]]

Because they are two different things. The {1,2,3} is an example of brace expansion. The {1,2,3} construct is expanded by the shell, before echo even sees it. You can see what happens if you use set -x:

$ set -x
$ echo {1,2,3}
+ echo 1 2 3
1 2 3

As you can see, the command echo {1,2,3} is expanded to:

echo 1 2 3

However, [[:digit:]] is a POSIX character class. When you give it to echo, the shell also processes it first, but this time it is being processed as a shell glob. it works the same way as if you run echo * which will print all files in the current directory. But [[:digit:]] is a shell glob that will match any digit. Now, in bash, if a shell glob doesn't match anything, it will be expanded to itself:

$ echo /this*matches*no*files
+ echo '/this*matches*no*files'

If the glob does match something, that will be printed:

$ echo /e*c
+ echo /etc

In both cases, echo just prints whatever the shell tells it to print, but in the second case, since the glob matches something (/etc) it is told to print that something.

So, since you don't have any files or directories whose name consists of exactly one digit (which is what [[:digit:]] would match), the glob is expanded to itself and you get:

$ echo [[:digit:]]

Now, try creating a file called 5 and running the same command:

$ echo [[:digit:]]

And if there are more than one matching files:

$ touch 1 5       
$ echo [[:digit:]]
1 5

This is (sort of) documented in man bash in the explanation of the nullglob options which turns this behavior off:

    If  set,  bash allows patterns which match no files (see
    Pathname Expansion above) to expand to  a  null  string,
    rather than themselves.

If you set this option:

$ rm 1 5
$ shopt -s nullglob
$ echo [[:digit:]]  ## prints nothing


{1,2,3} is brace expansion, it expands to the words listed without regard to their meaning.

[...] is a character group, used in filename expansion (or wildcard, or glob) similarly to the asterisk * and question mark ?. It matches any single character listed within, or characters that are members of named groups such as [:digit:] if those are listed. The default behaviour of most shells is to leave the wildcard as-is if there are no files that match it.

(Note that you can't really turn a wildcard/pattern into the set of strings it would match. The asterisk can match any string of any length, so expanding any pattern containing it would produce an infinite list of strings. )


$ bash -c 'echo [[:digit:]]'           # bash leaves it as-is
$ zsh -c 'echo [[:digit:]]'            # zsh by default complains if no match
zsh:1: no matches found: [[:digit:]]
$ touch 1 3 d i g t
$ bash -c 'echo [[:digit:]]'           # now there are two matches
1 3                                    # note that d, i, g and t do NOT match

But still:

$ bash -c 'echo {1,2,3}'
1 2 3

Both of those are expanded by the shell, it doesn't matter if the command you're running is ls, or echo or rm. Also note that if either of those is quoted, they will not be expanded:

$ bash -c 'echo "[[:digit:]]"'         # even though matching files still exist
$ bash -c 'echo "{1,2,3}"'

{1,2,3} (and e. g. {1..3} are brace expansions. They are interpreted by the shell before command execution.

[[:digit:]] is a pattern matching token, but you're not using it in a location with any files which match that pattern. If you use a pattern match which has no matches, it expands to itself:

$ echo [[:digit:]]; touch 3; echo [[:digit:]]