I need to inspect all sub-directories and report how many files (without further recursion) they contain:
directoryName1 numberOfFiles directoryName2 numberOfFiles
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
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.
Method 2
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).
Method 3
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.
Method 4
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 $'tfoonbar' vs printf %q $'tfoonbar'.
The find command works by simply printing a single character for each file, and then counting those instead of counting lines.
Method 5
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}'
Method 6
for i in *; do echo $i; ls $i | wc -l; done
Method 7
for i in `ls -1`; do echo $i : `ls -1 $i|wc -l`; done
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0