Is there a bash command which counts files?

Lots of answers here, but some don't take into account

  • file names with spaces, newlines, or control characters in them
  • file names that start with hyphens (imagine a file called -l)
  • hidden files, that start with a dot (if the glob was *.log instead of log*
  • directories that match the glob (e.g. a directory called logs that matches log*)
  • empty directories (i.e. the result is 0)
  • extremely large directories (listing them all could exhaust memory)

Here's a solution that handles all of them:

ls 2>/dev/null -Ubad1 -- log* | wc -l

Explanation:

  • -U causes ls to not sort the entries, meaning it doesn't need to load the entire directory listing in memory
  • -b prints C-style escapes for nongraphic characters, crucially causing newlines to be printed as \n.
  • -a prints out all files, even hidden files (not strictly needed when the glob log* implies no hidden files)
  • -d prints out directories without attempting to list the contents of the directory, which is what ls normally would do
  • -1 makes sure that it's on one column (ls does this automatically when writing to a pipe, so it's not strictly necessary)
  • 2>/dev/null redirects stderr so that if there are 0 log files, ignore the error message. (Note that shopt -s nullglob would cause ls to list the entire working directory instead.)
  • wc -l consumes the directory listing as it's being generated, so the output of ls is never in memory at any point in time.
  • -- File names are separated from the command using -- so as not to be understood as arguments to ls (in case log* is removed)

The shell will expand log* to the full list of files, which may exhaust memory if it's a lot of files, so then running it through grep is be better:

ls -Uba1 | grep ^log | wc -l

This last one handles extremely large directories of files without using a lot of memory (albeit it does use a subshell). The -d is no longer necessary, because it's only listing the contents of the current directory.


For a recursive search:

find . -type f -name '*.log' -printf x | wc -c

wc -c will count the number of characters in the output of find, while -printf x tells find to print a single x for each result. This avoids any problems with files with odd names which contain newlines etc.

For a non-recursive search, do this:

find . -maxdepth 1 -type f -name '*.log' -printf x | wc -c

This simple one-liner should work in any shell, not just bash:

ls -1q log* | wc -l

ls -1q will give you one line per file, even if they contain whitespace or special characters such as newlines.

The output is piped to wc -l, which counts the number of lines.


You can do this safely (i.e. won't be bugged by files with spaces or \n in their name) with bash:

$ shopt -s nullglob
$ logfiles=(*.log)
$ echo ${#logfiles[@]}

You need to enable nullglob so that you don't get the literal *.log in the $logfiles array if no files match. (See How to "undo" a 'set -x'? for examples of how to safely reset it.)

Tags:

Bash