Expanding tilde inside assignments and strings in bash Inspired by the work Charles Duffy did in this answer and the work of Jonathan Leffler in this one and because I couldn't leave well-enough alone.

I went and wrote expandTilde.sh:

#!/bin/bash

doExpand() {
  local path
  local -a resultPathElements

  for path in "$@"; do
    : "$path"
    case $path in
      "~+")
        path=$PWD
        ;;
      "~+"/*)
        path=$PWD/${path#"~+/"}
        ;;
      "~-")
        path=$OLDPWD
        ;;
      "~-"/*)
        path=$OLDPWD/${path#"~-/"}
        ;;
      "~")
        path=${HOME-~}
        ;;
      "~"/*)
        path=$HOME/${path#"~/"}
        ;;
      "~"[0-9]|"~"[+-][0-9])
        local num=${path#"~"}
        local op=${num%%[0-9]*}
        num=${num#[+-]}
        local opath=$path
        if [ "$op" = "-" ]; then
          ((num+=1))
        fi
        path=${DIRSTACK[@]: $op$num:1}
        : "${path:=$opath}"
        ;;
      "~"*)
        local username=${path%%/*}
        username=${username#"~"}
        IFS=: read -r _ _ _ _ _ homedir _ &lt; &lt;(getent passwd "$username")
        if [ "$homedir" ]; then
            if [[ $path = */* ]]; then
              path=${homedir}/${path#*/}
            else
              path=$homedir
            fi
        fi
        ;;
    esac
    resultPathElements+=( "$path" )
  done
  local result
  printf -v result '%s:' "${resultPathElements[@]}"
  printf '%s\n' "${result%:}"
}

expandAssign() {
  local -a pathElements
  IFS=: read -r -a pathElements &lt;&lt;&lt;"$1"
  : "${pathElements[@]}"
  doExpand "${pathElements[@]}"
}

expandString() {
    doExpand "$1"
}


So two questions:


Did I miss any cases or get any cases wrong? (Tests in the github repo.)
Can this be improved in any meaningful ways?
case is most useful when its order of expansion is considered. if you construct your conditions in such a way that a previous match will narrow the possibilities for the current match, then you can get much nearer much quicker to your target match. what's more, case seems always to make better sense as a function in itself - so that it might call itself recursively when matches are ambiguous. consider the following:

   pathcase()
        case    $1 in  ([!~]*|'') ! :;;       ### ^tilde or else
        (*/*)
            pathcase "${1%%/*}" "${1#*/}"     ### squeeze to relevant ele
        ;;
        (?)
            path=${HOME-~}${2+/$2}            ### let expansions work, to
        ;;
        (?-)
            path=${OLDPWD:+$OLDPWD${2+/$2}}   ### its not always set
        ;;
        (?+)
            path=$PWD${2+/$2}                 ### it should be
        ;;
        (?*[!-+0-9]*|??*[-+]*)                ### negate
            pathchk -p -- "${1#?}" &amp;&amp;         ### requires standards comp
            eval '[ "$1" != '"$1 ] &amp;&amp;         ### which makes this safe
                  path=$1${2+/\$2}"           ### and this
        ;;
        (${BASH_VERSION+$(( path  =
         ${#DIRSTACK[@]}-${1#?+}-1))}|\
         ${BASH_VERSION+${path##-*}}*)        ### matches !bash||bad inde
            unset path
           ${path?bad tilde: "$1${2+/$2}"}    ### break w/ error
        ;;
        (?+*)
            path=${DIRSTACK[${1#?+}]}${2+/$2}
        ;;
        (*) path=${DIRSTACK[path]}${2+/$2}
        esac