PATH Resolution
How the shell resolves command names to executables — and where that resolution breaks.
How the Shell Finds Commands
When you type a bare command name, the shell resolves it in this order:
-
Aliases —
alias decrypt-file="…"(interactive shells only) -
Shell functions —
decrypt-file() { … }(exported withexport -fin bash) -
Builtins —
cd,echo,type(shell-internal, no PATH lookup) -
PATH search — left-to-right scan of
$PATHdirectories, first match wins
type decrypt-file # function, alias, builtin, or file?
type -a decrypt-file # ALL definitions (zsh: where)
alias decrypt-file 2>/dev/null # check alias specifically
which decrypt-file # first PATH match only
where decrypt-file # zsh: all PATH matches (bash: type -a)
PATH Mechanics
echo "$PATH" | tr ':' '\n'
# Option 1: Add directory to PATH (in .zshrc or .bashrc)
export PATH="$HOME/.secrets/bin:$PATH"
# Option 2: Symlink into a directory already in PATH
ln -s ~/.secrets/bin/encrypt-file ~/.local/bin/encrypt-file
# Option 3: Alias (interactive only — scripts won't find it)
alias encrypt-file="$HOME/.secrets/bin/encrypt-file"
Aliases vs Functions vs PATH — When Each Breaks
| Context | Alias | Function | PATH binary |
|---|---|---|---|
Interactive shell |
✅ Works |
✅ Works |
✅ Works |
Inside a script ( |
❌ Not expanded |
✅ If exported |
✅ Works |
|
❌ sudo ignores aliases |
❌ sudo resets env |
⚠️ Only if in |
cron job |
❌ No interactive shell |
❌ No shell profile loaded |
⚠️ Minimal PATH ( |
|
❌ Clean environment |
❌ Clean environment |
❌ PATH is empty |
# In your shell:
alias greeting="echo hello"
greeting # → hello
# In a script:
echo 'greeting' > /tmp/test.sh
chmod +x /tmp/test.sh
bash /tmp/test.sh # → greeting: command not found
# Fails:
sudo encrypt-file secret.txt
# Works:
sudo /home/evanusmodestus/.secrets/bin/encrypt-file secret.txt
# Or expand alias explicitly:
sudo "$(which encrypt-file)" secret.txt
UsrMerge — Why /bin/age and /usr/bin/age Are the Same
where age
# /usr/bin/age
# /bin/age
On Arch (and modern RHEL/Fedora), /bin is a symlink to /usr/bin:
readlink -f /bin
# → /usr/bin
ls -li /usr/bin/age /bin/age
# Same inode — same file
This is the UsrMerge: /bin, /sbin, /lib are all symlinks into /usr/. RHEL did this in RHEL 7. The historical split (boot-essential in /bin, everything else in /usr/bin) no longer applies.
For RHCSA: if a question asks "where is the binary?", the canonical answer is /usr/bin/. The /bin/ path works but is the legacy symlink.
|
Variable vs Full Path — The Tradeoff
# Variable: single source of truth, change once, used everywhere
AGE_RCPT=~/.age/recipients/self.txt
age -e -R "${AGE_RCPT}" -o output.age input.txt
# Full path: unambiguous, survives environment resets
/usr/bin/age -e -R /home/evanusmodestus/.age/recipients/self.txt \
-o /full/path/output.age /full/path/input.txt
| Approach | Strength | Weakness |
|---|---|---|
Variable ( |
DRY, maintainable |
Requires shell expansion |
Full path |
Unambiguous, works in cron/sudo |
Brittle, verbose |
PATH binary ( |
Clean, portable |
Depends on PATH being set |
RHCSA Drill — Verify Your Understanding
# 1. Where does your shell find age?
type age
which age
# 2. Are /bin and /usr/bin the same?
ls -lid /bin /usr/bin
# 3. What PATH does cron see?
crontab -l # Check existing
# Add: * * * * * echo "$PATH" > /tmp/cron-path.txt
# Wait 1 min, then:
cat /tmp/cron-path.txt
# 4. What PATH does sudo see?
sudo env | grep PATH
# Compare with:
env | grep PATH
# 5. Does your alias survive sudo?
alias mytest="echo works"
mytest # → works
sudo mytest # → command not found