32

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

directoryName1 numberOfFiles
directoryName2 numberOfFiles
jasonwryan
  • 73,126
ShyBoy
  • 635
  • 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
  • @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 '14 at 04: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 '14 at 20:56

8 Answers8

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.

Shawn J. Goff
  • 46,081
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.

jasonwryan
  • 73,126
  • I have more than 2 dirs... How can I combine your command with find . -maxdepth 1 -type d output? – ShyBoy Oct 23 '11 at 06: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 07:29
  • 1
    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
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.

l0b0
  • 51,350
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}'
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$ 
GraemeV
  • 148
-1
for i in *; do echo $i; ls $i | wc -l; done
Dinesh
  • 1
  • 1