command_not_found_handle
[command_not_found_handle] is the WAY to deal with 'command not found' issues in BASH
Problem
WHEN you have code in BASH that is missing function definition.
main() {
echo_with_pid "START"
i_dont_exist hi1 hi2
echo_with_pid "DONE"
}
THEN by default BASH is going to run over the missing function, by just printing an error. It will not halt.
[$$=19608/19608] START
/tmp/scratch.sh: line 17: i_dont_exist: command not found
[$$=19608/19608] DONE
Solution: [command_not_found_handle] + Interrupting
Manpage
From the bash manpage we have the following documentation
If the name is neither a shell function nor a builtin, and contains no slashes, bash searches each element of the PATH for a directory containing an executable file by that name. Bash uses a hash table to remember the full pathnames of executable files (see hash under SHELL BUILTIN COMMANDS below). A full search of the directories in PATH is performed only if the command is not found in the hash table. If the search is unsuccessful, the shell searches for a defined shell function named command_not_found_handle. If that function exists, it is invoked in a separate execution environment with the original command and the original command's arguments as its arguments, and the function's exit status becomes the exit status of that subshell. If that function is not defined, the shell prints an error message and returns an exit status of 127. - bash manpage
Simple exit does NOT work with command_not_found_handle
⚠️ Simple exit in command_not_found_handle will not work
The catch is that command_not_found_handle will run in separate execution.
So even when we add the command_not_found_handle with an exit
#!/usr/bin/env bash
echo_with_pid(){
echo "[\$\$=$$/$BASHPID] $*"
}
command_not_found_handle() {
echo_with_pid "command_not_found_handle invoked with arguments=[$*]"
exit 127
}
main() {
echo_with_pid "START"
i_dont_exist hi1 hi2
echo_with_pid "DONE"
}
main "${@}"
Output:
[$$=21132/21132] START
[$$=21132/21133] command_not_found_handle invoked with arguments=[i_dont_exist hi1 hi2]
[$$=21132/21132] DONE
We still end up running over the command not found issue. Albeit at least now we have a piece of code that executes on this event.
We can see by $BASHPID value 21133 that command_not_found_handle ran in a separate process, then our main function. So when we call exit in command_not_found_handle we are exiting process 21133 NOT 21132, which is running our main function.
Use interrupt with command_not_found_handle
👍 Use interrupt with command_not_found_handle - for your Terminal Bash Env👍
While regular exit will not work from the child process that command_not_found_handle runs, interrupting the $$ process will.
Below is how the handle looks like that will halt the main when functions are NOT found.
#!/usr/bin/env bash
echo_with_pid(){
echo "[\$\$=$$/$BASHPID] $*"
}
# 🏁 This handler will prevent 'not found commands' from continuing scripts in BASH 🏁
command_not_found_handle() {
echo "command_not_found_handle: invoked with arguments=[$*]."
echo "command_not_found_handle: Interrupting process group [$$]."
# Send an interrupt signal (SIGINT) to the entire process group
# of the current shell session
#
# Breakdown:
# - kill: command to send signals to processes
# - -INT: the interrupt signal (same as Ctrl+C)
# - -$$: negative PID targets the process group
# - $$: shell variable containing current shell's PID
# - negative sign: targets entire process group, not just single process
kill -INT -$$
}
# Export this handler so it's available in:
# Child bash scripts that your script executes (e.g., ./some_script.sh)
#
# Without export: Only THIS script uses the handler
# With export: ALL child bash processes inherit and use this handler
export -f command_not_found_handle
main() {
echo_with_pid "START"
i_dont_exist hi1 hi2
echo_with_pid "DONE"
}
main "${@}"
Output is below, where we can see that 'DONE' was prevented from running.
[$$=23624/23624] START
[$$=23624/23625] command_not_found_handle: invoked with arguments=[i_dont_exist hi1 hi2].
[$$=23624/23625] command_not_found_handle: Interrupting process group [23624].
Interrupting and CI/CD
Important: This interrupt-based approach works well for interactive terminal use, but should not be used in CI/CD pipelines (e.g., Jenkins (CI/CD)). The interrupt signal can terminate your CI/CD runner unexpectedly, causing what appears to be a crash without clear error messages.
Alternative for CI/CD: Instead of interrupting, have command_not_found_handle log errors to a file. Then check this log file either:
- At the end of your script, or
- Before executing the next step
This ensures errors are visible without risking runner termination.
Works with non-zero branches
command_not_found_handle works when there are non zero branches ([set -e] does NOT)
Contrary to set -e, command_not_found_handle WORKs when there is non zero branch present.
set -e flag gets confused if some function up the chain has success or error branch. See: ❌ Checking for failure/success, even up the chain prevents 'set -e' from triggering.❌.
Example where ERROR checking is up the chain
#!/usr/bin/env bash
command_not_found_handle() {
# Exit
echo_with_pid "command_not_found_handle: invoked with arguments=[$*]."
echo_with_pid "command_not_found_handle: Interrupting process group [$$]."
# Send an interrupt signal (SIGINT) to the entire process group
# of the current shell session
#
# Breakdown:
# - kill: command to send signals to processes
# - -INT: the interrupt signal (same as Ctrl+C)
# - -$$: negative PID targets the process group
# - $$: shell variable containing current shell's PID
# - negative sign: targets entire process group, not just single process
kill -INT -$$
}
echo_with_pid(){
echo "[\$\$=$$/$BASHPID] $*"
}
foo2() {
echo_with_pid "foo2-enter"
i_dont_exist
echo_with_pid "foo2-exit"
}
foo1() {
echo_with_pid "foo1-enter"
foo2
echo_with_pid "foo1-exit"
}
main() {
echo_with_pid "START"
foo1
echo_with_pid "DONE"
}
if main; then
echo "Main succeeded"
else
echo "Main failed"
fi
As we would like, the handler get's called which allows to intercept.
❯/tmp/scratch.sh
[$$=39900/39900] START
[$$=39900/39900] foo1-enter
[$$=39900/39900] foo2-enter
[$$=39900/39901] command_not_found_handle: invoked with arguments=[i_dont_exist].
[$$=39900/39901] command_not_found_handle: Interrupting process group [39900].
The handler even works when we have non-zero branch right on the non existing function
#!/usr/bin/env bash
command_not_found_handle() {
echo_with_pid "command_not_found_handle: invoked with arguments=[$*]."
echo_with_pid "command_not_found_handle: Interrupting process group [$$]."
kill -INT -$$
}
echo_with_pid(){
echo "[\$\$=$$/$BASHPID] $*"
}
main() {
echo_with_pid "START"
i_dont_exist b-1 b-2 || echo "If it doesnt exist"
echo_with_pid "DONE"
}
main "${@}" || exit 1
Output:
❯/tmp/scratch.sh
[$$=42511/42511] START
[$$=42511/42512] command_not_found_handle: invoked with arguments=[i_dont_exist b-1 b-2].
[$$=42511/42512] command_not_found_handle: Interrupting process group [42511].
Gotcha: scripts without shebang
⚠️ Scripts without shebang do not properly respect command_not_found_handle.
The problem: without shebang handle does NOT work (fails to stop)
If you are relying on command_not_found_handle in your scripts. Make sure you are properly adding Shebang Hashbang to your scripts.
For example if you run the following example (without shebang):
main() {
echo "START-[\$\$=$$/$BASHPID]"
iDontExist
sleep 0.5
echo "FINISH-[\$\$=$$/$BASHPID]"
}
main "${@}"
You could be quite surprised to find out the following output, which shows that command_not_found_handle did trigger, BUT somehow the FINISH echo was allowed to execute:
START-[$$=2706051/2706051]
🤷 [$$=2706051/2706052] command_not_found_handle: invoked with arguments=['iDontExist'].
🤷 [$$=2706051/2706052] command_not_found_handle: Interrupting process group [2706051].
🤷 FunctionChain: [main:7 (scratch.sh)-->main:3 (scratch.sh)-->command_not_found_handle]
FINISH-[$$=2706051/2706051]
The fix: Add shebang
The fix is simple, add #!/usr/bin/env bash and it works as expected:
👍 Works as expected with shebang
#!/usr/bin/env bash
main() {
echo "START-[\$\$=$$/$BASHPID]"
iDontExist
sleep 0.5
echo "FINISH-[\$\$=$$/$BASHPID]"
}
main "${@}"
Output:
START-[$$=2707408/2707408]
🤷 [$$=2707408/2707409] command_not_found_handle: invoked with arguments=['iDontExist'].
🤷 [$$=2707408/2707409] command_not_found_handle: Interrupting process group [2707408].
🤷 FunctionChain: [main:10 (scratch.sh)-->main:5 (scratch.sh)-->command_not_found_handle]
FINISH echo is not reached.
Why it doesn't work.
At the time of writing I do not know why. Asking claude 4.5 sonnet gave some hallucination answers that were not correct.
Conclusion
command_not_found_handle handler with kill -INT -$$ allows us to create a safer bash environment & scripts without having to use set -e, which has its own set of challenges (more on set -e here)
Children
- Use Interrupt with Handle - for your Terminal Bash Env
- command_not_found_handle WORKs when there are non-zero branch
- simple Exit in Handle Will Not Work
- ⚠️ Scripts WITHOUT Shebang Do NOT Work with command_not_found_handle ⚠️
Backlinks