I need to inspect all sub-directories and report how many files (without further recursion) they contain:

directoryName1 numberOfFiles
directoryName2 numberOfFiles
link|improve this question
Why do you want to use find when Bash will do? (shopt -s dotglob; for dir in */; do all=("$dir"/*); echo "$dir: ${#all[@]}"; done): for all directories, count the number of entries in that directory (including hidden dot files, excluding . and ..) – janmoesen Oct 23 '11 at 20:27
feedback

5 Answers

up vote 6 down vote accepted

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

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.

link|improve this answer
Why there is person in the world who uses newlines in file name? Thanks for the answer. – ShyBoy Oct 23 '11 at 19:04
Filenames can contain any characters except / and the null character, I believe. dwheeler.com/essays/fixing-unix-linux-filenames.html – Flimm Oct 27 '11 at 23:22
feedback

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).

link|improve this answer
feedback

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.

link|improve this answer
I have more than 2 dirs... How can I combine your command with find . -maxdepth 1 -type d output? – ShyBoy Oct 23 '11 at 6:14
You could either (a) include the required directories in a variable and find $dirs ... or, (b) if they are exclusively in the one higher level directory, glob from that directory, find */ ... – jasonwryan Oct 23 '11 at 7:29
This will report incorrect results if any filename has a newline character in it. – Shawn J. Goff Oct 23 '11 at 11:23
@Shawn: thanks. I thought I had filenames with spaces covered, but hadn't considered new lines: any suggestions for a fix? – jasonwryan Oct 23 '11 at 11:37
Add -exec echo to your find command - that way it doesn't echo the filename, just a newline. – Shawn J. Goff Oct 23 '11 at 11:43
feedback

Based on a count script, Shawn's answer and a Bash trick to make sure even filenames with newlines are printed in a usable form on a single line:

for f in *
do
    if [ -d "./$f" ]
    then
        printf %q "$f"
        printf %s ' '
        find "$f" -maxdepth 1 -printf x | wc -c
    fi
done

printf %q is to print a quoted version of a string, that is, a single-line string which you could put into a Bash script to be interpreted as a literal string including (potentially) newlines and other special characters. For example, see echo -n $'\tfoo\nbar' vs printf %q $'\tfoo\nbar'.

The find command works by simply printing a single character for each file, and then counting those instead of counting lines.

link|improve this answer
feedback

Here's a "brute-force"-ish way to get your result, using find, echo, ls, wc, xargs and awk.

find . -maxdepth 1 -type d -exec sh -c "echo '{}'; ls -1 '{}' | wc -l" \; | xargs -n 2 | awk '{print $1" "$2}'
link|improve this answer
This work. But output messed if you have dirs that have ` ` space in name. – ShyBoy Oct 23 '11 at 11:20
This will report incorrect results if any filename has a newline character in it. – Shawn J. Goff Oct 23 '11 at 11:23
feedback

Your Answer

 
or
required, but never shown

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