I need to inspect all sub-directories and report how many files (without further recursion) they contain:
directoryName1 numberOfFiles
directoryName2 numberOfFiles
I need to inspect all sub-directories and report how many files (without further recursion) they contain:
directoryName1 numberOfFiles
directoryName2 numberOfFiles
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.
-mindepth 1
– toxalot
Mar 10 '14 at 04:20
-printf
, but not if you want it to work on FreeBSD, for example.
– Shawn J. Goff
Mar 10 '14 at 12:07
for f in .* *
if you want to include hidden folders as well.
– DanielSmedegaardBuus
Feb 01 '18 at 07:32
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
).
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.
find . -maxdepth 1 -type d
output?
– ShyBoy
Oct 23 '11 at 06:14
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
-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
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.
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}'
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$
for i in *; do echo $i; ls $i | wc -l; done
for i in `ls -1`; do echo $i : `ls -1 $i|wc -l`; done
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