How to report number of files in all subdirectories?

This does it in a safe and portable way. It won't get confused by strange filenames.

for f in *; do [ -d ./"$f" ] && find ./"$f" -maxdepth 1 -exec echo \; | wc -l && echo $f; done

Note that it will print the number of files first, then the directory name on a separate line. If you wish to keep OP's format you will need further formatting, e.g.

for f in *; do [ -d ./"$f" ] && find ./"$f" -maxdepth 1 -exec echo \;|wc -l|tr '\n' ' ' && echo $f; done|awk '{print $2"\t"$1}'

If you have a specific set of subdirectories you're interested in, you can replace the * with them.

Why is this safe? (and therefore script-worthy)

Filenames can contain any character except /. There are a few characters that are treated specially either by the shell or by the commands. Those include spaces, newlines, and dashes.

Using the for f in * construct is a safe way of getting each filename, no matter what it contains.

Once you have the filename in a variable, you still have to avoid things like find $f. If $f contained the filename -test, find would complain about the option you just gave it. The way to avoid that is by using ./ in front of the name; this way it has the same meaning, but it no longer starts with a dash.

Newlines and spaces are also a problem. If $f contained "hello, buddy" as a filename, find ./$f, is find ./hello, buddy. You're telling find to look at ./hello, and buddy. If those don't exist, it will complain, and it will never look in ./hello, buddy. This is easy to avoid - use quotes around your variables.

Finally, filenames can contain newlines, so counting newlines in a list of filenames will not work; you'll get an extra count for every filename with a newline. To avoid this, don't count newlines in a list of files; instead, count newlines (or any other character) that represent a single file. This is why the find command has simply -exec echo \; and not -exec echo {} \;. I only want to print a single new line for the purpose of tallying the files.


Assuming that you are looking for a standard Linux solution, a relatively straightforward way to achieve this is with find:

find dir1/ dir2/ -maxdepth 1 -type f | wc -l

Where find traverses the two specified subdirectories, to a -maxdepth of 1 which prevents further recursion and only reports files (-type f) separated by newlines. The result is then piped to wc to count the number of those lines.


By “without recursion”, do you mean that if directoryName1 has subdirectories, then you don't want to count the files in the subdirectories? If so, here's a way to count all the regular files in the indicated directories:

count=0
for d in directoryName1 directoryName2; do
  for f in "$d"/* "$d"/.[!.]* "$d"/..?*; do
    if [ -f "$f" ]; then count=$((count+1)); fi
  done
done

Note that the -f test performs two functions: it tests whether the entry matched by one of the globs above is a regular file, and it tests whether the entry was a match (if one of the globs matches nothing, the pattern remains as is¹). If you want to count all entries in the given directories regardless of their type, replace -f with -e.

Ksh has a way to make patterns match dot files and to produce an empty list in case no file matches a pattern. So in ksh you can count regular files like this:

FIGNORE='.?(.)'
count=0
for x in ~(N)directoryName1/* ~(N)directoryName2/*; do
  if [ -f "$x" ]; then ((++count)); fi
done

or all files simply like this:

FIGNORE='.?(.)'
files=(~(N)directoryName1/* ~(N)directoryName2/*)
count=${#files}

Bash has different ways to make this simpler. To count regular files:

shopt -s dotglob nullglob
count=0
for x in directoryName1/* directoryName2/*; do
  if [ -f "$x" ]; then ((++count)); fi
done

To count all files:

shopt -s dotglob nullglob
files=(directoryName1/* directoryName2/*)
count=${#files}

As usual, it's even simpler in zsh. To count regular files:

files=({directoryName1,directoryName2}/*(DN.))
count=$#files

Change (DN.) to (DN) to count all files.

¹ Note that each pattern matches itself, otherwise the results might be off (e.g. if you're counting files that start with a digit, you can't just do for x in [0-9]*; do if [ -f "$x" ]; then … because there might be a file called [0-9]foo).

Tags:

Find

Ls