Why wouldn't the `which` command work for `cd`? I can't find the executable for `cd` either!

I tried which cd and it didn't give a path but instead returned the exit code 1 (checked with echo $?). The coreutil cd itself is working, so the executable should be there, right? I also ran a find for cd, but there was no executable file shown. How is it implemented then?

Update:

I don't know if I should ask this in another post but since I think it is good here, I'm expanding (?) the post... So the answer was actually quite simple, there's no executable for that — because it's a builtin — But I've found some builtins (bash shell in Fedora) have the executable files! So builtin -> no executable isn't right I suppose? Maybe an answer explaining what builtins actually are (builtin commands?), which actually is the matter here, rather than focusing more on cd... Some good links posted previously indicate that builtins are not programs... so what are they? How do they work? Are they just functions or threads of the shell?


Solution 1:

Command cd can not be an executable

In a shell, cd is used to "go into another directory", or more formally, to change the current working directory (CWD). It's impossible to implement that as an external command:

The directory belongs to a process

The current working directory is the directory that is used to interpret relative paths to obtain a full path that can be used to access files.
Relative paths are used in many places, and the interpretation in one process should not influence another process.
For this reason, every process has its own current working directory.

cd is about changing the current working directory of the shell process, for example, bash.

If it were an external command, an executable in the path, running that executable would create a process with its own working directory, without influencing that of the current shell. Even if the external command would change its directory, that change goes away when the external process exits.

Shell builtin commands

So it makes no sense to run an external command for the task of cd. The command cd needs to apply a change to the currently running shell process.

To do that, it is a “builtin command” of the shell.

Builtin commands are commands that behave similar to external commands, but are implemented in the shell (so cd is not part of the coreutils). This allows the command to change the state of the shell itself, in this case, to call chdir() see (see man 2 chdir);

About which

Now, the answer to the title question is easy:
The executable command which can not tell us that cd is a builtin command because an executable command does not know anything about builtins.

Alternative type -a

As an alternative to which, you can use type -a; It can see executable commands and builtins; Additionally, it sees aliases and functions - also implemented in the shell:

$ type -a cd
cd is a shell builtin
$ type -a type
type is a shell builtin
$ type -a which
which is /usr/bin/which
which is /bin/which

Solution 2:

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:

  1. 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.


Case study: The Debian Almquist Shell (dash)

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):

maincmdloopevaltreeevalcommand

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:

  • Why is printf better than echo?
  • Why not use "which"? What to use then?
  • Why can't I redirect a path name output from one command to “cd”?
  • What is the point of the cd external command?
  • Is there a difference between prepending a name-value-pair to a command and using env in bash?

If you haven't noticed a pattern in the questions listed so far, nearly all of them involve Stéphane Chazelas.

Solution 3:

from man which:

which returns the pathnames of the files (or links) which would be executed in the current environment, had its arguments been given as commands in a strictly POSIX-conformant shell. It does this by searching the PATH for executable files matching the names of the arguments. It does not follow symbolic links.

As we can see from description of which, it is only checking PATH. So if you implemented some bash function, it will show you nothing. It is better to use type command along with which.

For example in Ubuntu ls command aliased to ls --color=auto.

$ type ls
ls is aliased to `ls --color=auto'

$ which ls
/bin/ls

And if you implement test function hello:

$ function hello() { for i in {1,2,3}; do echo Hello $i;done }
$ which hello

which shows nothing. But type:

$ type hello
hello is a function
hello () 
{ 
    for i in {1,2,3};
    do
        echo Hello $i;
    done
}

In your case:

$ type cd
cd is a shell builtin

This means that cd is a shell builtin, it is inside bash. All bash builtins described in man bash, in section SHELL BUILTIN COMMANDS

SHELL BUILTIN COMMANDS
       Unless otherwise noted, each builtin command documented in this section
       as accepting options preceded by - accepts -- to signify the end of the
       options.   The  :, true, false, and test builtins do not accept options
       and do not treat -- specially.  The exit, logout, break, continue, let,
       and  shift builtins accept and process arguments beginning with - with‐
       out requiring --.  Other builtins that accept  arguments  but  are  not
       specified  as accepting options interpret arguments beginning with - as
       invalid options and require -- to prevent this interpretation.