32

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

directoryName1 numberOfFiles
directoryName2 numberOfFiles
3
  • 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, 2011 at 20:27
  • @janmoesen Why didn't you make that an answer? I'm new to shell scripting, but I can't see any gotchas with your method. To me, it looks like the best way. No one has upvoted your comment, but no one has commented on why it might be bad either. The upvoted answers have way more rep than you so it makes me wonder if I am missing something.
    – toxalot
    Mar 10, 2014 at 4:00
  • @toxalot: I didn't bother adding it as an answer because it was so short (and possibly slightly condescending in tone). Feel free to upvote the comment. :-) Also, the question is somewhat vague with regards to what "how many files" means. My solution counts "regular" files and directories; maybe the poster really meant "files, not directories". Another thing to keep in mind is that this globbing does not take "hidden" dot files into account. There are ways around both of those gotchas, though. But again: not sure of the original poster's exact requirements.
    – janmoesen
    May 23, 2014 at 20:56

8 Answers 8

35

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.

9
  • 1
    Why there is person in the world who uses newlines in file name? Thanks for the answer.
    – ShyBoy
    Oct 23, 2011 at 19:04
  • 1
    Filenames can contain any characters except / and the null character, I believe. dwheeler.com/essays/fixing-unix-linux-filenames.html
    – Flimm
    Oct 27, 2011 at 23:22
  • 2
    The count will include the directory itself. If you want to exclude that from the count, use -mindepth 1
    – toxalot
    Mar 10, 2014 at 4:20
  • You could also use -printf '\n' instead of -exec echo.
    – toxalot
    Mar 10, 2014 at 11:06
  • 2
    @toxalot you can if you have a find that supports -printf, but not if you want it to work on FreeBSD, for example. Mar 10, 2014 at 12:07
7

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

6

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.

5
  • I have more than 2 dirs... How can I combine your command with find . -maxdepth 1 -type d output?
    – ShyBoy
    Oct 23, 2011 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, 2011 at 7:29
  • 1
    This will report incorrect results if any filename has a newline character in it. Oct 23, 2011 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, 2011 at 11:37
  • Add -exec echo to your find command - that way it doesn't echo the filename, just a newline. Oct 23, 2011 at 11:43
2

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.

1
1

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}'
2
  • This work. But output messed if you have dirs that have ` ` space in name.
    – ShyBoy
    Oct 23, 2011 at 11:20
  • This will report incorrect results if any filename has a newline character in it. Oct 23, 2011 at 11:23
0

I used the high scoring answer here and it contains an out-by-one error . This may be due to a change in the behaviour of find(1) , which now at least includes the name of the directory being listed (so always has one line)

I created variants of the solution , JUST counting file (no directories) counting files and sub-directories and finally putting the count at the front to make it easy use sort (I wanted to find directories with just one file)

graeme@real:~/test$ mkdir -p emptydir 1filedir 1dirdir 1file+1dir_dir
graeme@real:~/test$ touch 1filedir/singlefile
graeme@real:~/test$ mkdir 1dirdir/singledir
graeme@real:~/test$ mkdir  1file+1dir_dir/a_dir
graeme@real:~/test$ touch 1file+1dir_dir/a_file
graeme@real:~/test$ bash count-files-orig
1dirdir 2
1file+1dir_dir  3
1filedir    2
emptydir    1
graeme@real:~/test$ bash count-files-just-files
1dirdir 0
1file+1dir_dir  1
1filedir    1
emptydir    0
graeme@real:~/test$ bash count-files-and-dirs
1dirdir 1
1file+1dir_dir  2
1filedir    1
emptydir    0
graeme@real:~/test$ cat count-files-orig
#! /bin/bash -ue
for f in *; do [ -d ./"$f" ] && find ./"$f" -maxdepth 1 -exec echo \;|wc -l|tr '\n' ' ' && echo $f; done|awk '{print $2"\t"$1}'
graeme@real:~/test$ cat count-files-just-files
#! /bin/bash -ue
for f in *; do [ -d ./"$f" ] && find ./"$f" -maxdepth 1 -type f -exec echo \;|wc -l|tr '\n' ' ' && echo $f; done|awk '{print $2"\t"$1}'
graeme@real:~/test$ cat count-files-and-dirs
#! /bin/bash -ue
for f in *; do [ -d ./"$f" ] && find ./"$f" -maxdepth 1  -path ./"$f/*" -exec echo \;|wc -l|tr '\n' ' ' && echo $f; done|awk '{print $2"\t"$1}'
graeme@real:~/test$ bash ./count-files | sort -nr
2   1file+1dir_dir
1   1filedir
1   1dirdir
0   emptydir
graeme@real:~/test$ cat count-files
#! /bin/bash -ue
for f in *; do [ -d ./"$f" ] && find ./"$f" -maxdepth 1  -path ./"$f/*" -exec echo \;|wc -l|tr '\n' ' ' && echo $f; done|awk '{print $1"\t"$2}'
graeme@real:~/test$ 
-1
for i in *; do echo $i; ls $i | wc -l; done
2
  • 4
    Welcome to U&L. Answers should be long form with explanations and not simply code drops. Please expand this and explain what's going on. Also this is a very inefficient way to do this and doesn't handle files with spaces, for example.
    – slm
    Dec 11, 2014 at 13:28
-2
    for i in `ls -1`; do echo $i : `ls -1 $i|wc -l`; done
2

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .