The question of “why doesn’t sudo cd work?” has come up a couple times in the last few weeks. I figured that the explanation would make for an interesting blog post.

What Shells Do

(Interactive) shells repeatedly prompt the user to type in a command-line, and then attempt to execute that command-line. Once that command exits, the cycle is repeated. The big question is: what does execute mean? The first whitespace-separated word in the entered string is the command, and the rest of the words are the arguments. Most commands are either the name of an executable file, or the name of a built-in command. There are a few other possibilities such as aliases and function names, but they aren’t relevant to this explanation.

Executable Files

Executable files are stored on disk, typically in directories such as /bin, or /usr/bin. Whenever the shell executes such a command, it searches for the file in the current $PATH, and then creates a new process to run that file, passing the arguments to the command.

Built-in Commands

Some commands are so common or simple that the shell contains a built-in implementation of the command. Other commands have to be built-in simply to operate correctly. Examples of built-ins are echo, test, or cd.

Built-in or External?

At least in bash, you can determine whether a command is an executable file, a built-in, or something else using the type command:

$ type ls
ls is /bin/ls

$ type cd
cd is a shell built-in

You can also use which to determine if or where a given executable exists on disk:

$ which ls
/bin/ls

Process State

Every process has certain state associated with it. Examples are the process ID, ulimit configuration, and the current working directory. This state is generally not shared between processes, and state changes in one process do not affect any other process, even parent or child processes.

When a new process is created, the child process’s state is initially a copy of the parent process. The child process can then modify the state if desired and permissions allow it (e.g. ulimit or nice values often can’t be changed in certain ways). Such changes to the child process’s state do not affect the parent process. When a process exits, all aspects of its state are destroyed, and have no lasting effect. Of course, if a process makes changes to objects other than its own state, such as to files on disk, those changes are preserved even after the process exists; only a process’s own state is destroyed when a process exits.

Current Working Directory

Let’s consider what the current working directory of a process is used for. Users can specify paths (e.g. in command-line arguments or a request for an editor to open or save a file) in two forms; absolute and relative.

Absolute paths, those beginning with / such as /usr/bin, are meaningful as written since they refer directly to the desired file.

Relative paths are meaningless without stating where those paths are relative to. The current working directory is the directory against which any relative path name is resolved. For example, the relative path lib/i386-linux-gnu would refer to /lib/i386-linux-gnu if the current working directory was /, or would refer to /usr/lib/i386-linux-gnu/ if the current working directory was /usr.

The cd Built-in

The cd command changes the value of the current working directory of the current process.

What if cd were an executable file rather than a built-in? The shell would have to execute cd as a new process. cd would change the current working directory value for its own process; a state change that would not affect any other process. Then, cd would exit, and the shell would prompt the user for another command. Overall, these steps wouldn’t have any side-effects; in particular, the current working directory value of the shell would not have changed. This means that a cd executable file wouldn’t be that useful. For this reason, cd must be a built-in command within the shell, so that it affects the shell’s process state.

cd Executable in HP-UX

An interesting piece of Unix trivia: Some versions of HP-UX do actually contain a /usr/bin/cd executable file. This doesn’t seem very useful on the surface. One potential use I’ve seen proposed is to test whether a path name is a valid target for cd without affecting the current process’s state, e.g.:

$ if [ /usr/bin/cd /non/existent/path ]; then echo yes; else echo no; fi
echo no

$ if [ /usr/bin/cd /usr/lib ]; then echo yes; else echo no; fi
echo yes

What sudo Does

sudo is a setuid program which allows running commands as a different user ID. For example, if given permission by a system administrator, sudo may allow certain unprivileged users to run specific commands as root. This is commonly used to allow such users to perform specific limited system administration duties, or if people simply want to hack around permissions issues!

sudo is an executable file and not a shell built-in. sudo therefore runs as a separate process to the shell, and always creates a new process to run the user-specified command; sudo has no built-ins that are run in-process.

An example of sudo usage:

$ ls /root
ls: cannot open directory '/root': Permission denied

$ sudo ls /root
[sudo] password for swarren: 
some
files

Putting it All Together

A simple cd command is executed directly by the shell, and hence affects the shell’s own state.

A sudo cd command attempts to run a cd executable. Since this typically doesn’t exist, this will typically fail as:

$ sudo cd /root
sudo: cd: command not found

Should a cd executable actually exist, and actually implement a “change working directory” operation, it still wouldn’t do what we want, since it would only change the current working directory for the cd process itself, which would not affect either the sudo or shell parent processes.

Credits

Aaron Johnson for the information about the HP-UX /usr/bin/cd executable.

Bob Proulx for review of this blog post. All mistakes are mine though!

Changelog

2017/09/26: Typo.