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:

  1. Aliasesalias decrypt-file="…​" (interactive shells only)

  2. Shell functionsdecrypt-file() { …​ } (exported with export -f in bash)

  3. Builtinscd, echo, type (shell-internal, no PATH lookup)

  4. PATH search — left-to-right scan of $PATH directories, first match wins

Inspect what the shell actually resolves
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

View current PATH (one directory per line)
echo "$PATH" | tr ':' '\n'
How a custom script gets into PATH
# 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 (#!/bin/bash)

❌ Not expanded

✅ If exported

✅ Works

sudo command

❌ sudo ignores aliases

❌ sudo resets env

⚠️ Only if in secure_path

cron job

❌ No interactive shell

❌ No shell profile loaded

⚠️ Minimal PATH (/usr/bin:/bin)

env -i command

❌ Clean environment

❌ Clean environment

❌ PATH is empty

Prove aliases don’t survive scripts
# 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
sudo workaround — use full path
# 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 ($AGE_RCPT)

DRY, maintainable

Requires shell expansion

Full path

Unambiguous, works in cron/sudo

Brittle, verbose

PATH binary (age)

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