A long-running bash script installs a newer build of a tool as /usr/local/bin/mytool, but later calls to mytool in the same script still run the old /usr/bin/mytool. How does bash resolve a bare command name, and what makes it keep using the stale path?
answer
- bash remembers, it does not re-search
- alias, function, builtin, then PATH
- the old file still exists, so the cache stays valid
- hash -r and type -a
- the table dies with the process
basics
~20 sBash remembers the full path of each external command it has run, in a per-shell hash table, and reuses it instead of searching PATH again. The old location is still valid, so the cached entry keeps winning. Clearing it with hash -r forces a fresh lookup.
solid answer
~40 sFor a bare name, bash resolves in a fixed order: alias, then shell function, then builtin, and only then an external program found by searching `PATH` left to right for the first executable file with that name. The result of that last step is cached in the shell's *hash table*, so the second and later invocations skip the search entirely. Your script ran `mytool` once before the install, cached `/usr/bin/mytool`, and keeps using it — the cached file still exists, so bash has no reason to look again. `hash -r` forgets everything remembered, and `hash -d mytool` forgets just that one entry; after either, the next call searches `PATH` again and finds `/usr/local/bin/mytool`. `type mytool` shows you whether an entry is hashed and which path it points to.
code
bash · 7 linesinstall -m 0755 ./mytool /usr/local/bin/mytool
hash -r # forget remembered command locations
type -a mytool # every candidate on PATH, in resolution order
command -v mytool # just the one that will run
/usr/local/bin/mytool --version # or sidestep lookup entirelygo deeper
Know that a bare command name is found by searching PATH left to right and that the first match wins, and that type or command -v tells you which file would actually run.
Explain the full resolution order — alias, function, builtin, hash table, PATH — and that bash caches resolved paths per shell, with hash -r as the way to clear that cache.
Diagnose the stale-binary case: recognise that the cache stays valid because the old file still exists, that it is per process so it will not reproduce in a fresh terminal, and prefer absolute paths or an explicit hash -r in scripts that install what they then run.
Treat command resolution as part of a job's reproducibility contract. Decide whether pipelines pin absolute paths, set PATH explicitly, or run inside an image where only one version exists, so a build never silently executes a different binary than the one it thinks it installed.
## The resolution order When bash sees a bare word in command position it works down a fixed list: 1. **Alias** — expanded only in interactive shells by default, so rarely a factor in a script. 2. **Shell function** — a function named `mytool` shadows any program of that name. 3. **Builtin** — `cd`, `echo`, `test` and friends are inside bash; no process is created for them. 4. **The hash table** — a per-shell cache of full paths bash has already resolved. 5. **A `PATH` search** — each directory in `PATH`, left to right, checking for a file of that name that is executable. The *first* match wins, not the best one. `type -a mytool` prints every candidate it can see, in order, and annotates the cached one. That single command answers "which one will actually run, and why" faster than any amount of reasoning about `PATH`. ## The hash table Step four is the one that surprises people. Searching directories on every invocation is wasteful in a loop that calls a command thousands of times, so bash records the resolved path the first time and reuses it. The `hash` builtin is the interface to that cache: - `hash` with no arguments lists the remembered commands and their hit counts. - `hash -r` forgets all of them. - `hash -d mytool` forgets one entry. - `hash -p /usr/local/bin/mytool mytool` records an entry explicitly, without searching. - `set +h` turns hashing off for the shell entirely (it is on by default; `set -h` turns it back on). Bash does re-search when a remembered path no longer exists, so *replacing* a binary in place, or deleting the old one, tends not to produce this bug. The stale-lookup case is precisely the one in the question: the old file is still there and perfectly runnable, so the cache stays valid by bash's rules while being wrong by yours. ```bash ls >/dev/null # first call: PATH is searched and the result cached type ls # ls is hashed (/bin/ls) hash -r type ls # ls is /bin/ls -- searched again, no longer cached ``` ## The table is per process The cache lives in one bash process. A child shell, a command substitution, or the next script the same runner invokes all start with an empty table and resolve from scratch. That is why the symptom appears only inside a single long-running script that both installs and then uses a tool — and why it usually vanishes the moment you try to reproduce it by hand, which makes it a genuinely confusing bug to chase. ## The other half: PATH order Even with a clean cache, which binary runs is decided by `PATH` order, and the first match wins. If `/usr/bin` precedes `/usr/local/bin`, installing into `/usr/local/bin` changes nothing at all. `type -a mytool` shows you the full candidate list and makes the ordering problem obvious. `command -v mytool` prints just the path that would be used, which is the scriptable form for asserting a dependency: ```bash command -v mytool >/dev/null || { echo "mytool not installed" >&2; exit 1; } ``` Be aware that `command -v` reports functions and builtins too, so it answers "is this name runnable" rather than "is this file installed". ## Making a script immune A script that installs something and then uses it should not rely on cache behaviour at all. In rough order of robustness: 1. **Call the absolute path.** `/usr/local/bin/mytool` cannot be misresolved by anything. Best for a script that just installed the file and knows exactly where it put it. 2. **`hash -r` after any install step** that adds or shadows an executable, and after any change to `PATH`. One cheap line, and it documents the intent. 3. **Set `PATH` explicitly at the top of the script**, so the resolution order is a property of the script rather than of whatever environment invoked it. This matters most for unattended runs, where the inherited `PATH` is far shorter than the interactive one. ## In review The tell is a script that both *modifies what is installed* and *calls the modified thing by bare name* in the same process. If those two happen in the same bash, either use the absolute path or clear the hash between them. It is a small bug, but it produces the worst possible failure mode: a deployment that reports success while running the previous version.
- You clear the hash table and it still runs the old binary. What is the next thing to check?`PATH` order. The search takes the first match, left to right, so if `/usr/bin` comes before `/usr/local/bin` the old copy wins every time regardless of caching. `type -a mytool` lists all candidates in order and makes it obvious. Fix by reordering `PATH` for the script, or by calling the absolute path you just installed.
- Why does the problem disappear when you run the same commands by hand in a fresh terminal?The hash table belongs to a single bash process. Your new shell starts with an empty one and searches `PATH` from scratch, finding the new binary. The bug needs one process that both resolved the name before the install and called it again afterwards — which is the shape of a long-running script, not of an interactive session.
- How should a script assert that a tool it depends on is available?`command -v mytool >/dev/null || { echo "mytool missing" >&2; exit 1; }` near the top, so the script fails immediately with a clear message rather than midway through with a confusing one. Note that `command -v` also matches shell functions and builtins, so it answers "is this name runnable here", not "is this file installed".
saying these in an interview costs you the question
- Believes bash searches PATH on every single invocation
- Thinks the hash table is shared between shells or persisted to disk
- Assumes a later PATH entry can override an earlier one
- Blames the installer instead of checking type -a
- Cannot name a way to force re-resolution of a command name