0

I want to know the number of items (files, sub-directories etc) inside a particular directory.

There seems to be a lot of questions about it but most, if not all, answers seems to rely on wc -l. For instance:

ls -1 | wc -l

But that will give the wrong answer if one or more files have new lines in their names. Another problem here is that ls -1 might return more than just files.

A non portable answer was hidden inside the comments of one of the thousands similar questions. Works, but unfortunately it relies on -printf which is not available in my distro.

find -maxdepth 1 -type f -printf "\n" | wc -l (by godlygeek)

Is there a more portable solution that can correctly handle files containing any char in their names?

user1593842
  • 135
  • 4
  • What OS flavor and version are you using. What shell are you using (it would seem to be POSIX shell, but can you specify)? – AdminBee Oct 19 '20 at 13:38
  • It seems I made a mistake while setting the tags. It was supposed to be sh, which would tell you all you needed to know (I think) :) And the system is Alpine Linux v3.12 – user1593842 Oct 19 '20 at 13:44

4 Answers4

6

Assuming you don’t need your positional parameters, you can set them and count them:

set -- .* *
echo $#

This can be in a function, or in a subshell, to limit the impact of the set:

countfiles() {
    set -- .* *
    echo $#
}

countfiles

or

(set -- .* *; echo $#)

Only the function’s or subshell’s parameters are “lost” here, the positional parameters in the main context are untouched.

You can specify a path too:

set -- path/to/dir/.* path/to/dir/*
echo $#

The exact expansion of hidden files will depend on shell settings; POSIX shells should include . and .. when expanding .*, so these will be counted. If you don’t want to count them, check that they are included (look at $1 and $2), and subtract two from the result.

In some shells, dot expansion can be configured, and/or other expansions (.[^.]* ..?*) can be used to exclude . and ..; thus in Bash, using dotglob:

shopt -s dotglob
set -- *
echo $#
Stephen Kitt
  • 434,908
  • Awesome answer. Too bad it kills the positional parameters. That, of course, could be remedied. But I decided to pick AdminBee's because his not only keeps the parameters intact, but it is also the closest of what a plain C program would look like. Although performance is not really important in my case, I assume the performance of your script and his should be just about the same. But I'm willing to change my answer in case yours performs much better in some particular case. – user1593842 Oct 20 '20 at 11:43
  • 1
    The positional parameters are only killed in the immediate context of the set, so if you put this in a function it will only kill the function’s parameters. As far as performance goes, for loops in shells are slow, so for large number of files $# is quite a bit faster than looping. – Stephen Kitt Oct 20 '20 at 12:02
  • If you're setting nullglob, you may just as well set dotglob (* does not match . or .. in bash with dotglob set). Also, in the POSIX case, you may want to test (with the -e test) whether the names that you glob exists or not. – Kusalananda Oct 20 '20 at 12:41
  • @Stephen Kitt you seem to be absolutely right about the performance issue. Although it takes a somewhat extreme case for the difference to be really visible, your answer seems to be twice as fast as the one I accepted. I tested by creating 30.000 files on tmpfs and then used time to time both yours and AdminBee's. I also timed roaima's, but it seems that using find like that is painfully slow. Thousands of times slower than any other solution. And I am not exaggerating. Shame on you, roaima :) – user1593842 Oct 20 '20 at 20:27
1

The following is probably not very elegant, but should be portable. It relies on shell arithmetic:

n=0; for f in *; do n=$((n+1)); done; echo "$n"

Note that this will not count hidden files. If you need them too, you would have to extend the iteration list:

n=0; for f in * .*; do n=$((n+1)); done; echo "$n"

Depending on your shell, the n=$((n+1)) can be golfed into ((n++)).

Note that the explicit shell loop makes this approach rather slow, which can become noticable if you are dealing with a directory containing several thousand files. The answer by Stephen Kitt is substantially faster, although a little obscure if you are new to shell scripting.

AdminBee
  • 22,803
  • @roaima You are right; I have included your suggestion. – AdminBee Oct 19 '20 at 14:10
  • I don't know about being elegant or not, but I really liked this one. And I don't feel bad about looping because I was about to give up and create simple C application that would have done basically the same :) And other than, maybe, Stephen Kitt's, it is very easy to understand what is going on here. One thing though - I had to change it to n=0; for f in * .*; do n=$((n+1)); done; echo "$n" otherwhise it would not work on mine plain /bin/sh – user1593842 Oct 20 '20 at 11:36
  • @user1593842 Funny, it originally had this explicit math expression, but changed it upon a comment by roaima :) – AdminBee Oct 20 '20 at 12:04
  • I knew roaima was up to no good :) By the way, as you can see in the comments of Stephen Kitt's answer, according to my tests, his answer performs way better than yours. I won't change the accepted answer, since I still think yours is, IMO, more readable. Also, it takes a directory with thousands of files before any performance difference become apparent and, even then, it is only a matter of milliseconds. I just thought I'd give others a heads up that there is a more performant answer out there, in case it might matter to them. – user1593842 Oct 20 '20 at 20:33
  • @user1593842 I wouldn't mind if you accepted Stephen Kitt's answer instead of mine; it is more efficient after all. – AdminBee Oct 21 '20 at 07:09
1

If you want POSIX compliance (which also means excluding bash and its shopt -s dotglob), one way of counting the files in a directory is this somewhat slow approach

find . -path './?*' ! -type f -prune -o -type f -exec echo x \; | wc -l

If you don't care what you've got you can simplify this considerably

find . -path './?*' -prune -exec echo x \; | wc -l

Actually, building on another answer, this will give you a count of all the non-directories (probably just files, but would include devices and pipes if any were present) in the current directory

( set -- * .* ; all=$#; set -- */ .*/ ; echo $(( all - $# -2 )) )
Chris Davies
  • 116,213
  • 16
  • 160
  • 287
  • 1
    Loved your idea. When I first asked this question on the wrong stack site and it was closed, we had a working answer using find. Only it was using -printf to achieve the same thing you did with -exec echo x, but -printf was not available on my platform and I was struggling to find an alternative. Little did a now that just a small change would have saved us a lot of time. Thanks! – user1593842 Oct 20 '20 at 11:48
0

Try find'ing the inode numbers instead... which does not involve the filename.

$ find . -type f -printf '%i\n' 

But seeing your find doesnt have the printf

$ find . -type f -name 'test*' -exec stat -c "%i" {} \; 

will return just the inodes

$ find . -type f -name 'test*' -exec stat -c "%i" {} \; | wc -l

will give you the count you need.

Hope this helps.

Chai Ang
  • 148
  • well, now that roaima (and others) made me realize I could have just called -exec something for each item returned by find, it all seems so simple :) With that in mind, although your solution certainly works and, up until a few hours ago would have saved me a lot of trouble, now your ends up being simply a variation of roaima. In fact, yours is a little bit worse (I think), since it performs something much more complex than his. But thanks!!! – user1593842 Oct 20 '20 at 19:38
  • My thinking was more guided by using the inode cos that doesnt have the filename. But seeing you dont have the printf, I just kept going along that direction :-) – Chai Ang Oct 20 '20 at 22:48