Can I make cd be local to a function?

Yes. Just make the function run its commands in a ( ) subshell instead of a { } group command:

doStuffAt() (
        cd -- "$1" || exit # the subshell if cd failed.
        # do stuff
)

The parentheses (( )) open a new subshell that will inherit the environment of its parent. The subshell will exit as soon as the commands running it it are done, returning you to the parent shell and the cd will only affect the subshell, therefore your PWD will remain unchanged.

Note that the subshell will also copy all shell variables, so you cannot pass information back from the subshell function to the main script via global variables.

For more on subshells, have a look at man bash:

(list)

list is executed in a subshell environment (see COMMAND EXECUTION ENVIRONMENT below). Variable assignments and builtin commands that affect the shell's environment do not remain in effect after the command completes. The return status is the exit status of list.

Compare to:

{ list; }

list is simply executed in the current shell environment. list must be terminated with a newline or semicolon. This is known as a group command. The return status is the exit status of list. Note that unlike the metacharacters ( and ), { and } are reserved words and must occur where a reserved word is permitted to be recognized. Since they do not cause a word break, they must be separated from list by whitespace or another shell metacharacter.


It depends.

You can put the function in a subshell. (See also Do parentheses really put the command in a subshell?) What happens in a subshell stays in a subshell. Changes that the function makes to variables, the current directory, to redirections, to traps, and so on, do not affect the calling code. The subshell inherits all these properties from its parent but there is no transfer in the other direction. exit in a subshell only exits the subshell, not the calling process. You can put a piece of code in a subshell by wrapping it in parentheses (line breaks and even whitespace before and after the parentheses are optional):

(
  set -e # to exit the subshell as soon as an error happens
  cd -- "$1"
  do stuff # in $1
)
do more stuff # in whatever directory was current before the '('

If you want to run the whole function in a subshell, you can use parentheses instead of braces to wrap the function's code.

doStuffAt () (
    set -e
    cd -- "$1"
    # do stuff
)

With the Korn-style function definition syntax, you need:

function doStuffAt { (
    set -e
    cd -- "$1"
    # do stuff
) }

The downside of a subshell is that nothing escapes it. If you need to change the current directory but then update some variables, you can't do that with a subshell. There are only two easy ways to retrieve information from a subshell. Like any other command, a subshell has an exit status, but this is an integer between 0 and 255 so it doesn't convey much information. You can use a command substitution to produce some output: a command substitution is a subshell whose standard output (minus trailing newlines) is collected into a string. This lets you output one string.

data=$(
  set -e
  cd -- "$1"
  do stuff # in $1
)
# Now you're still in the original directory, and you have some data in $data

You can save the current directory into a variable, and restore it later.

set -e
old_cwd="$PWD"
cd -- "$1"
…
cd "$old_cwd"

However this is not very reliable. If the code exits between the two cd commands, it'll be in the wrong directory. If the old directory is moved in the meantime, the second cd will not return to the right place. It's possible to be in a directory that you have no permission to change into (because the script has less privileges than its caller), and in this case the second cd will fail. So you should not do this unless you're in a controlled environment where this can't happen (for example, to cd into and out of a temporary directory created by your script).

If you need to both change directory temporarily and affect the shell environment in some way (such as setting variables), you need to carefully split your scripts into parts that affect the shell environment and parts that change the current directory. The shell inherits limitations of early unix systems which didn't have a way to return to the previous directory. Modern unix systems do (you can “save” the current directory's file descriptor, and return to it with fchdir() in an exception handler), but there's no shell interface to this functionality.


When descending into a directory "temporarily" to do some work — IOW, when you want to scope the directory change in some way — it makes sense to take advantage of the directory stack by using pushd and popd. It's a common technique in things like build scripts.

Say you're building a bunch of plugins.

for plugindir in plugin1 plugin2 plugin2 plugin4; do
  pushd -- "$plugindir"
  make
  popd
done