How to loop over users?

There's no standard command to enumerate all existing user accounts. On most Unix variants, /etc/passwd contains the list of local accounts, always with the same traditional format (colon-separated columns). On Linux with Glibc (i.e. any non-embedded Linux), you can use the getent command: getent passwd is similar to cat /etc/passwd, but also includes remote accounts (NIS, LDAP, …).

The following snippet enumerates user accounts:

getent passwd | while IFS=: read -r name password uid gid gecos home shell; do
  echo "$name's home directory is $home"
done

Filtering the flesh-and-blood users is not possible in a completely reliable way, because nothing in the user database says whether a user is flesh-and-blood. (Plus: do test accounts count? Do guest accounts count? etc.) Here are some heuristics you can apply.

  • You can filter users whose home directory seems not to be a system directory.

    top=${home#/}; top=${top%%/*}
    case $top in
      |bin|dev|etc|lib*|no*|proc|sbin|usr|var) echo "Looks like a system user";;
      *) echo "Looks like a user with a real home directory";;
    esac
    
  • You can test if the user owns their home directory. That's almost always the case for flesh-and-blood users, and usually not the case for system users, but this is not very reliable because some system users do own their home directory. Furthermore it's possible for the home directory of flesh-and-blood users not to exist if it's a remote directory that isn't currently available, though normally the directory would be automounted. On Linux:

    if [ -d "$home" ] && [ "$(stat -c %u "$home")" = "$uid" ]; then
      echo "Likely flesh-and-blood"
    else 
      echo "Probably a system user"
    fi
    
  • You can test if the user has a shell that's an actual shell. But many system users also do, so that's even less reliable than the home directory.

  • You can test whether the account has a password. This isn't fully reliable because flesh-and-blood users don't always have a password (for example they might only have SSH keys). Occasionally system accounts have a password — in particular the root account often does. How to do this depends on the Unix variant and often requires root access. On Linux with shadow passwords (which is the normal thing for Linux), you need to look in /etc/shadow for the password.

    case $(getent shadow "$name" | awk -F: '{print $2}') in
      ?|??) echo "No password on this account";;
      |\$*) echo "This account has a password";;
      *) echo "Weird password field";;
    esac
    
  • Many systems define UID ranges for system accounts vs flesh-and-blood users. The ranges are system-dependent: the default depends on the Unix variant and distribution, and it can usually be configured by the system administrator. On most Linux distributions, the ranges are defined in login.defs: UID_MIN to UID_MAX for human users, other values for system accounts.

I think the path of the home directory is the most reliable single indicator, but you may want to combine it with other heuristics.


Based on the comments on the question and some experimentation, here's what I came up with...

#!/bin/bash
#Use awk to do the heavy lifting.
#For lines with UID>=1000 (field 3) grab the home directory (field 6)
usrInfo=$(awk -F: '{if ($3 >= 1000) print $6}' < /etc/passwd)

IFS=$'\n' #Use newline as delimiter for for-loop
for usrLine in $usrInfo
do
  #Do processing on each line
  echo $usrLine
done

Or as suggested in the comments, getent passwd could be used for cases where /etc/passwd doesn't have the full list. Kudos to Bratchley for that gem. :)

usrInfo=$(getent passwd | awk -F: '{if ($3 >= 1000) print $6}')

Note, however, that the check >= 1000 might need to be different on different OSes.


I just wrote the following using @gilles awesome answer as a template. Works well for me. Tested on Ubuntu 16.04 using zsh.

# get all users
getent passwd | while IFS=: read -r name password uid gid gecos home shell; do
    # only users that own their home directory
    if [ -d "$home" ] && [ "$(stat -c %u "$home")" = "$uid" ]; then
        # only users that have a shell, and a shell is not equal to /bin/false or /usr/sbin/nologin
        if [ ! -z "$shell" ] && [ "$shell" != "/bin/false" ] && [ "$shell" != "/usr/sbin/nologin" ]; then
            echo "$name's home directory is $home using shell $shell"
        fi
    fi
done