Bash Error Guide: 'command not found' — Fix PATH, Typos, and Missing Binaries
Fix 'command not found' in Bash: repair PATH under cron and systemd, install missing binaries, resolve typos and empty variables, and fail loudly on exit 127.
- #bash
- #automation
- #troubleshooting
- #errors
Stuck on this Bash & Python Automation error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
command not found is the single most common failure in Bash automation. The shell tried to run a word as a command, searched every directory in $PATH, found no matching executable, and gave up. It appears interactively, in scripts, and — most painfully — inside cron and CI where $PATH is far smaller than your login shell:
./deploy.sh: line 14: kubectl: command not found
The number after line is where the shell gave up, and the word after the last colon is exactly what it could not resolve. That word is the key to every diagnosis below.
Symptoms
- A script that runs fine in your terminal fails under cron, systemd, or CI with
command not found. - The named command clearly “exists” —
whichfinds it interactively — yet the script cannot. - A pipeline or
&&chain stops partway, or worse, continues as if nothing failed because the exit code was ignored. - A typo’d or renamed variable expands to an empty or unexpected word that the shell tries to execute.
- The error names something that looks like an argument, not a command (a quoting or line-continuation bug).
Common Root Causes
- Binary not installed — the tool genuinely isn’t on this host (common on fresh CI runners and minimal containers).
- Reduced PATH in non-interactive shells — cron and systemd start with a minimal
PATH(often just/usr/bin:/bin), so/usr/local/bin,~/.local/bin, or a language version manager’s shims are missing. - Typo or wrong casing —
Dockervsdocker,pyhtonvspython. - Relative/local script not on PATH — running
myscriptinstead of./myscriptwhen.is not (and should not be) onPATH. - A variable expands to nothing —
$CMDis unset (typo, orset -unot enabled), so the line becomes an empty or partial command. - Broken line continuation — a trailing space after a
\, so the next line’s first word is parsed as a standalone command. - A function or alias exists only in the interactive shell — aliases and functions defined in
~/.bashrcare not available to non-interactive scripts. - Wrong architecture or missing loader — the file exists and is executable but the dynamic loader is missing, which can surface as
not foundon some systems.
Diagnostic Workflow
First, ask the shell how it resolves the name. type shows whether it’s a builtin, function, alias, or a PATH lookup:
type -a kubectl # every match: alias, function, and each PATH hit
command -v kubectl # the single path that would actually run, or nothing
which -a kubectl # PATH matches only (external tool; less reliable than type)
If command -v prints nothing, the shell genuinely cannot find it. Compare your interactive PATH with the one the script actually runs under:
echo "$PATH" # your login shell
env -i PATH=/usr/bin:/bin ./deploy.sh # simulate a minimal cron-like PATH
Reproduce the exact cron environment, which is the usual culprit:
# Add this temporarily to a crontab to capture the real environment:
* * * * * env > /tmp/cron.env 2>&1
Then diff /tmp/cron.env against env from your shell — you will almost always find PATH is shorter under cron.
Confirm the binary exists somewhere and is executable:
ls -l "$(command -v kubectl)" 2>/dev/null || sudo find / -name kubectl -type f 2>/dev/null
Catch the empty-variable case by turning unset variables into hard errors:
set -u # referencing an unset variable now aborts instead of running a truncated command
Example Root Cause Analysis
A nightly backup worked by hand but failed every night under cron. The log showed:
/home/deploy/backup.sh: line 9: aws: command not found
Line 9 was aws s3 sync .... Interactively, command -v aws returned /home/deploy/.local/bin/aws — a pip install --user install. The fix required understanding why cron differed:
# Interactive shell:
$ echo "$PATH"
/home/deploy/.local/bin:/usr/local/bin:/usr/bin:/bin
# Captured cron environment (/tmp/cron.env):
PATH=/usr/bin:/bin
Cron never sourced ~/.profile, so ~/.local/bin was absent from PATH and aws was unreachable. Two robust fixes: set an explicit PATH at the top of the script, or call the binary by absolute path.
#!/usr/bin/env bash
set -euo pipefail
export PATH="/home/deploy/.local/bin:/usr/local/bin:/usr/bin:/bin"
aws s3 sync /data s3://backups/nightly/
The set -euo pipefail matters just as much as the PATH: without set -e, the failed aws line returned non-zero and the script blithely continued to its “backup complete” log line, hiding the failure for weeks.
Prevention Best Practices
- Set an explicit
PATHat the top of every script that runs under cron/systemd, or invoke critical tools by absolute path. - Always use strict mode —
set -euo pipefail— so acommand not found(exit 127) actually aborts the script instead of being ignored. - Preflight your dependencies — early in the script, loop over required commands and fail fast with a clear message:
for cmd in kubectl aws jq; do
command -v "$cmd" >/dev/null 2>&1 || { echo "Missing required command: $cmd" >&2; exit 1; }
done
- Run ShellCheck — it flags unquoted expansions, typos, and line-continuation bugs that produce phantom commands.
- Don’t rely on aliases/functions from
.bashrcin scripts; define what you need inside the script. - Pin tool installs in CI — install dependencies explicitly in the pipeline rather than assuming the runner image has them.
Quick Command Reference
type -a NAME # how the shell resolves NAME (builtin/func/alias/path)
command -v NAME # the exact path that would run, or nothing
echo "$PATH" # inspect the current search path
env -i PATH=/usr/bin:/bin CMD # test under a minimal PATH
sudo find / -name NAME -type f 2>/dev/null # locate the binary anywhere
set -u # make unset variables fatal
command -v NAME >/dev/null || exit 1 # dependency preflight
Conclusion
command not found (exit code 127) almost always means one of three things: the binary isn’t installed, it isn’t on the $PATH the script actually runs under, or a typo/empty variable produced a word that was never a command. Diagnose by asking the shell directly with type -a and command -v, and by capturing the real cron/CI environment rather than trusting your interactive shell. Prevent recurrence with an explicit PATH, strict mode so failures abort loudly, and a dependency preflight that fails fast with a message a human can act on.
Fixed it? Get 500 Bash & Python Automation & DevOps AI prompts — free
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.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
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.