Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

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

directoryName1 numberOfFiles
directoryName2 numberOfFiles
share|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
    
@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 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 at 20:56

6 Answers 6

up vote 10 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.

share|improve this answer
1  
Why there is person in the world who uses newlines in file name? Thanks for the answer. –  ShyBoy Oct 23 '11 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 '11 at 23:22
    
The count will include the directory itself. If you want to exclude that from the count, use -mindepth 1 –  toxalot Mar 10 at 4:20
    
You could also use -printf '\n' instead of -exec echo. –  toxalot Mar 10 at 11:06
    
@toxalot you can if you have a find that supports -printf, but not if you want it to work on FreeBSD, for example. –  Shawn J. Goff Mar 10 at 12:07

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.

share|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
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

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

share|improve this answer

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.

share|improve this answer

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}'
share|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
for i in *; do echo $i; ls $i | wc -l; done
share|improve this answer
3  
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 at 13:28

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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