3

I have 35 files and directory in my home at the first level (without descending into subdirectories). But the results are "faked" and report 36 because they include the . special directory. How can I exclude the "."?

I have tried the switch -not -path but it doesn't work.

find . -not -path '*/\.*' -not -path /\. -maxdepth 1|wc -l
36
find $HOME -not -path '*/\.*'  -maxdepth 1|wc -l
36
8

find includes the directories from which it starts; exclude those from the output, without decoration, to get the result you’re after:

find . -maxdepth 1 ! -path . | wc -l
find "$HOME" -maxdepth 1 ! -path "$HOME" | wc -l

(as long as $HOME doesn’t contain any characters which would be interpreted as pattern characters, i.e. *, ?, []). You can also match against the . name only, by appending it (if necessary) to the path you’re interested in:

find . -maxdepth 1 ! -name . | wc -l
find "$HOME"/. -maxdepth 1 ! -name . | wc -l

To accurately count the files found by find, you should count something other than the file names (which can include newlines), e.g. if your find supports -printf:

find . -maxdepth 1 ! -name . -printf 1 | wc -c

or, with POSIX find, by using the // trick:

find .//. -maxdepth 1 ! -name . | grep -c //

.. isn’t a problem with find, it doesn’t include it unless you add it explicitly (find .. ...).

ls won’t show . and .. by default; if you want to see hidden files, but not . and .., use the -A option instead of -a. With GNU ls, you can ask it to quote special characters, which will allow you to accurately count files:

ls -q1A . | wc -l
5
  • 1
    Seems like GNU ls performs quoting by default, so ls -1A | wc -l should be fine. It also supports -b which makes it use C-style quoting which, in my tests, also works just fine for CR and LFs. – kostix Nov 30 '20 at 17:59
  • 1
    @kostix it does so when writing to a terminal, not when piping to wc -l; but ls -1Aq | wc -l works. – Stephen Kitt Nov 30 '20 at 22:00
  • -path $HOME is not guaranteed to work in the general case (and not only because you forgot to quote $HOME), as the argument of -path is treated as a pattern. Better to use find ~/. ! -name .. -not is a GNU extension btw. To count files POSIXly, you can use the find .//. ... | LC_ALL=C grep -c // trick. – Stéphane Chazelas Dec 1 '20 at 9:21
  • You can also exclude the root paths with -mindepth 1 – Tavian Barnes Dec 1 '20 at 18:10
  • @Tavian yes, that’s umläute’s answer. – Stephen Kitt Dec 1 '20 at 18:15
11

use -mindepth:

find . -mindepth 1 -maxdepth 1    |wc -l
0
5

Use just ! -path .. The pathnames that find carries out its tests on are relative to the search path that you use, and the . directory will show up as just . to find in your example command. Had you used any other search path, you would have used that in the test to avoid listing it (e.g. find "$HOME" ! -path "$HOME").

Note too that by using wc -l, you are not actually counting files but the number of lines produced by find, which may be more than the number of files if any name contains newlines.

With a POSIX find, you could use

find . ! -path . -prune -exec echo x \; | wc -l 

to count the number of names in the current directory. Here, -prune would stop find from entering any subdirectories and -exec echo x \; would output an x character on a line for each found name. These lines would then be counted by wc -l.

As for not showing . or .. with ls, use ls -A instead of ls -a.


Another way to count the names in a single directory, which allows for all possible filenames, would be

shopt -s dotglob nullglob
set -- *
echo "$#"

... in bash, or

set -- *(DN)
echo $#

... in zsh (where (DN) modifies the behavior of * so that it also matches hidden names and so that the pattern is completely removed if there are no matches, much like the effect of enabling the dotglob and nullglob shell options in bash).

Both of these pieces of code sets the positional parameters to the list of names in the current directory, including hidden names. Once that has been done, the number of set positional parameters are found in the special variable $#.

The * globbing pattern does not match . nor ... Not even when asked to match hidden names.

In the yash shell, setting the dot-glob shell option with set -o dot-glob makes * match hidden names, including both . and .., so you may want to deduct 2 from the value of $# if you're using this shell.

2
  • 1
    Or (){echo $#} *(ND) in zsh or count * .* in fish. – Stéphane Chazelas Dec 1 '20 at 9:15
  • Note that -s and _s are ignored in yash options, so you can also use set -o dotglob there like in most other shells. . and .. are not guaranteed to be always returned by readdir() so always deducting 2 may not be valid (consider also the case of directories that can't be read). – Stéphane Chazelas Dec 1 '20 at 9:17
4

POSIXly:

find . ! -name . -prune | LC_ALL=C grep -c /

Using wc -l is invalid as it counts newline characters which would include the ones printed by find to separate file names, but also the ones in the file names themselves (as file names can contain any byte value except 0 and that representing /).

On the other hand, there should be one and only one / per filename in that output of find.

grep being a text utility, and filenames being any arbitrary sequence of bytes, we do need LC_ALL=C to make sure any sequence of bytes form valid characters to guarantee a deterministic behaviour from grep (filenames are also generally guaranteed to be smaller than the maximum line length supported by grep implementations).

Another option would be:

LC_ALL=C ls -Aq | wc -l

though -A to include hidden files but not . nor .. is a relatively recent addition to the POSIX standard. Also note that busybox ls currently doesn't support -q (needed here for newline characters in file names to be rendered as ? so they're not counted by wc -l).

For any arbitrary directory whose path is stored in a variable, you'd run (CDPATH= cd -P -- "$dir" && find/ls command above) | ..., though note that it wouldn't work properly for a file called - (or, depending on the cd implementation things like -1, +2...), which you'd need to address by prepending a ./ to those.

3

Simplest. Works. Maybe not with embedded newlines in file names, but since that is such horribad standards, I would say rename those files.

ls -d * | wc -l

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.