Signal9Signal9 — home
online
all lessons

How the Shell Finds a Command: PATH, Builtins & Lookup Order

Beginner25 min5 steps
What you'll learn
  • Explain the shell's command lookup order: alias → function → builtin → PATH search
  • Read PATH as a colon-separated, left-to-right search list and find which file a name resolves to
  • Distinguish a shell builtin from an external program, and say why cd must be a builtin
  • Diagnose 'command not found' (127) and 'permission denied' (126) from how lookup works
  1. Typing a name is a question, not an address

    See that the shell has to resolve a bare name to something runnable before anything happens.

    When you type ls and press Enter, you didn't tell the shell where ls is — you gave it a name and asked it to figure out what that name means right now. Resolving that name is a real step that happens before your program ever starts, and it follows fixed rules. Understanding those rules turns two of the most common beginner errors — command not found and permission denied — from mysteries into one-line diagnoses.

    The shell has several kinds of thing a name could refer to: an alias you defined, a shell function, a builtin baked into the shell itself, or an external program — a real file sitting in a directory somewhere. When more than one exists for the same name, the shell doesn't guess; it checks them in a set order and takes the first hit.

  2. The lookup order — and type -a shows it

    Recite the resolution order and inspect every meaning of a name at once.

    Bash resolves a plain command name in this order, stopping at the first match:

    1. alias — a text shortcut you (or a startup file) defined.

    2. function — a shell function.

    3. builtin — a command implemented inside the shell (cd, echo, pwd, export).

    4. PATH search — the first matching executable file found by scanning the PATH directories.

    The tool that reveals all of this is `type`. Plain type name tells you what would run; `type -a name` lists every meaning in priority order, so you can see when one thing is shadowing another. Prefer type over the older which: which only looks at PATH files and is blind to aliases, functions, and builtins — exactly the things that change what actually runs.

    type -a echo
    echo is a shell builtin
    echo is /usr/bin/echo
    echo is /bin/echo
    -a
    list ALL meanings (alias, function, builtin, and every PATH match), highest priority first
    echo is BOTH a builtin and a real program — and the builtin wins because builtins are checked before the PATH search. So plain echo runs the builtin; you'd only get /usr/bin/echo by asking for it by path. The file shows up twice because the PATH search reports every match in order: on this box /bin is a symlink to /usr/bin (a "merged /usr" system) and both directories are on PATH, so the SAME binary is found under both names. type -a exposes all of it; which would show only the first file and miss the builtin that actually runs.
    type cd
    cd is a shell builtin
    cd is a builtin, not a program — and it MUST be. A program runs in its own child process; if cd were external it could only change that child's directory, which vanishes when the child exits, leaving your shell exactly where it started. Only a builtin runs inside the shell itself and can move the shell.

    Check yourself

    Which command shows you EVERY meaning of a name (alias, function, builtin, and PATH files), in the order the shell would pick?

  3. PATH: a search list, left to right

    Read PATH as an ordered list of directories and find which file a name resolves to.

    When lookup reaches step 4, the shell searches `PATH` — an environment variable holding a colon-separated list of directories. It checks them left to right and runs the first executable file whose name matches. Order is everything: if two directories both contain a python, the one earlier in PATH wins.

    That's the whole mechanism. command -v name (or which name) reports the winning file. And it's why a fresh install sometimes 'can't find' a tool that's clearly present — the tool's directory just isn't on PATH.

    echo "$PATH" | tr ':' '\n'
    /usr/local/bin
    /usr/bin
    /bin
    /usr/sbin
    /sbin
    PATH printed one directory per line (tr swaps colons for newlines just to read it). These are searched top to bottom. A name is only runnable as a bare command if it lives in one of these — and if it appears in two, the higher one wins.
    command -v ls; command -v sort
    /usr/bin/ls
    /usr/bin/sort
    -v
    print how the name resolves (the path, or the alias/builtin) instead of running it
    command -v resolves a name to the exact file that would run — the winner of the PATH search. It's the scriptable, POSIX way to answer 'where does this resolve?' and to test whether a tool exists at all (it exits non-zero if the name resolves to nothing).
  4. Reading the two classic errors

    Turn 'command not found' and 'permission denied' into precise lookup diagnoses.

    Command lookup explains the two errors every beginner hits, and their exit statuses (from the previous lesson):

    • `command not found` (exit 127) — lookup found nothing: no alias/function/builtin, and no matching file in any PATH directory. Usually a typo, or the program's directory isn't on PATH.
    • `permission denied` (exit 126) — lookup found the file, but it isn't executable (its execute bit is off), so the shell won't run it. chmod +x fixes that.

    Bash also keeps a small hash table caching where it last found each command, so it doesn't re-scan PATH every time. If you move or install a program and the shell still 'can't find' it (or finds the old one), hash -r clears that cache.

    printf '#!/bin/sh\necho hi\n' > /tmp/greet; /tmp/greet; echo "status: $?"
    /tmp/greet: Permission denied
    status: 126
    The file exists and lookup found it — but it was created without the execute bit, so the shell refuses to run it: exit 126, 'permission denied'. This is NOT 'not found' (127); the distinction tells you exactly what's wrong — the file is there, it's just not marked runnable.

    Example — not runnable on your lab box

    chmod +x /tmp/greet; /tmp/greet; echo "status: $?"
    hi
    status: 0
    +x
    add the execute permission so the shell is allowed to run the file
    chmod +x sets the execute bit; now the same file runs and exits 0. The lesson: 126 vs 127 pinpoints the fix — 126 means 'make it executable', 127 means 'fix the name or PATH'.

    Check yourself

    A command you just installed gives 'command not found' with exit 127, but you can see the binary exists. What's the most likely cause?

  5. Now do it — ask the shell what a name means

    Resolve real names and watch precedence and PATH in action.

    Three quick experiments. First, make precedence visible: ask type -a about a name that has more than one meaning and watch the order.

    type -a echo; echo '---'; type cd; type -a ls
    Read the winners: echo resolves to the builtin (listed first) even though a program also exists; cd is builtin-only; ls is typically the /usr/bin/ls file — unless your shell has an ls alias, in which case type shows the alias first. You're seeing the resolution order decide what runs.
    echo "$PATH" | tr ':' '\n' | nl; command -v sort
    Number the PATH directories with nl and see command -v report which one won for sort. It will be the first PATH entry that contains a sort — proof that the search is ordered and first-match-wins.
    printf '#!/bin/sh\necho from-path\n' > /tmp/mytool; chmod +x /tmp/mytool; PATH="/tmp:$PATH" mytool
    Put a script in /tmp, make it executable, and prepend /tmp to PATH for just this one command — now the bare name mytool resolves and runs, no ./ needed. This is exactly how installing a tool works: drop an executable into a PATH directory and its name becomes a command. (The PATH change lasts only for this line.)
Recap
  • The shell resolves a bare name in a fixed order — alias → function → builtin → PATH search — and takes the first match. type -a name shows every meaning in that order.
  • PATH is a colon-separated list of directories searched left to right; the first matching executable wins. command -v name reports the winner.
  • Builtins (cd, echo, export) run inside the shell; cd must be a builtin because an external program couldn't change the shell's own directory. Prefer type over which, which only sees PATH files.
  • 127 'command not found' = lookup found nothing (typo or PATH). 126 'permission denied' = file found but not executable (chmod +x). Never put . on PATH.
Command lookup at a glance
QuestionTool / answer
What does this name resolve to (all meanings)?type -a name
Which file will actually run?command -v name
What directories are searched, in order?echo "$PATH" | tr ':' '\n'
Why 'command not found' (127)?not on PATH / typo
Why 'permission denied' (126)?file not executable → chmod +x
Shell still finds an old/moved binary?hash -r (clear the lookup cache)