Fix initialization hook in the PROMPT_COMMAND array on Bash 5.1+

If PROMPT_COMMAND contains unrelated commands before installing
bash-preexec, then these unrelated commands may trigger
`__bp_preexec_invoke_exec`, which causes interactive mode marker to be
unset.  This causes preexec to be missed for the first interactive
command, since bash-preexec assumes that we are still not interactive.

Example of what can trigger such behavior: on Arch Linux in a login
shell /etc/profile.d/80-systemd-osc-context.sh causes
`__systemd_osc_context_precmdline` to be added to PROMPT_COMMAND, and
`__systemd_osc_context_precmdline` trips
`__bp_preexec_interactive_mode`.

This patch avoids the issue by appending our initialization hook as
the last element of PROMPT_COMMAND in Bash >= 5.1, instead of
appending the hook to the first element of PROMPT_COMMAND.
This commit is contained in:
Platon Pronko 2026-03-30 13:47:32 +04:00 committed by Koichi Murase
parent fdd5ba732f
commit 5d8ba5dd9c
2 changed files with 83 additions and 14 deletions

View file

@ -542,14 +542,18 @@ __bp_install_after_session_init() {
__bp_require_not_readonly PS0 || return
fi
local sanitized_prompt_command
__bp_sanitize_string sanitized_prompt_command "${PROMPT_COMMAND:-}"
if [[ -n "$sanitized_prompt_command" ]]; then
# shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0
PROMPT_COMMAND=${sanitized_prompt_command}$'\n'
if __bp_use_array_prompt_command; then
PROMPT_COMMAND+=("${__bp_install_string}")
else
local sanitized_prompt_command
__bp_sanitize_string sanitized_prompt_command "${PROMPT_COMMAND:-}"
if [[ -n "$sanitized_prompt_command" ]]; then
# shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0
PROMPT_COMMAND=${sanitized_prompt_command}$'\n'
fi
# shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0
PROMPT_COMMAND+=${__bp_install_string}
fi
# shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0
PROMPT_COMMAND+=${__bp_install_string}
}
# Run our install so long as we're not delaying it.

View file

@ -76,13 +76,28 @@ set_exit_code_and_run_precmd() {
# Assert that before running, the command contains the install string, and
# afterwards it does not
[[ "$PROMPT_COMMAND" == *"$__bp_install_string"* ]] || return 1
[[ "$(join_PROMPT_COMMAND)" == *"$__bp_install_string"* ]] || return 1
eval_PROMPT_COMMAND
[[ "$PROMPT_COMMAND" != *"$__bp_install_string"* ]] || return 1
[[ "$(join_PROMPT_COMMAND)" != *"$__bp_install_string"* ]] || return 1
}
@test "__bp_install should remove trap logic and itself from modified PROMPT_COMMAND" {
PROMPT_COMMAND=()
__bp_install_after_session_init
PROMPT_COMMAND="$PROMPT_COMMAND; true"
# Assert that before running, the command contains the install string, and
# afterwards it does not
[[ "$(join_PROMPT_COMMAND)" == *"$__bp_install_string"* ]] || return 1
eval_PROMPT_COMMAND
[[ "$(join_PROMPT_COMMAND)" != *"$__bp_install_string"* ]] || return 1
}
@test "__bp_install should preserve an existing DEBUG trap" {
trap_invoked_count=0
foo() { (( trap_invoked_count += 1 )); }
@ -239,7 +254,9 @@ set_exit_code_and_run_precmd() {
@test "Appending or prepending to PROMPT_COMMAND should work after bp_install_after_session_init" {
__bp_install_after_session_init
nl=$'\n'
PROMPT_COMMAND="$PROMPT_COMMAND; true"
# On Bash 5.1+ we append to the array properly,
# so first element of PROMPT_COMMAND may still be empty.
PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND; }true"
PROMPT_COMMAND="$PROMPT_COMMAND $nl true"
PROMPT_COMMAND="$PROMPT_COMMAND; true"
PROMPT_COMMAND="true; $PROMPT_COMMAND"
@ -249,6 +266,24 @@ set_exit_code_and_run_precmd() {
eval_PROMPT_COMMAND
}
@test "Appending or prepending to PROMPT_COMMAND array should work after bp_install_after_session_init" {
if __bp_use_array_prompt_command; then
PROMPT_COMMAND=()
__bp_install_after_session_init
nl=$'\n'
PROMPT_COMMAND=("${PROMPT_COMMAND[@]}" "true")
PROMPT_COMMAND=("${PROMPT_COMMAND[@]}" "$nl true")
PROMPT_COMMAND=("${PROMPT_COMMAND[@]}" "true")
PROMPT_COMMAND=("true" "${PROMPT_COMMAND[@]}")
PROMPT_COMMAND=("true" "${PROMPT_COMMAND[@]}")
PROMPT_COMMAND=("true" "${PROMPT_COMMAND[@]}")
PROMPT_COMMAND=("true $nl" "${PROMPT_COMMAND[@]}")
eval_PROMPT_COMMAND
else
skip
fi
}
# Case where a user is appending or prepending to PROMPT_COMMAND.
# This can happen after 'source bash-preexec.sh' e.g.
# source bash-preexec.sh; PROMPT_COMMAND="$PROMPT_COMMAND; other_prompt_command_hook"
@ -268,7 +303,11 @@ set_exit_code_and_run_precmd() {
@test "Adding to PROMPT_COMMAND after with semicolon" {
PROMPT_COMMAND="echo before"
__bp_install_after_session_init
PROMPT_COMMAND="$PROMPT_COMMAND; echo after"
if __bp_use_array_prompt_command; then
PROMPT_COMMAND[${#PROMPT_COMMAND[@]}-1]+='; echo after'
else
PROMPT_COMMAND+='; echo after'
fi
eval_PROMPT_COMMAND
@ -285,12 +324,38 @@ set_exit_code_and_run_precmd() {
precmd() { echo "inside precmd"; }
run eval_PROMPT_COMMAND
[ "${#lines[@]}" == '5' ]
[ "${lines[0]}" == "after2" ]
[ "${lines[1]}" == "before" ]
[ "${lines[2]}" == "before2" ]
[ "${lines[3]}" == "inside precmd" ]
[ "${lines[4]}" == "after" ]
[ "${#lines[@]}" == '5' ]
if __bp_use_array_prompt_command; then
[ "${lines[3]}" == "after" ]
[ "${lines[4]}" == "inside precmd" ]
else
[ "${lines[3]}" == "inside precmd" ]
[ "${lines[4]}" == "after" ]
fi
}
@test "during install PROMPT_COMMAND and precmd functions should be executed each once (Bash 5.1+ PROMPT_COMMAND array)" {
if __bp_use_array_prompt_command; then
PROMPT_COMMAND=("echo before")
PROMPT_COMMAND=("${PROMPT_COMMAND[@]}" "echo before2")
__bp_install_after_session_init
PROMPT_COMMAND=("${PROMPT_COMMAND[@]}" "echo after")
PROMPT_COMMAND=("echo after2" "${PROMPT_COMMAND[@]}")
precmd() { echo "inside precmd"; }
run eval_PROMPT_COMMAND
[ "${#lines[@]}" == '5' ]
[ "${lines[0]}" == "after2" ]
[ "${lines[1]}" == "before" ]
[ "${lines[2]}" == "before2" ]
[ "${lines[3]}" == "inside precmd" ]
[ "${lines[4]}" == "after" ]
else
skip
fi
}
@test "No functions defined for preexec should simply return" {