Bash & Python Error Guide: 'command not found' in Shell and Scripts
Resolve bash 'command not found': fix a broken PATH, missing installs, sudo stripping PATH, typos, and binaries that exist but are not on the search path.
- #automation
- #troubleshooting
- #errors
- #bash
Overview
command not found means bash searched every directory in $PATH for the name you typed and found no matching executable. The shell is not saying the program is broken — it is saying it cannot locate it. The error therefore lives at the intersection of three things: whether the binary is installed, where it lives, and whether that location is on $PATH for the current shell, user, and context.
The standard form:
deploy.sh: line 8: kubectl: command not found
Or interactively:
bash: terraform: command not found
It surfaces in subtly different contexts: a tool that works in your login shell but not in a script (different $PATH), one that works as your user but not under sudo (sudo resets PATH), one that works in an interactive terminal but not in cron (cron has a minimal PATH), and one installed into a Python/Node user directory that was never added to the path.
Symptoms
- A specific command name fails with
command not foundwhile others work fine. - The command runs in your terminal but a script or cron job invoking it fails.
- It works as your user but
sudo <cmd>reportscommand not found. which <cmd>prints nothing even though you “know” it is installed.
which kubectl
(no output, exit code 1)
echo "$PATH"
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Common Root Causes
1. The program is not installed
The simplest case: the package was never installed, or installed under a different name.
command -v jq || echo "jq is not on PATH"
dpkg -l | grep -i jq
jq is not on PATH
Install it (the package name often differs from the command):
sudo apt-get install -y jq
2. The binary exists but its directory is not on $PATH
Tools installed to /usr/local/bin, /opt/<tool>/bin, ~/.local/bin, or a language-specific dir are unreachable until that directory is in $PATH.
ls -l ~/.local/bin/ | grep -i ansible
echo "$PATH" | tr ':' '\n' | grep -q "$HOME/.local/bin" && echo "on path" || echo "MISSING"
-rwxr-xr-x 1 ubuntu ubuntu 1234 Jun 23 ansible
MISSING
The binary is there but ~/.local/bin is not on $PATH. Add it:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
3. sudo strips your PATH
sudo runs with a sanitized secure_path defined in /etc/sudoers, which usually omits /usr/local/bin and ~/.local/bin. A command on your PATH vanishes under sudo.
which helm
sudo which helm
/usr/local/bin/helm
/usr/bin/which: no helm in (/usr/sbin:/usr/bin:/sbin:/bin)
Use the absolute path under sudo, or run sudo env "PATH=$PATH" helm ..., or add the dir to secure_path in sudoers.
4. Cron / systemd / CI has a minimal PATH
Non-login, non-interactive contexts do not source ~/.bashrc or ~/.profile, so the rich PATH you see interactively is gone. Cron’s default is often just /usr/bin:/bin.
# What cron actually sees
* * * * * echo "PATH=$PATH" > /tmp/cron_path.txt
cat /tmp/cron_path.txt
PATH=/usr/bin:/bin
Set PATH explicitly at the top of the crontab or use absolute paths in the job.
5. A typo, wrong case, or a shell alias/function that no longer exists
The name is simply misspelled, or you are relying on an alias defined only in an interactive shell.
type -a kubctl
bash: type: kubctl: not found
kubctl should be kubectl. type -a confirms whether the name resolves to a binary, alias, or function at all.
6. Hashed path is stale after a move/reinstall
Bash caches command locations. If you moved or reinstalled a binary, the cached path may be wrong or the new one not yet seen.
hash -r # clear the command hash table
which terraform
/usr/local/bin/terraform
After hash -r, the freshly installed binary resolves.
Diagnostic Workflow
Step 1: Confirm what the name resolves to
type -a <cmd>
command -v <cmd>
No output means it is neither a binary on PATH, alias, nor function — installation or PATH is the issue.
Step 2: Find the binary anywhere on disk
ls -l /usr/local/bin/<cmd> /usr/bin/<cmd> ~/.local/bin/<cmd> 2>/dev/null
sudo find / -name '<cmd>' -type f 2>/dev/null | head
If find locates it, this is a PATH problem, not a missing-install problem.
Step 3: Inspect the PATH for the exact context that fails
echo "$PATH" | tr ':' '\n'
# In a script, print PATH at the failing line:
# echo "PATH=$PATH" >&2
Compare the directory from Step 2 against this list.
Step 4: Test under the same context (sudo/cron/CI)
sudo bash -c 'echo "$PATH"; command -v <cmd>'
If it works as you but not under sudo, it is the secure_path issue.
Step 5: Fix PATH where it actually takes effect
# Interactive/login shells
echo 'export PATH="/usr/local/bin:$HOME/.local/bin:$PATH"' >> ~/.bashrc
# Cron
( crontab -l; echo 'PATH=/usr/local/bin:/usr/bin:/bin' ) | crontab -
Example Root Cause Analysis
A nightly cron job that runs aws s3 sync ... fails every night, but the same command works perfectly when an engineer runs it by hand. The cron log shows:
/home/deploy/backup.sh: line 3: aws: command not found
The AWS CLI v2 was installed to /usr/local/bin/aws, which is on the engineer’s interactive PATH (via /etc/profile). Cron, however, runs with a minimal PATH:
which aws
/usr/local/bin/aws
grep -c '/usr/local/bin' <(crontab -l 2>/dev/null) || echo "not in crontab PATH"
not in crontab PATH
Cron’s default PATH=/usr/bin:/bin does not include /usr/local/bin, so aws is invisible to the job even though it is installed.
Fix: pin a complete PATH at the top of the crontab:
( echo 'PATH=/usr/local/bin:/usr/bin:/bin'; crontab -l ) | crontab -
The backup runs successfully on the next scheduled invocation.
Prevention Best Practices
- Use absolute paths (
/usr/local/bin/aws) in cron jobs, systemd units, and CI scripts, where the interactive PATH does not apply. - Set
PATHexplicitly at the top of crontabs and in systemdEnvironment=directives rather than assuming the login PATH. - Add per-user tool directories (
~/.local/bin, language package bins) to~/.bashrcand~/.profileso both interactive and login shells see them. - When a tool must run under
sudo, add its directory tosecure_pathin/etc/sudoersor invoke it with the full path. - After installs or moves, run
hash -r(or open a new shell) so bash does not serve a stale cached location. - For triaging failures in scheduled automation, the free incident assistant can flag PATH/context mismatches from job logs.
Quick Command Reference
# What does the name resolve to?
type -a <cmd>
command -v <cmd>
# Is it installed anywhere?
sudo find / -name '<cmd>' -type f 2>/dev/null | head
# Inspect PATH in the failing context
echo "$PATH" | tr ':' '\n'
sudo bash -c 'echo "$PATH"'
# Add a directory to PATH (interactive)
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc
# Clear stale command cache
hash -r
# Pin PATH in cron
( echo 'PATH=/usr/local/bin:/usr/bin:/bin'; crontab -l ) | crontab -
Conclusion
command not found means bash searched every $PATH directory and found no matching executable. The recurring causes:
- The program is genuinely not installed (or installed under a different package name).
- The binary exists but its directory is not on
$PATH. sudoreplaces your PATH with a restrictedsecure_path.- Cron, systemd, or CI runs with a minimal PATH that omits
/usr/local/binand user dirs. - A typo, wrong case, or reliance on an interactive-only alias.
- A stale hashed location after a move or reinstall.
Resolve it by confirming what the name resolves to with type -a, locating the binary on disk, then aligning $PATH in the exact context that fails — interactive, sudo, and cron each have their own. Read more in the Bash & Python automation guides.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.