cd
is a POSIX-mandated shell builtin:
If a simple command results in a command name and an optional list of arguments, the following actions shall be performed:
- If the command name does not contain any slashes, the first successful step in the following sequence shall occur:
...
- If the command name matches the name of a utility listed in the following table, that utility shall be invoked.
...
cd
...
- Otherwise, the command shall be searched for using the PATH...
While this does not explicitly say it has to be a built-in, the specification goes on to say, in the description of cd
:
Since cd affects the current shell execution environment, it is always provided as a shell regular built-in.
From the bash
manual:
The following shell builtin commands are inherited from the Bourne Shell. These commands are implemented as specified by the POSIX standard.
...
cd
cd [-L|[-P [-e]]] [directory]
I suppose it you could think of an architecture where cd
doesn't have to be a builtin. However, you have to see what a built-in implies. If you write special code in the shell to do something for a some command, you're getting close to having a builtin. The more you do, the better it is to simply have a builtin.
For example, you could have the shell have IPC to communicate with subprocesses, and there would be a cd
program which would check for existence of the directory and whether you have permission to access and it and then communicates with the shell to tell it to change its directory. However, you'll have to then check if the process communicating with you is a child (or make special means of communication with children only, such as a special file descriptor, shared memory, etc.), and if the process is in fact running the trusted cd
program or something else. That's a whole can of worms.
Or you could have a cd
program which makes the chdir
system call, and the starts a new shell with all the current environment variables applied to the new shell, and then kills its parent shell (somehow) when done.1
Worse, you could even have a system where a process can alter other processes' environments (I think technically you can do this with debuggers). However such a system would be very, very vulnerable.
You'll find yourself adding more and more code to secure such methods, and it's considerably simpler to simply make it a builtin.
That something is an executable does not prevent it from being a builtin. Case in point:
echo
and test
echo
and test
are POSIX-mandated utilities (/bin/echo
and /bin/test
). Yet nearly every popular shell has a builtin echo
and test
. Similarly, kill
is also builtin that's available as a program. Others include:
sleep
(not as common)
time
false
true
printf
However, there are some cases where a command cannot be anything but a builtin. One of those is cd
. Typically, if the full path is not specified, and the command name matches that of a builtin, a function suited to that command is called. Depending on the shell, the behaviour of the builtin and that of the executable may differ (this is particularly a problem for echo
, which has wildly differing behaviours. If you want to be certain of the behaviour, it is preferable to call the executable using the full path, and set variables like POSIXLY_CORRECT
(even then there's no real guarantee).
Technically there's nothing preventing you from providing an OS that is also a shell and has every command as a builtin. Close to this extreme end is the monolithic BusyBox. BusyBox is a single binary, that (depending on the name with which it is called) can behave as any of over 240 programs, including an Almquist Shell (ash
). If you unset PATH
while running the BusyBox ash
, the programs available in BusyBox are still accessible to you without specifying a PATH
. They come close to being shell builtins, except that the shell itself is a sort-of builtin to BusyBox.
If you look at the dash
source, the execution thread is something like this (of course, with additional functions involved when pipes and other things are used):
main
→ cmdloop
→ evaltree
→ evalcommand
evalcommand
then uses findcommand
to determine what the command is. If it is a builtin, then:
case CMDBUILTIN:
if (spclbltin > 0 || argc == 0) {
poplocalvars(1);
if (execcmd && argc > 1)
listsetvar(varlist.list, VEXPORT);
}
if (evalbltin(cmdentry.u.cmd, argc, argv, flags)) {
if (exception == EXERROR && spclbltin <= 0) {
FORCEINTON;
break;
cmdentry.u.cmd
is a struct
(struct builtincmd
), one of whose members is a function pointer, with a signature typical of main
: (int, char **)
. The evalbltin
function calls (depending on whether the builtin is the eval
command or not) either evalcmd
, or this function pointer. The actual functions are defined in various source files. echo
, for example, is:
int
echocmd(int argc, char **argv)
{
int nonl;
nonl = *++argv ? equal(*argv, "-n") : 0;
argv += nonl;
do {
int c;
if (likely(*argv))
nonl += print_escape_str("%s", NULL, NULL, *argv++);
if (nonl > 0)
break;
c = *argv ? ' ' : '\n';
out1c(c);
} while (*argv);
return 0;
}
All the links to source code in this section are line number-based, so they may change without notice.
1 POSIX systems do have a cd
executable.
Side note:
There are a lot of excellent posts on Unix & Linux which deal with shell behaviour. In particular:
If you haven't noticed a pattern in the questions listed so far, nearly all of them involve Stéphane Chazelas.
type
commandcd
needs to be a builtin: Why is cd not a program? and this one on whytype
is better thanwhich
: Why not use "which"? What to use then?