LearnLinux FoundationsBash Shell Environment

builtin

builtin - a hands-on Linux lab on a real virtual machine.

Force the shell to run its genuine built-in version of a name with builtin, skipping any function that shadowed it. The one-word fix for a wrapper function that would otherwise recurse into itself forever, the not-a-shell-builtin error when the name is a file, and how builtin differs from command.

Here is a puzzle that has bitten more engineers than they will admit. Someone wants cd to also list the folder it lands in, so every time they change directory they see what is there. Reasonable. So they write a little wrapper and call it cd:

cd() { cd "$1" && ls; }

They test it. The terminal freezes. It never comes back. They have to kill the whole session. What went wrong, and what one word fixes it, is this entire lesson.

The black boxes below are a practice terminal: a safe sandbox that checks the one command each step teaches. Nothing you type here can hang or break a real machine. Every output shown is exactly what bash prints, because builtin reports facts about the shell itself, not about a disk that changes from machine to machine.

First, a word you have met before. A builtin is a command baked directly into the shell, not a separate program on disk. cd, pwd, and echo are builtins: bash runs them itself, instantly, without going hunting for a file. You saw this in the type lesson, where type cd reported cd is a shell builtin.

The builtin command forces bash to run the built-in version of a name and nothing else. Its whole job is to say: ignore any function someone has defined with this name, and run the real shell builtin underneath. That is the escape hatch we need for the frozen cd above.

Before you use it, prove it exists. type reports what any name is, so ask it about builtin itself:

type builtin

prompt: student@linuxcamp:~$ answer: type builtin output: builtin is a shell builtin hint: Type the word type, a space, then the word builtin: type builtin

So builtin is itself a shell builtin, which is a little bit circular and exactly right: it is a tool the shell provides for reaching its own built-in commands. Now watch it do the one job it exists for.

The plain form is builtin NAME ARGS. It runs the builtin called NAME, handing it whatever arguments follow, and it refuses to run anything that is not a builtin. Nothing fancy happens when no function is in the way, and that is the point: it is a promise about WHICH cd runs, not a new behavior.

Change directory to /tmp using the genuine builtin, then confirm where you landed with the genuine pwd:

builtin cd /tmp

prompt: student@linuxcamp:~$ answer: builtin cd /tmp output: hint: Type builtin, a space, then the cd command and its path: builtin cd /tmp

No output, which is correct: the real cd says nothing when it succeeds, exactly like the cd you already know. builtin cd /tmp moved you to /tmp using the shell's genuine built-in cd, even if a function named cd had been defined to do something else. The builtin keyword stepped over the function and reached the real thing underneath.

builtin is not only for cd. Any shell builtin can be summoned this way. Ask the genuine pwd builtin to print your current directory, and it prints the folder the previous step moved you into:

builtin pwd

prompt: student@linuxcamp:/tmp$ answer: builtin pwd output: /tmp hint: Type builtin, a space, then pwd: builtin pwd

/tmp, the folder the last step moved you into. builtin pwd ran the real pwd builtin and it reported the working directory honestly. This is the pattern: builtin in front of any builtin name guarantees you get the shell's own version, never a redefined one. Now back to the frozen terminal from the cold open, because you finally have the word that fixes it.

Recall the broken wrapper: cd() { cd "$1" && ls; }. The moment you defined a function named cd, the name cd stopped meaning the builtin and started meaning YOUR function. So the cd "$1" inside the function does not call the real cd. It calls the function again. Which calls it again. Forever. That is the freeze: a function calling itself with no way out, an infinite recursion.

The fix is one word. Put builtin in front of the inner cd so it reaches past the function to the real builtin:

cd() { builtin cd "$1" && ls; }

Now the inner builtin cd is guaranteed to be the shell's own cd, not the function, so there is no loop. The function changes directory once, then runs ls. This is the single most common reason builtin exists: a wrapper that overrides a builtin and must call the original exactly once without recursing into itself.

This is the trap to remember for life. Any time you write a function that shares its name with a builtin (cd, pwd, echo), calling that bare name INSIDE the function calls the function again, not the builtin. Reach for the real one with builtin NAME. Without that keyword, the function eats itself.

builtin has a strict rule: the name you hand it must actually be a shell builtin. If it is a regular program on disk, like ls, grep, or cat, builtin refuses. Those are files, not builtins, so there is no built-in version for it to run.

Watch it refuse. Ask builtin to run ls, which is an external program, not a builtin:

builtin ls

prompt: student@linuxcamp:~$ answer: builtin ls output: bash: builtin: ls: not a shell builtin hint: Type builtin, a space, then ls: builtin ls

bash: builtin: ls: not a shell builtin. Read it left to right: bash is speaking, the builtin command is the complainer, ls is the name it choked on, and the reason is that ls is not a shell builtin. It is a program living in a file, so builtin has nothing to run and says so. If you see this, check whether the name really is a builtin: run type NAME and look for the words shell builtin. If type calls it a file instead, you wanted plain command, not builtin.

You met command in the last lesson, and it is easy to confuse the two because both bypass functions. Here is the clean split, and it is worth burning in:

Put plainly: command skips your shortcuts to reach the normal command underneath, which is usually a program on disk. builtin insists on the SHELL'S OWN version and errors if there is not one. When you have wrapped a builtin like cd in a function and need the real builtin once, builtin is the exact tool. When you have aliased or wrapped a regular program and want the plain program, that is command.

A quick way to remember it: builtin only ever talks to commands that live INSIDE bash. If the name is a file out on the disk, builtin cannot help you and command is what you want.

Scaffolding off. No command is printed this time. You have everything you need from the steps above.

Someone on this machine defined a function named pwd that prints the wrong thing. You do not care about their function. You want the shell's genuine pwd builtin to print the honest working directory, ignoring the function entirely.

prompt: student@linuxcamp:/tmp$ answer: builtin pwd output: /tmp hint: You want the keyword that forces the shell's own version of a builtin, followed by the builtin name pwd.

builtin pwd reached past the redefined function to the shell's real pwd, which printed the true working directory. That is the everyday move: whenever a name has been wrapped in a function and you need the genuine builtin underneath, builtin NAME is the guarantee. You recalled it from memory, flag-and-all, which is the same recall the real machine will ask of you.

You earned this cheat sheet. Every row is something you just ran or reasoned through:

And the one pattern that makes builtin worth knowing: inside a wrapper function that shares a builtin's name, call the original with builtin NAME so the function does not recurse into itself. cd() { builtin cd "$1" && ls; } is the shape to remember.

builtin reaches inside the shell; command reaches out to programs on disk. If builtin NAME says not a shell builtin, you almost always wanted command NAME instead.

The practice terminal has shown you the shape of builtin. You confirmed it with type, ran the genuine cd and pwd builtins through it, saw the not a shell builtin error, and fixed the classic wrapper that recurses into itself. Every one of those you typed yourself.

The Bash Environment module ends with one real Linux machine, the Bash Environment capstone mission, and that is where you use builtin for real. A VM boots just for you, with its own live shell. It hands you objectives that use exactly what you practiced across this module: variables, aliases, type, which, command, and now builtin. One difference from here: the mission shows no commands. You read the objective, you recall the command, you type it. That recall is what makes it stick.

Finish the other Bash Environment lessons, then go put the shell's escape hatches to work on a real machine.

Practice builtin in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.