0

I wondered what might be a safe or the Unix compatible way to change to the directory from which a script is called.

These two methods even work in a directory with spaces and special characters in it, without using quotes. But there might be some situations where this might not work. Or is there?

#!/bin/bash

cd $(dirname $0)
pwd
cd ${0%/*}
pwd

~/test 1"\ ü`\($$ ./cd.sh
/home/syss/test 1"\ ü`\($
/home/syss/test 1"\ ü`\($
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
syss
  • 701
  • you tagged bash and gave a bash script; are you looking for a bash-centric solution, or across any shell? – Jeff Schaller Mar 07 '16 at 19:39
  • basically I am interested in bash, zsh and busybox – syss Mar 07 '16 at 19:41
  • 3
    Your test is not valid. In your case dirname $0 is . since you called the script as ./ch.sh. If you had called it with its full pathname then you would see that quotes are required because then dirname would produce a path that has a space in it. – Celada Mar 07 '16 at 20:03
  • Might be tricky if the script is piped to the shell via standard input. – thrig Mar 07 '16 at 20:17
  • 2
    Parameter expansions without double quoting is rarely a good idea. – PM 2Ring Mar 07 '16 at 20:23

1 Answers1

5

Beware that $0 can be a relative path, so calling cd twice might not work. Apart from that, extracting the directory part of $0 works in most cases. Most is not the same as all. Some cases where it can fail include:

  • The script is not invoked by running its path but by calling a shell on it, e.g. bash myscript (which does a PATH lookup).
  • The script is moved during its execution.

As long as you document that your script must be called without shenanigans, taking the directory part of $0 is ok.

You need to be a little careful; it's possible for $0 to not contain a slash at all, if it was found via an empty PATH entry or, with some shells, a PATH entry for .. This case is worth supporting, and it means you need to take precautions with ${0%/*}.

The dirname approach isn't completely straightforward either. The command substitution eats up newlines at the end. And with both approaches you need to take care that the string could begin with - and be interpreted as an option; pass -- to terminate a command's option list.

case "$0" in
  */*) cd -- "${0%/*}";;
  *) cd -- "$0";;
esac

or

cd -- "$(dirname -- "$0"; echo /)"

Regarding the double quotes, you're asking the wrong question. "$0" is the value of the parameter 0. $0, unquoted, is the value of the parameter 0 split at characters in IFS with each element then interpreted as a glob pattern and replaced by matching files if there are any (that latter part only if globbing is not turned off). You don't mean the value of the parameter 0 split at characters in IFS with each element then interpreted as a glob pattern and replaced by matching files if there are any (that latter part only if globbing is not turned off), do you? You mean the value of the parameter 0. So write what you mean: "$0".

The only reason your tests didn't choke is that you didn't try it with problematic names and you tested with a specific shell that happens to repair your mistake in this specific scenario. With a directory name like foo bar, you end up passing two arguments foo and bar to the cd command; bash's cd command interprets this as “change to the directory obtained by concatenating foo, a space and bar” so it happens to build back the right name. A different shell might interpret this as “complain about a spurious argument”, “change to the directory foo”, or “change to the directory obtained by replacing foo by bar in the current working directory”. With bash, your script would fail if the name contained two consecutive spaces, for example.