33

I would like a BASH command to list just the count of files in each subdirectory of a directory.

E.g. in directory /tmp there are dir1, dir2, ... I'd like to see :

`dir1` : x files 
`dir2` : x files ...
jldupont
  • 6,604
  • 14
  • 38
  • 47

9 Answers9

46

Assuming you want a recursive count of files only, not directories and other types, something like this should work:

find . -maxdepth 1 -mindepth 1 -type d | while read dir; do
  printf "%-25.25s : " "$dir"
  find "$dir" -type f | wc -l
done
Thor
  • 6,419
  • 1
  • 36
  • 42
  • Also, I get "find: warning: you have specified the -maxdepth option after a non-option argument -type, but options are not positional (-maxdepth affects tests specified before it as well as those specified after it). Please specify options before other arguments." – jldupont Sep 14 '12 at 21:38
  • 2
    Both answers given so far will give incorrect results in the unlikely case that there are files whose names include newline characters.  You can handle that with a `find` ... `-print0 | xargs -0` .... – Scott - Слава Україні Sep 14 '12 at 21:57
  • @jldupont: move the depth arguments before the ´-type d´, I've edited the answer. – Thor Sep 14 '12 at 22:58
  • Yes, and let me add the information that this excellent solution will not take any *external* variables and thus will work with `bash` `alias`!! – syntaxerror Nov 25 '14 at 13:07
  • fast, and piping `sort -rn -k 2,2 -t$':'` you get the DESC list – Andre Figueiredo Apr 18 '18 at 23:05
  • 1
    Any time you want to use `read foo`, consider [`IFS= read -r foo`](https://unix.stackexchange.com/q/209123/108618). In many cases the latter is what you really want. This is such case. The improvement does not solve the (already noted) problem with possible newline characters in pathnames. – Kamil Maciorowski Dec 02 '21 at 08:59
20

This task fascinated me so much that I wanted to figure out a solution myself. It doesn't even take a while loop and MAY be faster in execution speed. Needless to say, Thor's efforts helped me a lot to understand things in detail.

So here's mine:

find . -maxdepth 1 -mindepth 1 -type d -exec sh -c 'echo "{} : $(find "{}" -type f | wc -l)" file\(s\)' \;

It looks modest for a reason, for it's way more powerful than it looks. :-)

However, should you intend to include this into your .bash_aliases file, it must look like this:

alias somealias='find . -maxdepth 1 -mindepth 1 -type d -exec sh -c '\''echo "{} : $(find "{}" -type f | wc -l)" file\(s\)'\'' \;'

Note the very tricky handling of nested single quotes. And no, it is not possible to use double quotes for the sh -c argument.

syntaxerror
  • 603
  • 6
  • 13
  • 1
    It is slower as it invokes /bin/sh for each directory. You can check this with `strace -fc script`. Your version makes about 70% more system-calls. +1 for shorter code :-) – Thor Oct 31 '16 at 13:58
  • 1
    inspired by this; sorted by filecount: `find . -maxdepth 1 -mindepth 1 -type d -exec sh -c 'echo "$(find "{}" -type f | wc -l)" {}' \; | sort -nr` – mnagel Mar 21 '17 at 08:44
  • [Never embed `{}` in the shell code](https://unix.stackexchange.com/a/156010/108618). – Kamil Maciorowski Dec 02 '21 at 08:47
14
find . -type f | cut -d"/" -f2 | uniq -c

Lists a folders and files in the current folder with a count of files found beneath. Quick and useful IMO. (files show with count 1).

Thor
  • 6,419
  • 1
  • 36
  • 42
nothx
  • 141
  • 1
  • 2
2

Using find is definitely the way to go if you want to count recursively, but if you just want a count of the files directly under a certain directory:

ls dir1 | wc -l

jonvuri
  • 131
  • 2
  • 1
    I don't want to do this for each of the 1000's of directories I've got there... – jldupont Sep 14 '12 at 21:37
  • Then use xargs. `ls -d */ | xargs -n1 ls | wc -l` (Use the answer you accepted if it already works, though! This is just And Now You Know.) – jonvuri Sep 14 '12 at 21:41
  • your proposal didn't show up any results in many seconds whereas the answer I accepted did. – jldupont Sep 15 '12 at 08:03
  • @jrajav this approach absolutely fails for directories with whitespace in them. This is why `find` is so important. (let alone `-print0` and `xargs -0`, already pointed out by Scott in the other answer) – syntaxerror Nov 25 '14 at 13:03
1
find . -mindepth 1 -type d -print0 | xargs -0 -I{} sh -c 'printf "%4d : %s\n" "$(find {} -type f | wc -l)" "{}"'

I need often need to count the number of files in my sub-directories and use this command. I prefer the count to appear first.

0

You could use this python code. Boot up the interpreter by running python3 and paste this:

folder_path = '.'
import os, glob
for folder in sorted(glob.glob('{}/*'.format(folder_path))):
    print('{:}: {:>8,}'.format(os.path.split(folder)[-1], len(glob.glob('{}/*'.format(folder)))))

Or a recursive version for nested counts:

import os, glob
def nested_count(folder_path, level=0):
    for folder in sorted(glob.glob('{}/'.format(os.path.join(folder_path, '*')))):
        print('{:}{:}: {:,}'.format('    '*level, os.path.split(os.path.split(folder)[-2])[-1], len(glob.glob(os.path.join(folder, '*')))))
        nested_count(folder, level+1)
nested_count('.')

Example output:

>>> figures: 5
>>> misc: 1
>>> notebooks: 5
>>>     archive: 65
>>>     html: 12
>>>     py: 12
>>>     src: 14
>>> reports: 1
>>>     content: 6
>>> src: 1
>>>     html_download: 1
Alex
  • 531
  • 4
  • 5
0

For reliable methods of counting files in a directory, see this answer of mine: What is a reliable code to count files?

In your case we additionally need to iterate over subdirectories. A for loop is good for this:

for d in */; do
    c="$(cd -- "$d" && find . ! -name . -exec printf a \; | wc -c)"
    if [ "$?" -ne 0 ]; then c="?"; fi
    printf '%-25s : %s\n' "${d%/}" "$c"
done

Notes:

  • Normally */ does not match directories with names starting with a dot ("hidden" directories). In Bash shopt -s dotglob changes this.

  • ! -name . is responsible for not counting the respective subdirectory itself.

  • ${d%/} removes trailing /. If we used * instead of */ in the for loop, then there would be nothing to remove, but the loop would iterate over non-directories as well.

  • Double dash (--) is useful in case a name starts with -. If we used ./*/ in the for loop, then there would be no need for --, but you probably would want to remove the leading ./ while printing the output, this would complicate the code.

  • In the output ? appears in case of a problem. Failing cd (probably because of insufficient permissions) is the problem I had in mind. find unable to descend to some (sub-…)subdirectory does not qualify as a problem in this context. If you see a number then it means find found as many files there, so there are at least as many files in the directory. If you see ? then it means find most likely didn't run because cd had failed.

  • See the already linked answer, it will help you tailor the find command to your needs (e.g. non-recursive solution, counting files of a specific type, some optimizations).

  • Multi-byte characters in names will confuse printf and the output may appear misaligned.

  • The format that prints names is %-25s, so any name is printed as-is (plus padding). Newline characters, carriage return characters, escape sequences in names may cause results you don't expect. With printf builtin in Bash use %q instead of %s (it will be %-25q in our case) to mitigate the problem.

Kamil Maciorowski
  • 69,815
  • 22
  • 136
  • 202
-1

Output as csv format for Folders :

for f in $(find * -type d); do echo $f,$(ls ./$f | wc -l) ; done

output:

aFolder,30

bFolder,20

cFolder,10
DarkDiamond
  • 1,875
  • 11
  • 12
  • 19
Ronen Tal
  • 1
  • 1
  • Why did you post an answer very similar to [your other answer](https://superuser.com/a/1690776/432690) and with *the same set of flaws*? – Kamil Maciorowski Dec 03 '21 at 12:56
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Dec 03 '21 at 13:24
-1

What I use... This makes an array of all the subdirectories in the one you give as a parameter. Print the subdirectory and the count of that same subdirectory until all the subdirectories are processed.

#!/bin/bash    
directories=($(/bin/ls -l $1 | /bin/grep "^d" | /usr/bin/awk -F" " '{print $9}'))

for item in ${directories[*]}
    do
        if [ -d "$1$item" ]; then
            echo "$1$item"
            /bin/ls $1$item | /usr/bin/wc -l
        fi
    done
karel
  • 13,390
  • 26
  • 45
  • 52
  • -1. [Do not parse the output of `ls`](https://mywiki.wooledge.org/ParsingLs). Your code still falls into the [Bash pitfall number one](https://mywiki.wooledge.org/BashPitfalls#for_f_in_.24.28ls_.2A.mp3.29); the fact you used an array only obfuscates the problem, bad things may happen when you define the array. Worse, *additional* bad things may happen when you use `${directories[*]}` (it should be `"${directories[@]}"`). – Kamil Maciorowski Dec 02 '21 at 08:44