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:


doExpand() {
  local path
  local -a resultPathElements

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

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

expandString() {
    doExpand "$1"

So two questions:

  1. Did I miss any cases or get any cases wrong? (Tests in the github repo.)

  2. Can this be improved in any meaningful ways?


1 Answer 1


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:

        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#?}" &&         ### requires standards comp
            eval '[ "$1" != '"$1 ] &&         ### which makes this safe
                  path=$1${2+/\$2}"           ### and this
        (${BASH_VERSION+$(( path  =
         ${BASH_VERSION+${path##-*}}*)        ### matches !bash||bad inde
            unset path
           ${path?bad tilde: "$1${2+/$2}"}    ### break w/ error
        (*) path=${DIRSTACK[path]}${2+/$2}
  • \$\begingroup\$ Nice. This is certainly much shorter. The recursion is clever (though I don't know how much it really helps here). This doesn't quite work correctly though (it fails on the ~+ non-digit, the ~- digit cases and the ~+ digit out of bounds cases). Fixing the latter is easy (just check for that before indexing into DIRSTACK). Fixing the others is escaping me at the moment. Also indexing into DIRSTACK generates a warning that mine doesn't. I've made some changes to mine based on insights from this though. You can check the repo for them (the new version is only 25 lines for the case). \$\endgroup\$ Dec 14, 2015 at 4:50
  • \$\begingroup\$ @EtanReisner - what's it supposed to do with a non-digit? the recursion matters - but it only ever happens the one time, of course. it makes it much easier to negate. I was able to use the dirstack thing. in truth, i almost never use bash - or arrays - and i'm admittedly not all that hip to the popdirs deals, but i checked and it worked for $PWD and $OLDPWD and a stack of 4 or 5 test dirs. \$\endgroup\$
    – mikeserv
    Dec 14, 2015 at 5:24
  • \$\begingroup\$ @EtanReisner - i guess i know what you mean now about the negatives failing now - apparently bash reports errors for unset array indexes only when they are referenced in reverse - with a the - negative index. so i fixed that by doing less and just ~ twiddling the negative indexes. but how do the positives fail? if a user references an unset index $path should be set null - or else unset altogether. that would make sense, but what else should happen? \$\endgroup\$
    – mikeserv
    Dec 14, 2015 at 20:20
  • \$\begingroup\$ Ah... I see what the recursion gains you now. Yeah, that is useful. I would probably have just handled that manually myself. This version still fails on a number of tests (and this shouldn't error on a bad tilde it should just return the original, which assigning in the case label, while clever, makes a bit hard without adding a temporary variable). I think I'll be accepting this answer though as it gave me many insights, a few new test cases and caused great improvements to the original. \$\endgroup\$ Dec 16, 2015 at 13:14
  • \$\begingroup\$ I might file a new question with the current code to get more feedback though. (... Eventually at least given the pace at which I got answer to this question. =)) \$\endgroup\$ Dec 16, 2015 at 13:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.