Assuming that your bash
version is at least 4.0, actually you were almost there.
You can allow your code to count files recursively with the globstar
shell option. From man bash(1)
:
If set, the pattern **
used in a pathname expansion context will match all files and zero or more directories and subdirectories. If the pattern is followed by a /
, only directories and subdirectories match.
If you want to recursively count all files, including subdirectories, that are in your top-level directories:
shopt -s dotglob globstar
for dir in */; do
all=( "$dir"/** )
printf '%s\n' "$dir: ${#all[@]}"
done
As in the code you tried, for each of your top-level directory we are populating an array with the results of pathname expansion and then displaying the number of its elements.
dotglob
is used to include files whose names start with .
(hidden files).
If you want to recursively count all files except for subdirectory objects, you can just subtract the count of subdirectories from the count of all files:
shopt -s dotglob globstar
for dir in */; do
all=( "$dir"/** )
alldir=( "$dir"/**/ )
printf '%s\n' "$dir: $(( ${#all[@]} - ${#alldir[@]} ))"
done
However, here I'm assuming a broad definition of "file", which, in POSIX, may refer to a regular file, character, block or FIFO special file, symbolic link, socket, directory, or whatever specific implementations may add beyond the standard.
To count a specific type of files only (e.g. regular files), it may be easier to resort to a find
-based solution.
Alternatively you can extend the above code, testing for the file type in a loop:
shopt -s dotglob globstar
for dir in */; do
all=( "$dir"/** )
count=0
for file in "${all[@]}"; do
test -f "$file" && count="$(( "$count" + 1 ))"
done
printf '%s\n' "$dir: $count"
done
But this less convenient solution will also be significantly slower than the find
-based alternative (e.g. more than two times slower than the faster one in Kusalananda's answer, tested on Linux with bash
5.0 and find
4.6).
Also note that, unlike find
in its default behavior, pathname expansion with the globstar
option will follow symbolic links that resolve to files, making all the above snippets include them in the counts as well.
(Initially it used to follow symbolic links that resolve to directories too, but this behavior has been changed in bash
4.3).
Finally — to also provide a solution that does not depend on the globstar
shell option — you can use a recursive function to recursively count all regular files in the top-level subdirectories of the $1
directory:
#!/bin/bash
# nullglob is needed to avoid the function being
# invoked on 'dir/*' when * matches nothing
shopt -s nullglob dotglob
function count_files () {
for file in "$1"/*; do
# Only count regular files
[ -f "$file" ] && count="$(( "$count" + 1 ))"
# Only recurse on directories
[ -d "$file" ] && count_files "$file"
done
}
for dir in "$1"/*/; do
count="0"
count_files "$dir"
printf '%s: %s\n' "$dir" "$count"
done
sub1
,sub1/subsub1
,sub2
,sub3
,sub3/subsub3
, andsubsubsub3
? Or just forsub1
,sub2
, andsub3
? If this second option, shouldsub1
andsub3
count files in their subdirectories too? – Chris Davies Feb 13 '19 at 22:48