The default character set excludes easy to confuse characters ILOl0. It is fast too

Generating 1 million 40 character passwords

time pw -n 1000000 >/dev/null
0.47s user 0.24s system 229% cpu 0.310 total
cat pw
#!/usr/bin/env bash
#set -x

num_passwords=20                                  # Default number of passwords to return.
pw_len=40                                         # Default password length.
random_data='/dev/urandom'                        # Random data
urandom_bytes_default=300000                      # Default random bytes to read.

letters='A-HJ-KM-NP-Za-km-z'                      # Default letters set.
numbers='1-9'                                     # Default numbers set.
symbols='!?*^_@#%^&*()=+<>}{][;:",./|~\\'\''`-'   # Default symbols set. If dash "-" is needed, put it at the end
characters="$letters$numbers$symbols"             # All default sets combined

min_calculated_urandom_bytes=20000                # Minimum bytes when calculated. Fix issue when not enough data for simple character sets 
urandom_bytes_user=0                              # Leave at 0, for use with logic of -b , --bytes=
urandom_bytes_calculated=0                        # Leave at 0, for use with end logic
regex_match_flags="^-(b|-bytes=|c|-characters=|l|-length=)$" # Pattern to check against a flag being blank and reading next flag as arguemnt

while test $# -gt 0; do
  case "$1" in

    -h|--help)
      echo " "
      echo " "
      echo " "
      echo "pw - generate passwords"
      echo " "
      echo "pw [options]"
      echo " "
      echo "options:"
      echo "-b NUM   ,  --bytes=NUM          Specify bytes to read from "$random_data". Not compatible with flag -n, --ncount. Defaults to $urandom_bytes_default bytes"
      echo "-c 'CHAR',  --characters='CHAR'  Specify allowed password characters. Defaults to '$characters'"
      echo "-h       ,  --help               Show brief help"
      echo "-l NUM   ,  --length=NUM         Specify password length. Defaults to length of $pw_len"
      echo "-n NUM   ,  --ncount=NUM         Specify number of passwords to return. Not compatible with flag -b, --bytes"
      echo " "
      echo " "
      echo " "
      echo " "
      echo "examples:"
      echo " "
      echo " "
      echo "# 20 character alphanumeric with symbols "'!?"*#-'" using 20000 bytes of data from "$random_data""
      echo "pw --bytes=20000 --characters='a-zA-Z0-9"'!?"*#-'"' --length=20"
      echo "   IjLVomO*LZIvBWhmITtS"
      echo "pw -b 20000 -c 'a-zA-Z0-9"'!?"*#-'"' -l 20"
      echo "   IjLVomO*LZIvBWhmITtS"
      echo " "
      echo " "
      echo " "
      echo "# 200 passwords using default values"
      echo "pw --ncount=200"
      echo '   !=[8x|d`dHdVA-:xn8t>G=~tkgbg}T#~2(/r?9N&'
      echo "   ...{200 lines}"
      echo " "
      echo "pw -c '18bu' -l 10 -n 2"
      echo "   bb8b8bb1ub"
      echo "   88b1ub8b8u"
      echo " "
      echo " "
      echo "pw -c '0-4' --length=80 --ncount=10"
      echo "   10132440443120133034412013333104142320411133221101130324111200442311420044122312"
      echo " "
      echo " "
      echo "pw -c 'zplaeiou' --length=80 --ncount=1"
      echo "   uuzzzalilepauzuepaazoizoeiiaazupupalolzliluuoazluzuepzlozepapaioipupapleuzaolpuu"
      echo " "
      echo " "
      echo "pw -c '1-4*-' -l 10 -n 2"
      echo "   2414443*24"
      echo "   *123-*4-31"
      echo " "
      echo " "
      echo "pw -b 400 -c 'a-zA-Z0-9 [#!?*(){}~[]/\\-]'\''' -l 40"
      echo "   EVuMxtVR**6}?M2HTZlED{ARjKL?D]r8h[7Pidvo"
      echo " "
      echo " "
      echo " "
      exit 0
      ;;


    -b)
      shift
      # Test that -b value (previously shifted $1) is gt 0 before setting var urandom_bytes_user
      # And test that $pw_line_count_target has not been set
      if [[ $1 -gt 0 ]] && [[ -z $pw_line_count_target ]] 2> /dev/null; then
        urandom_bytes_user=$1
        urandom_bytes_default=0
        pw_line_count_target=0
      else
          printf "error: \"-b NUM\" needs numeral greater that 0. Value > 1000 recommended\n"
          exit 1
      fi
      shift
      ;;
    --bytes*)
      # Test that --bytes value "${1/*"="/}" is gt 0 before setting var urandom_bytes_user
      # And test that pw_line_count_target is not set
      if [[ "${1/*"="/}" -gt 0 ]] && [[ $pw_line_count_target -le 0 ]] 2> /dev/null; then
        urandom_bytes_user="${1/*"="/}"
        urandom_bytes_default=0
        pw_line_count_target=0
      else
        if [[ ! $pw_line_count_target -le 0 ]] 2> /dev/null; then
          printf "\nflag -n, --ncount not compatible with flag -b, --bytes\n"
          exit 1
        else
          printf "error: usage \"--bytes=NUM\" needs numeral greater that 0. Value > 1000 recommended\n"
          exit 1
        fi
      fi
      shift
      ;;


    -c)
      shift
      # Before set var characters, test for -c value (previously shifted $1) being blank,
      # or another flag shifted in as unintended -c value.
      if [[ ! -z $1 ]] && [[ ! "$1" =~ $regex_match_flags ]]; then
        characters="$1"
      else
        printf "error: usage \"-c 'CHARACTERS'\" (allowed password characters) needs value\n"
        exit 1
      fi
      shift
      ;;
    --characters*)
    # Before set var characters, test for --characters string "${1/*"="/}" being blank,
    # or another flag shifted in as unintended --characters string by checking 
    # $characters_to_check for regex match on $regex_match_flags.
    characters_to_check="${1/*"="/}"
      if [[ ! -z "${1/*"="/}" ]] && [[ ! "$characters_to_check" =~ $regex_match_flags ]]; then
        characters="${1/*"="/}"
      else
        printf "error: usage \"--characters 'CHARACTERS'\" (allowed password characters) needs value\n"
        exit 1
      fi
      shift
      ;;


    -l)
      shift
      # Test that -l value (previously shifted $1) is gt 0 before setting var pw_len
      if [ $1 -gt 0 ] 2> /dev/null; then
        pw_len=$1
      else
        printf "error: usage \"-l NUM\" (password length) needs numeral greater that 0\n"
        exit 1
      fi
      shift
      ;;
    --length*)
      # Test that --length value "${1/*"="/}" is gt 0 before setting var pw_len
      if [[ "${1/*"="/}" -gt 0 ]] 2> /dev/null; then
        pw_len="${1/*"="/}"
      else
        printf "error: usage \"--length=NUM\" (password length) needs numeral greater that 0\n"
        exit 1
      fi
      shift
      ;;


    -n)
      shift
      # Test that -b value (previously shifted $1) is gt 0 before setting var pw_line_count_target
      if [ $1 -gt 0 ] ; then
        pw_line_count_target=$1
        urandom_bytes_default=0
      else
          printf "error: \"-n NUM\" needs numeral greater that 0\n"
          exit 1
      fi
      shift
      ;;
    --ncount*)
      # Test that --bytes value "${1/*"="/}" is gt 0 before setting var pw_line_count_target
      if [[ "${1/*"="/}" -gt 0 ]] ; then
        pw_line_count_target="${1/*"="/}"
        urandom_bytes_default=0
      else
        printf "error: usage \"--ncount=NUM\" needs numeral greater that 0\n"
        exit 1
      fi
      shift
      ;;


    *)
      break
      ;;
  esac
