The pattern matching behavior you describe is - according to this technet blog - a grandfathered bug:
...you'll also discover that *.* is the same as * by itself.
In addition ... if you typed DIR .TXT, the command prompt acted as if you had typed DIR *.TXT... This behavior was probably ... not intentional, but it was an accident that some people came to rely upon. When we fixed the bug in Windows 95, more than one person complained that their DIR .TXT command wasn't working.
The FCB matching algorithm was abandoned during the transition to Win32 since it didn't work with long file names. Long file names can contain multiple dots, and of course files can be longer than eleven characters, and there can be more than eight characters before the dot. But some quirks of the FCB matching algorithm persist into Win32 because they have become idiom.
For example, if your pattern ends in .*, the .* is ignored. Without this rule, the pattern *.* would match only files that contained a dot, which would break probably 90% of all the batch files on the planet, as well as everybody's muscle memory, since everybody running Windows NT 3.1 grew up in a world where *.* meant all files.
As another example, a pattern that ends in a dot doesn't actually match files which end in a dot; it matches files with no extension. And a question mark can match zero characters if it comes immediately before a dot.
This may not differ too terribly from the origin of the Unix precedent for dot files, as Rob Pike notes in his blog:
Long ago, as the design of the Unix file system was being worked out, the entries . and .. appeared, to make navigation easier. I'm not sure but I believe .. went in during the Version 2 rewrite, when the file system became hierarchical (it had a very different structure early on). When one typed ls, however, these files appeared, so either Ken or Dennis added a simple test to the program. It was in assembler then, but the code in question was equivalent to something like this:
if (name[0] == '.') continue;
This statement was a little shorter than what it should have been, which is
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
...but hey, it was easy...
I'm pretty sure the concept of a hidden file was an unintended consequence. It was certainly a mistake.
(For those who object that dot files serve a purpose, I don't dispute that but counter that it's the files that serve the purpose, not the convention for their names. They could just as easily be in $HOME/cfg or $HOME/lib, which is what we did in Plan 9, which had no dot files. Lessons can be learned.)
Since then, though, the . dot files came to be associated with the standard shell . utility, and from there just with user-specific plain-text configuration files.
You won't typically find dot files anywhere but a user's ~ because that is really the only place they would make sense - a user shouldn't be keeping any of their files anywhere else in the filesystem, anyway. When working on files in one's home directory it would be unfortunate if a user were to accidentally remove some or all of their configuration files - but it would be even more unfortunate if - when in ~ or any other directory - a loop were to unintentionally affect . - the current directory - or (worse still) .. - the parent directory. POSIX has this to say about .:
If a filename begins with a period ., the period shall be explicitly matched by using a period as the first character of the pattern or immediately following a /slash character. The leading period shall not be matched by:
The *asterisk or ?question-mark special characters
A [bracket] expression containing a non-matching list, such as [!a], a range expression, such as "[%-0]", or a character class expression, such as [[:punct:]]
- It is unspecified whether an explicit period in a bracket expression matching list, such as
[.abc], can match a leading period in a filename.
Still, though, it is possible to loop over all files in the current directory - dot files or not - in a single loop simply enough. For example:
set -- .* *
while until [ -f "$1" ] ||
[ -z "$1" ] &&
break "$((!$#+1))"
do shift; done
do printf %s\\n "$1"
shift
done
The above prints all regular files in the current directory in dash, bash, ksh, zsh, mksh, posh, and yash. It is even more akin to the Windows FOR loop than is the typical shell for in that this doesn't set any permanent shell variables - just like a cmd FOR it processes its arguments and when it is through with them their names no longer hold any value. In a typical shell for the last value in $var in for var in ... remains.
It doesn't have to be like that:
set -- .* * ''
while [ -n "$1" ] || ! shift
do [ -f "$1" ] &&
set -- "$@" "$1"
shift
done
printf %s\\n "$@"
...accomplishes the same goal just as portably - printing all regular files in the current directory - but it loops over the arguments once first and prunes those which are not regular files. The advantage, though, is that afterward the shell's arg "$@" array still holds the whole list, and from that point on you can do:
for f do : something w/ "$f"; done
...as many times as you please on the same array. But you also get the count in "$#" and all args concatenated into a single string in "$*" and each addressable singly in sorted order in "$1" ... "${10}" and so on.
Those are typically not the kind of things you can do w/ Win32 cmd.