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-inYou can also use which to determine if or where a given executable exists on
disk:
$ which ls
/bin/lsProcess 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 yesWhat 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
filesPutting 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 foundShould 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.