How to manually expand a special variable (ex: ~ tilde) in bash

I have a variable in my bash script whose value is something like this:

~/a/b/c

Note that it is unexpanded tilde. When I do ls -lt on this variable (call it $VAR), I get no such directory. I want to let bash interpret/expand this variable without executing it. In other words, I want bash to run eval but not run the evaluated command. Is this possible in bash?

How did I manage to pass this into my script without expansion? I passed the argument in surrounding it with double quotes.

Try this command to see what I mean:

ls -lt "~"

This is exactly the situation I am in. I want the tilde to be expanded. In other words, what should I replace magic with to make these two commands identical:

ls -lt ~/abc/def/ghi

and

ls -lt $(magic "~/abc/def/ghi")

Note that ~/abc/def/ghi may or may not exist.


Solution 1:

If the variable var is input by the user, eval should not be used to expand the tilde using

eval var=$var  # Do not use this!

The reason is: the user could by accident (or by purpose) type for example var="$(rm -rf $HOME/)" with possible disastrous consequences.

A better (and safer) way is to use Bash parameter expansion:

var="${var/#\~/$HOME}"

Solution 2:

Due to the nature of StackOverflow, I can't just make this answer unaccepted, but in the intervening 5 years since I posted this there have been far better answers than my admittedly rudimentary and pretty bad answer (I was young, don't kill me).

The other solutions in this thread are safer and better solutions. Preferably, I'd go with either of these two:

  • Charle's Duffy's solution
  • Håkon Hægland's solution

Original answer for historic purposes (but please don't use this)

If I'm not mistaken, "~" will not be expanded by a bash script in that manner because it is treated as a literal string "~". You can force expansion via eval like this.

#!/bin/bash

homedir=~
eval homedir=$homedir
echo $homedir # prints home path

Alternatively, just use ${HOME} if you want the user's home directory.

Solution 3:

Plagarizing myself from a prior answer, to do this robustly without the security risks associated with eval:

expandPath() {
  local path
  local -a pathElements resultPathElements
  IFS=':' read -r -a pathElements <<<"$1"
  : "${pathElements[@]}"
  for path in "${pathElements[@]}"; do
    : "$path"
    case $path in
      "~+"/*)
        path=$PWD/${path#"~+/"}
        ;;
      "~-"/*)
        path=$OLDPWD/${path#"~-/"}
        ;;
      "~"/*)
        path=$HOME/${path#"~/"}
        ;;
      "~"*)
        username=${path%%/*}
        username=${username#"~"}
        IFS=: read -r _ _ _ _ _ homedir _ < <(getent passwd "$username")
        if [[ $path = */* ]]; then
          path=${homedir}/${path#*/}
        else
          path=$homedir
        fi
        ;;
    esac
    resultPathElements+=( "$path" )
  done
  local result
  printf -v result '%s:' "${resultPathElements[@]}"
  printf '%s\n' "${result%:}"
}

...used as...

path=$(expandPath '~/hello')

Alternately, a simpler approach that uses eval carefully:

expandPath() {
  case $1 in
    ~[+-]*)
      local content content_q
      printf -v content_q '%q' "${1:2}"
      eval "content=${1:0:2}${content_q}"
      printf '%s\n' "$content"
      ;;
    ~*)
      local content content_q
      printf -v content_q '%q' "${1:1}"
      eval "content=~${content_q}"
      printf '%s\n' "$content"
      ;;
    *)
      printf '%s\n' "$1"
      ;;
  esac
}

Solution 4:

How about this:

path=`realpath "$1"`

Or:

path=`readlink -f "$1"`

Solution 5:

A safe way to use eval is "$(printf "~/%q" "$dangerous_path")". Note that is bash specific.

#!/bin/bash

relativepath=a/b/c
eval homedir="$(printf "~/%q" "$relativepath")"
echo $homedir # prints home path

See this question for details

Also, note that under zsh this would be as as simple as echo ${~dangerous_path}