4

I have a file, say: ~/cwd. The content of this file is a single line:

~/tmp

I want fo cd to this (~/tmp) dir. I'm trying:

> cd `cat ~/cwd`

And got:

-bash: cd: ~/tmp: No such file or directory

Why the RELATIVE paths failed? When the content of the ~/cwd is absolute path - it works.

codeforester
  • 39,467
  • 16
  • 112
  • 140
egor7
  • 4,678
  • 7
  • 31
  • 55
  • 2
    `~/cwd` is still an absolute path; `~` is simply a short-cut for the absolute path of the current user's home directory. It's intended for interactive use, though, not for scripting. If you have control over the file (and you should, if you are even thinking about using `eval`), then you should write out the directory name in full, in which case `cd $(< ~/cwd)` would work. – chepner Sep 11 '13 at 13:22

4 Answers4

5

It's not a problem with relative paths -- this happens because the shell evaluation model does tilde expansion BEFORE parameter expansion. Skipping back up to the very beginning of the evaluation process, with eval, introduces security bugs -- so if one REALLY needs to support this (and I argue, strongly, that it's a bad idea), a safe implementation (targeting platforms with the getent command available) would look like the following:

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 _ _ _ _ _ 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%:}"
}

...to use this for a path read from a file safely:

printf '%s\n' "$(expandPath "$(<file)")"

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
}
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • ~username doesn't work with variable: `path="~root"; printf '%s\n' "$(expandPath "$path")"` returns `~root` not `/root`. But it's working if I do `printf '%s\n' "$(expandPath ~root)"` – Jahid Jun 02 '15 at 16:19
  • I started tracing with `set -x` and noticed the unused variable `assetsPath` :D... thanks for quick response... – Jahid Jun 02 '15 at 16:32
  • This should be the accepted answer, considering the security implications of `eval`. – codeforester Apr 25 '18 at 17:40
3

Try this:

eval cd `cat ~/cwd`

The '~' needs to be expanded by the shell. The eval causes the command to be run through the shell's command processing, which includes '~' expansion.

Ziffusion
  • 8,779
  • 4
  • 29
  • 57
  • Thanks, it works! But why 'cd `cat ~/cwd`' didn't? `cat ~/cwd` gives a string, and 'cat' should interpret it well.. – egor7 Sep 11 '13 at 12:12
  • 1
    Keep in mind that '~' is interpreted by the shell, not cat. So, "~/cwd" is first expanded by the shell to a path, and then passed to cat. The output of cat also contains '~', and this needs to be expanded by the shell too. The use of eval causes shell to run it through its command processing, which expands the '~'. – Ziffusion Sep 11 '13 at 12:16
  • 3
    It works, but you'd better trust that file not to be malicious. If it has `$(rm -rf /)/cwd`, you're in bad, bad shape. – Charles Duffy Sep 11 '13 at 12:40
2

Use eval:

eval cd $(cat file)

Otherwise the shell will not be able to interpret the meaning of ~.

fedorqui
  • 275,237
  • 103
  • 548
  • 598
1

Without the need of cat:

eval cd "$(<~/cwd)"
konsolebox
  • 72,135
  • 12
  • 99
  • 105
  • 2
    +1 for not using cat, -1 for suggesting eval without a warning about trust. – Charles Duffy Sep 11 '13 at 12:41
  • @CharlesDuffy Everyone did and it's just a trivial matter anyway. Besides, concluding that someone might not be smart enough to realize the possible consequence of using `eval` on a simple test file could only be somebody else's worry or opinion. Mine is also just an update for their answers. – konsolebox Sep 11 '13 at 14:15
  • 1
    If everybody else jumped off a bridge, would you? Seriously, though, the whole reason this question comes up is that folks don't understand bash's evaluation model (order of evaluation between different expansion rules). If someone doesn't grok that, then *yes*, it _is_ appropriate to assume that they don't appreciate the security implications of jumping back to the beginning of the evaluation chain. – Charles Duffy Sep 11 '13 at 14:33
  • @CharlesDuffy For that obvious quote which I somehow expected, I did recognize the necessity of mentioning the detail about `eval`, but I chose not to for I didn't find it necessary enough for just a simple question. Again considering security in this kind of topic is just an opinion. And I hope you don't conclude that I'm imprudent for such matters. I had answered with many scripts in SO that make use of eval. Try to see if you can find a flaw in those. Besides this of course. – konsolebox Sep 11 '13 at 14:43
  • 1
    I'm holding you to a higher standard than most here because you've demonstrated a higher-than-average level of clue. Take it as a compliment. :) – Charles Duffy Sep 11 '13 at 16:56