@@ -71,7 +71,11 @@ __bp_inside_precmd=0
7171__bp_inside_preexec=0
7272
7373# Initial PROMPT_COMMAND string that is removed from PROMPT_COMMAND post __bp_install
74- __bp_install_string=$' __bp_trap_string="$(trap -p DEBUG)"\n trap - DEBUG\n __bp_install'
74+ bash_preexec_install_string=$' __bp_trap_string="$(trap -p DEBUG)"\n trap - DEBUG\n __bp_install'
75+
76+ # The command string that is registered to the DEBUG trap.
77+ # shellcheck disable=SC2016
78+ bash_preexec_trapdebug_string=' __bp_preexec_invoke_exec "$_"'
7579
7680# Fails if any of the given variables are readonly
7781# Reference https://stackoverflow.com/a/4441178
@@ -157,21 +161,38 @@ __bp_precmd_invoke_cmd() {
157161 return
158162 fi
159163 local __bp_inside_precmd=1
164+ bash_preexec_invoke_precmd_functions " $__bp_last_ret_value " " $__bp_last_argument_prev_command "
165+
166+ __bp_set_ret_value " $__bp_last_ret_value " " $__bp_last_argument_prev_command "
167+ }
160168
169+ # This function invokes every function defined in our function array
170+ # "precmd_function". This function receives the arguments $1 and $2 for $? and
171+ # $_, respectively, that will be set for the precmd functions. This function
172+ # returns the last non-zero exit status of the hook functions. If there is no
173+ # error, this function returns 0.
174+ bash_preexec_invoke_precmd_functions () {
175+ local lastexit=$1 lastarg=$2
161176 # Invoke every function defined in our function array.
162177 local precmd_function
178+ local precmd_function_ret_value
179+ local precmd_ret_value=0
163180 for precmd_function in " ${precmd_functions[@]} " ; do
164181
165182 # Only execute this function if it actually exists.
166183 # Test existence of functions with: declare -[Ff]
167184 if type -t " $precmd_function " 1> /dev/null; then
168- __bp_set_ret_value " $__bp_last_ret_value " " $__bp_last_argument_prev_command "
185+ __bp_set_ret_value " $lastexit " " $lastarg "
169186 # Quote our function invocation to prevent issues with IFS
170187 " $precmd_function "
188+ precmd_function_ret_value=$?
189+ if [[ " $precmd_function_ret_value " != 0 ]]; then
190+ precmd_ret_value=" $precmd_function_ret_value "
191+ fi
171192 fi
172193 done
173194
174- __bp_set_ret_value " $__bp_last_ret_value "
195+ __bp_set_ret_value " $precmd_ret_value "
175196}
176197
177198# Sets a return value in $?. We may want to get access to the $? variable in our
@@ -260,7 +281,27 @@ __bp_preexec_invoke_exec() {
260281 return
261282 fi
262283
263- # Invoke every function defined in our function array.
284+ bash_preexec_invoke_preexec_functions " ${__bp_last_ret_value:- } " " $__bp_last_argument_prev_command " " $this_command "
285+ local preexec_ret_value=$?
286+
287+ # Restore the last argument of the last executed command, and set the return
288+ # value of the DEBUG trap to be the return code of the last preexec function
289+ # to return an error.
290+ # If `extdebug` is enabled a non-zero return value from any preexec function
291+ # will cause the user's command not to execute.
292+ # Run `shopt -s extdebug` to enable
293+ __bp_set_ret_value " $preexec_ret_value " " $__bp_last_argument_prev_command "
294+ }
295+
296+ # This function invokes every function defined in our function array
297+ # "preexec_function". This function receives the arguments $1 and $2 for $?
298+ # and $_, respectively, that will be set for the preexec functions. The third
299+ # argument $3 specifies the user command that is going to be executed
300+ # (corresponding to BASH_COMMAND in the DEBUG trap). This function returns the
301+ # last non-zero exit status from the preexec functions. If there is no error,
302+ # this function returns `0`.
303+ bash_preexec_invoke_preexec_functions () {
304+ local lastexit=$1 lastarg=$2 this_command=$3
264305 local preexec_function
265306 local preexec_function_ret_value
266307 local preexec_ret_value=0
@@ -269,7 +310,7 @@ __bp_preexec_invoke_exec() {
269310 # Only execute each function if it actually exists.
270311 # Test existence of function with: declare -[fF]
271312 if type -t " $preexec_function " 1> /dev/null; then
272- __bp_set_ret_value " ${__bp_last_ret_value :- } "
313+ __bp_set_ret_value " $lastexit " " $lastarg "
273314 # Quote our function invocation to prevent issues with IFS
274315 " $preexec_function " " $this_command "
275316 preexec_function_ret_value=" $? "
@@ -278,14 +319,7 @@ __bp_preexec_invoke_exec() {
278319 fi
279320 fi
280321 done
281-
282- # Restore the last argument of the last executed command, and set the return
283- # value of the DEBUG trap to be the return code of the last preexec function
284- # to return an error.
285- # If `extdebug` is enabled a non-zero return value from any preexec function
286- # will cause the user's command not to execute.
287- # Run `shopt -s extdebug` to enable
288- __bp_set_ret_value " $preexec_ret_value " " $__bp_last_argument_prev_command "
322+ __bp_set_ret_value " $preexec_ret_value "
289323}
290324
291325__bp_install () {
@@ -294,7 +328,8 @@ __bp_install() {
294328 return 1;
295329 fi
296330
297- trap ' __bp_preexec_invoke_exec "$_"' DEBUG
331+ # shellcheck disable=SC2064
332+ trap " $bash_preexec_trapdebug_string " DEBUG
298333
299334 # Preserve any prior DEBUG trap as a preexec function
300335 local prior_trap
@@ -327,7 +362,7 @@ __bp_install() {
327362 # Remove setting our trap install string and sanitize the existing prompt command string
328363 existing_prompt_command=" ${PROMPT_COMMAND:- } "
329364 # Edge case of appending to PROMPT_COMMAND
330- existing_prompt_command=" ${existing_prompt_command// $__bp_install_string /: } " # no-op
365+ existing_prompt_command=" ${existing_prompt_command// $bash_preexec_install_string /: } " # no-op
331366 existing_prompt_command=" ${existing_prompt_command// $' \n ' : $' \n ' / $' \n ' } " # remove known-token only
332367 existing_prompt_command=" ${existing_prompt_command// $' \n ' : ;/ $' \n ' } " # remove known-token only
333368 __bp_sanitize_string existing_prompt_command " $existing_prompt_command "
@@ -346,10 +381,13 @@ __bp_install() {
346381 PROMPT_COMMAND+=$' \n __bp_interactive_mode'
347382 fi
348383
349- # Add two functions to our arrays for convenience
350- # of definition.
351- precmd_functions+=(precmd)
352- preexec_functions+=(preexec)
384+ # Add two functions to our arrays for convenience of definition only when
385+ # the functions have not yet added.
386+ if [[ ! ${__bp_installed_convenience_functions-} ]]; then
387+ __bp_installed_convenience_functions=1
388+ precmd_functions+=(precmd)
389+ preexec_functions+=(preexec)
390+ fi
353391
354392 # Invoke our two functions manually that were added to $PROMPT_COMMAND
355393 __bp_precmd_invoke_cmd
@@ -371,8 +409,46 @@ __bp_install_after_session_init() {
371409 PROMPT_COMMAND=${sanitized_prompt_command} $' \n '
372410 fi ;
373411 # shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0
374- PROMPT_COMMAND+=${__bp_install_string}
412+ PROMPT_COMMAND+=${bash_preexec_install_string}
413+ }
414+
415+ # Remove hooks installed in the DEBUG trap and PROMPT_COMMAND.
416+ bash_preexec_uninstall () {
417+ # Remove __bp_install hook from PROMPT_COMMAND
418+ # shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0
419+ if [[ ${PROMPT_COMMAND-} == * " $bash_preexec_install_string " * ]]; then
420+ PROMPT_COMMAND=" ${PROMPT_COMMAND// ${bash_preexec_install_string} [;$'\n']} " # Edge case of appending to PROMPT_COMMAND
421+ PROMPT_COMMAND=" ${PROMPT_COMMAND// $bash_preexec_install_string } "
422+ fi
423+
424+ # Remove precmd hook from PROMPT_COMMAND
425+ local i prompt_command
426+ for i in " ${! PROMPT_COMMAND[@]} " ; do
427+ prompt_command=${PROMPT_COMMAND[i]}
428+ case $prompt_command in
429+ __bp_precmd_invoke_cmd | __bp_interactive_mode)
430+ prompt_command= ;;
431+ * )
432+ prompt_command=${prompt_command/# $' __bp_precmd_invoke_cmd\n ' / $' \n ' }
433+ prompt_command=${prompt_command% $' \n __bp_interactive_mode' }
434+ prompt_command=${prompt_command# $' \n ' }
435+ esac
436+ PROMPT_COMMAND[i]=$prompt_command
437+ done
438+
439+ # Remove preexec hook in the DEBUG trap
440+ local q=" '" Q=" '\''"
441+ if [[ $( trap -p DEBUG) == " trap -- '${bash_preexec_trapdebug_string// $q / $Q } ' DEBUG" ]]; then
442+ if [[ ${__bp_trap_string-} ]]; then
443+ eval -- " $__bp_trap_string "
444+ else
445+ trap - DEBUG
446+ fi
447+ fi
375448}
449+ # Note: We need to add "trace" attribute to the function so that "trap - DEBUG"
450+ # inside the function takes an effect.
451+ declare -ft bash_preexec_uninstall
376452
377453# Run our install so long as we're not delaying it.
378454if [[ -z " ${__bp_delay_install:- } " ]]; then
0 commit comments