What does -1 in "ls -1 path" mean?
COUNT=$(ls -1 ${DIRNAME} | wc -l)
...is a buggy way to count files in a directory: ls -1
tells ls
not to put multiple files on a single line; making sure that wc -l
will then, by counting lines, count files.
Now, let's speak to "buggy":
- Filenames can contain literal newlines. How a version of
ls
handles this is implementation-defined; some versions could double-count such files (GNU systems won't, but I wouldn't want to place bets about, say, random releases ofbusybox
floating around on embedded routers). - Unquoted expansion of
${DIRNAME}
allows the directory name to be string-split and glob-expanded before being passed tols
, so if the name contains whitespace, it can become multiple arguments. This should be"$DIRNAME"
or"${DIRNAME}"
instead.
...also, this is inefficient, as it invokes multiple external tools (ls
and wc
) to do something the shell can manage internally.
If you want something more robust, this version will work with all POSIX shells:
count_entries() { set -- "${1:-.}"/*; if [ -e "$1" ]; then echo "$#"; else echo 0; fi; }count=$(count_entries "$DIRNAME") ## ideally, DIRNAME should be lower-case.
...or, if you want it to be faster-executing (not requiring a subshell), see the below (targeting only bash):
# like above, but write to a named variable, not stdoutcount_entries_to_var() { local destvar=$1 set -- "${2:-.}"/* if [[ -e "$1" || -L "$1" ]]; then printf -v "$destvar" %d "$#" else printf -v "$destvar" %d 0 fi}count_entries_to_var count "$DIRNAME"
...or, if you're targeting bash and don't want to bother with a function, you can use an array:
files=( "$DIRNAME"/* )if [[ -e "${files[0]}" || -L "${files[0]}" ]]; then echo "At least one file exists in $DIRNAME" echo "...in fact, there are exactly ${#files[@]} files in $DIRNAME"else echo "No files exist in $DIRNAME"fi
Finally -- if you want to deal with a list of file names too large to fit in memory, and you have GNU find
, consider using that:
find "$DIRNAME" -mindepth 1 -maxdepth 1 -printf '\n' | wc -l
...which avoids putting the names in the stream at all (and thus generates a stream for which one could simply measure length in bytes rather than number of lines, if one so chose).
To complement Charles Duffy's excellent answer:
There's one edge case his answer doesn't cover: if the first directory entry happens to be a broken symlink, testing for glob expansion with -e
is not enough, given that Bash always applies the existence test to a symlink's target - which in the case of a broken symlink doesn't exist by definition. In other words: for a broken symlink, -e
will indicate false, even though the link itself exists. A fully robust solution must therefore use something like [[ -e "$1" || -L "$1" ]]
(-L
tests if its argument is a symlink, whether broken or not.)
Here's a slightly shorter bash
alternative (uses a subshell):
count=$(shopt -s nullglob; entries=(*); echo "${#entries[@]}")
shopt -s nullglob
ensures that the pattern expands to the empty string if nothing matches.entries=(*)
collects all matches (in the current dir.) in an arrayecho "${#entries[@]}"
output the element-array count.- Since no external utilities are involved, this command is not subject to the
getconf ARG_MAX
limit, so should work with large directories.
Note that whether the above counts hidden (.*
) items as well depends on the state of the dotglob
option.It is easy however, to build fixed hidden-items-included-or-not logic into the command:
Explicitly include hidden items:
count=$(shopt -s nullglob dotglob; entries=(*); echo "${#entries[@]}")
Explicitly exclude hidden items:
count=$(shopt -s nullglob; shopt -u dotglob; entries=(*); echo "${#entries[@]}")
It's possible to wrap all of the above in a flexible function:
countEntries [<dir>] ... counts based on current state of the `dotglob` optioncountEntries <dir> 0 ... counts non-hidden entries onlycountEntries <dir> 1 ... counts all entries, including hidden ones
#!/usr/bin/env bash# SYNOPSIS# countEntries [<dir> [<includeHidden>]]# DESCRIPTION# <dir> defaults to .# <includeHidden> default to the current state of `shopt dotglob`;# a value of 0 explicitly EXcludes, 1 explicity INcludes hidden items.countEntries() ( # Run entire function in subhell. local dir=${1:-.} includeHidden=$2 entries shopt -s nullglob case $includeHidden in 0) # EXclude hidden entries shopt -u dotglob ;; 1) # INclude hidden entries shopt -s dotglob ;; # Otherwise: use *current state* of `dotglob` esac entries=("$1"/*) # Collect in array echo "${#entries[@]}" # Output count.)