done




# Test that urandom_bytes_user has not been changed from 0
# And test that pw_line_count_target gt 0
if [[ $pw_line_count_target -gt 0 ]] && [[ $urandom_bytes_user -eq 0 ]] ; then
  count_out_of_10000="$(head -c 10000 < "$random_data" | tr -dc "$characters" | wc -c)"
  urandom_bytes_calculated=$(( (13000/$count_out_of_10000) * ($pw_len * $pw_line_count_target) ))
  if [[ $urandom_bytes_calculated -lt $min_calculated_urandom_bytes ]] ; then
    urandom_bytes_calculated=$min_calculated_urandom_bytes
  fi
else
  if [[ $pw_line_count_target -gt 0 ]] && [[ $urandom_bytes_user -ne 0 ]] ; then
    printf "\nflag \" -n|--ncount \" not compatible with flag \" -b|--bytes \"\n"
    exit 1
  fi
fi


if [[ $pw_line_count_target -eq 0 ]]; then
  pw_line_count_target=$num_passwords
fi


# PW generation bits
urandom_bytes=$(( ($urandom_bytes_default) + ($urandom_bytes_user) + ($urandom_bytes_calculated) )) 
head -c     "$urandom_bytes"      < "$random_data" |
tr   -dc    "$characters"                          |
fold -s     -w$pw_len                              |
head -n     "$pw_line_count_target"