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

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

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.

Fix this by using Interrupt.

While regular exit will not work from the child process, interrupting the $$ process will.

This 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].

More examples

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)

#gem


Children
  1. Example Catching Not Even When There Is Failure Branch

Backlinks