What does -1 in "ls -1 path" mean? What does -1 in "ls -1 path" mean? shell shell

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 of busybox floating around on embedded routers).
  • Unquoted expansion of ${DIRNAME} allows the directory name to be string-split and glob-expanded before being passed to ls, 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 array
  • echo "${#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.)