Sometimes you need to change the working directory in a shell script. You should take care to reset it back.
Here's an example scenario: you've written a fairly complex shell script, and it needs to reference files that are in the same directory. You can't make assumptions about the working directory that your script was invoked from, so you have to sprinkle dirname "$0"
everywhere.
$0
is a Bash "special parameter" that "expands to the name of the shell or shell script." If the shell is invoked using a relative path, $0
will be a relative path - it doesn't automatically resolve to a full path.
dirname
prints the parent directory name of a file or subdirectory. dirname
also does not resolve paths to a full path.
Here's how to use dirname "$0"
in a script:
#!/usr/bin/env bash
set -euo pipefail
# Source some local file
. "$(dirname "$0")/aliases.sh"
# Load some local config
config=$(cat "$(dirname "$0")/config.json")
# Enumerate some local files
while read -r file; do
# Do something with the file...
done <<< "$(find "$(dirname "$0")" -maxdepth 1 -type f)"
All of these are difficult to read because of the quotation and $(...)
nesting.
Wouldn't it be easier to just make an assumption about the working directory? I strongly believe no, but we should feel free to change the working directory, as long as we reset it on exit. That last part is important, because you may be setting the caller up for confusion or danger.
The one-liner
Here's the trick, put this at the top of every script you write, just below the shebang .
trap "cd \"${PWD}\"" EXIT
Now you're safe to cd
to your heart's content!
Note: if you use ShellCheck to check your scripts for errors (and you should), you will need to put # shellcheck disable=SC2064
above the one-liner to signal that we know what we're doing with the quotes.
Examples
Here's the one-liner in action:
#!/usr/bin/env bash
set -euo pipefail
trap "cd \"${PWD}\"" EXIT
cd "$(dirname "$0")"
echo "Now I'm safe to reference files in '${PWD}' with relative paths!"
And after adding it to our example from above, we can now safely reference sibling files with relative paths:
#!/usr/bin/env bash
set -euo pipefail
trap "cd \"${PWD}\"" EXIT
cd "$(dirname "$0")"
# Source some local file
. ./aliases.sh
# Load some local config
config=$(cat ./config.json)
# Enumerate some local files
while read -r file; do
# Do something with the file...
done <<< "$(find . -maxdepth 1 -type f)"
Limitations
trap
doesn't work if the script is SIGKILL
ed rather than SIGTERM
inated (or other signals). SIGKILL
must end processes immediately, which means any shutdown hooks like this will be skipped.
Here's a script to test this limitation:
#!/usr/bin/env bash
set -euo pipefail
trap "echo \"I exited gracefully!\"" EXIT
echo "My PID is: $$"
kill -s KILL "$$"
Running it would give an output similar to:
$ ./script.sh
My PID is: 17959
zsh: killed ./script.sh
You can see that I exited gracefully!
never printed.
Alternatives
cd -
and cd "${OLDPWD}"
can both take you back to your previous directory in Bash, but the above trap
solution is still better in case you need to change directories multiple times.