mirror of
https://github.com/rcaloras/bash-preexec.git
synced 2026-11-03 22:49:41 -09:00
Merge pull request #143 from akinomyoga/adjust-PROMPT_COMMAND
Re-adjust `PROMPT_COMMAND` when `PROMPT_COMMAND` is modified elsewhere
This commit is contained in:
commit
23011bb11b
2 changed files with 187 additions and 25 deletions
155
bash-preexec.sh
155
bash-preexec.sh
|
|
@ -71,6 +71,7 @@ __bp_inside_precmd=0
|
|||
__bp_inside_preexec=0
|
||||
|
||||
# Initial PROMPT_COMMAND string that is removed from PROMPT_COMMAND post __bp_install
|
||||
# shellcheck disable=SC2016
|
||||
__bp_install_string='__bp_install "$_"'
|
||||
|
||||
# Fails if any of the given variables are readonly
|
||||
|
|
@ -121,21 +122,93 @@ __bp_trim_whitespace() {
|
|||
|
||||
|
||||
# Trims whitespace and removes any leading or trailing semicolons from $2 and
|
||||
# writes the resulting string to the variable name passed as $1. Used for
|
||||
# manipulating substrings in PROMPT_COMMAND
|
||||
# writes the resulting string to the variable name passed as $1. This also
|
||||
# removes the no-op colons, which are converted from the hooks to remove. Used
|
||||
# for manipulating substrings in PROMPT_COMMAND
|
||||
__bp_sanitize_string() {
|
||||
local var=${1:?} text=${2:-} sanitized
|
||||
__bp_trim_whitespace sanitized "$text"
|
||||
local var=${1:?} sanitized=${2:-}
|
||||
|
||||
local unset_extglob=
|
||||
if ! shopt -q extglob; then
|
||||
unset_extglob=yes
|
||||
shopt -s extglob
|
||||
fi
|
||||
|
||||
# We specify newline character through the variable `nl' because $'\n'
|
||||
# inside "${var//...}" is treated literally as "\$'\\n'" when `extquote' is
|
||||
# unset (shopt -u extquote). (Note: Bash 5.2's extquote seems to be buggy.)
|
||||
local tmp sp=$' \t' nl=$'\n'
|
||||
while
|
||||
# Quoting parameter expansions $nl in PAT of ${var//PAT/REP} is
|
||||
# required by shellcheck. On the other hand, we should not quote the
|
||||
# parameter expansions $nl in REP because the quotes will remain in the
|
||||
# replaced result with `shopt -s compat42'.
|
||||
tmp="${sanitized//[";$nl"]*(["$sp"]):*(["$sp"])[";$nl"]/$nl}"
|
||||
[[ "$tmp" != "$sanitized" ]]
|
||||
do
|
||||
sanitized="$tmp"
|
||||
done
|
||||
sanitized="${sanitized#:*(["$sp"])[";$nl"]}"
|
||||
sanitized="${sanitized%[";$nl"]*(["$sp"]):}"
|
||||
__bp_trim_whitespace sanitized "$sanitized"
|
||||
sanitized=${sanitized%;}
|
||||
sanitized=${sanitized#;}
|
||||
__bp_trim_whitespace sanitized "$sanitized"
|
||||
if [[ "$sanitized" == ":" ]]; then
|
||||
sanitized=
|
||||
fi
|
||||
printf -v "$var" '%s' "$sanitized"
|
||||
|
||||
if [[ -n "$unset_extglob" ]]; then
|
||||
shopt -u extglob
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# Bash >= 5.1 supports the array version of PROMPT_COMMAND.
|
||||
__bp_use_array_prompt_command() {
|
||||
(( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) ))
|
||||
}
|
||||
|
||||
|
||||
# Remove $1 and sanitize each elements of PROMPT_COMMAND. We want to keep
|
||||
# PROMPT_COMMAND scalar in bash < 5.1 because some configuration tests the
|
||||
# support for the array PROMPT_COMMAND by checking the array attribute of
|
||||
# PROMPT_COMMAND.
|
||||
__bp_remove_command_from_prompt_command() {
|
||||
local removed_command="${1-}"
|
||||
if __bp_use_array_prompt_command; then
|
||||
local i sanitized_prompt_command
|
||||
for i in "${!PROMPT_COMMAND[@]}"; do
|
||||
sanitized_prompt_command="${PROMPT_COMMAND[i]:-}"
|
||||
sanitized_prompt_command="${sanitized_prompt_command//"$removed_command"/:}"
|
||||
__bp_sanitize_string sanitized_prompt_command "$sanitized_prompt_command"
|
||||
if [[ -n "$sanitized_prompt_command" ]]; then
|
||||
PROMPT_COMMAND[i]="$sanitized_prompt_command"
|
||||
else
|
||||
unset -v 'PROMPT_COMMAND[i]'
|
||||
fi
|
||||
done
|
||||
else
|
||||
local sanitized_prompt_command="${PROMPT_COMMAND:-}"
|
||||
sanitized_prompt_command="${sanitized_prompt_command//"$removed_command"/:}" # no-op
|
||||
__bp_sanitize_string PROMPT_COMMAND "$sanitized_prompt_command"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# This function is installed as part of the PROMPT_COMMAND;
|
||||
# It sets a variable to indicate that the prompt was just displayed,
|
||||
# to allow the DEBUG trap to know that the next command is likely interactive.
|
||||
__bp_interactive_mode() {
|
||||
if [[ "${1-}" != "force" && ! "${BATS_VERSION-}" ]] && (( ${#FUNCNAME[*]} > 1 )); then
|
||||
# When this function is not called from the top level, the current
|
||||
# function call is probably performed via PROMPT_COMMAND saved by
|
||||
# another framework (e.g., starship). In this case, we do not want to
|
||||
# turn on the "interactive mode" here.
|
||||
return 0
|
||||
fi
|
||||
|
||||
__bp_preexec_interactive_mode="on"
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +231,19 @@ __bp_precmd_invoke_cmd() {
|
|||
if (( __bp_inside_precmd > 0 )); then
|
||||
return
|
||||
fi
|
||||
|
||||
# Check and adjust PROMPT_COMMAND to make sure that PROMPT_COMMAND has the
|
||||
# form "__bp_precmd_invoke_cmd; ...; __bp_interactive_mode"
|
||||
if ! __bp_install_prompt_command && [[ ! "${BATS_VERSION-}" ]] && (( ${#FUNCNAME[*]} > 1 )); then
|
||||
# When PROMPT_COMMAND is already properly set up but this function is
|
||||
# not called from the top level, the current function call is probably
|
||||
# performed via PROMPT_COMMAND saved by another framework (e.g.,
|
||||
# starship). In this case, we do not need to invoke precmd because it
|
||||
# is supposed to be already processed by the top-level
|
||||
# __bp_precmd_invoke_cmd.
|
||||
return 0
|
||||
fi
|
||||
|
||||
local __bp_inside_precmd=1
|
||||
__bp_invoke_precmd_functions "$__bp_last_ret_value" "$__bp_last_argument_prev_command"
|
||||
|
||||
|
|
@ -378,34 +464,17 @@ fi
|
|||
__bp_install() {
|
||||
local lastexit=$? lastarg=$_
|
||||
# Exit if we already have this installed.
|
||||
# shellcheck disable=SC2016
|
||||
if [[ "${PROMPT_COMMAND[*]:-}" == *'__bp_precmd_invoke_cmd "$_"'* ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
"$__bp_hook_preexec_proc"
|
||||
|
||||
local existing_prompt_command
|
||||
# Remove setting our trap install string and sanitize the existing prompt command string
|
||||
existing_prompt_command="${PROMPT_COMMAND:-}"
|
||||
# Edge case of appending to PROMPT_COMMAND
|
||||
existing_prompt_command="${existing_prompt_command//$__bp_install_string/:}" # no-op
|
||||
existing_prompt_command="${existing_prompt_command//$'\n':$'\n'/$'\n'}" # remove known-token only
|
||||
existing_prompt_command="${existing_prompt_command//$'\n':;/$'\n'}" # remove known-token only
|
||||
__bp_sanitize_string existing_prompt_command "$existing_prompt_command"
|
||||
if [[ "${existing_prompt_command:-:}" == ":" ]]; then
|
||||
existing_prompt_command=
|
||||
fi
|
||||
__bp_remove_command_from_prompt_command "$__bp_install_string"
|
||||
|
||||
# Install our hooks in PROMPT_COMMAND to allow our trap to know when we've
|
||||
# actually entered something.
|
||||
PROMPT_COMMAND='__bp_precmd_invoke_cmd "$_"'
|
||||
PROMPT_COMMAND+=${existing_prompt_command:+$'\n'$existing_prompt_command}
|
||||
if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then
|
||||
PROMPT_COMMAND+=('__bp_interactive_mode')
|
||||
else
|
||||
# shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0
|
||||
PROMPT_COMMAND+=$'\n__bp_interactive_mode'
|
||||
fi
|
||||
__bp_install_prompt_command || true
|
||||
|
||||
# Add two functions to our arrays for convenience
|
||||
# of definition.
|
||||
|
|
@ -415,7 +484,7 @@ __bp_install() {
|
|||
# Invoke our two functions manually that were added to $PROMPT_COMMAND
|
||||
__bp_set_ret_value "$lastexit" "$lastarg"
|
||||
__bp_precmd_invoke_cmd
|
||||
__bp_interactive_mode
|
||||
__bp_interactive_mode force
|
||||
}
|
||||
|
||||
# Note: We need to add the "trace" attribute to these functions so that "trap
|
||||
|
|
@ -423,6 +492,42 @@ __bp_install() {
|
|||
# effect even when there is an existing DEBUG trap.
|
||||
declare -ft __bp_install __bp_hook_preexec_into_debug
|
||||
|
||||
# Encloses PROMPT_COMMAND hooks within __bp_precmd_invoke_cmd and
|
||||
# __bp_interactive_mode. If all the PROMPT_COMMAND hooks are already surrounded
|
||||
# by __bp_precmd_invoke_cmd and __bp_interactive_mode, the function exits with
|
||||
# status 1.
|
||||
__bp_install_prompt_command() {
|
||||
local prompt_command="${PROMPT_COMMAND:-}"
|
||||
if __bp_use_array_prompt_command; then
|
||||
local IFS=$'\n'
|
||||
prompt_command="${PROMPT_COMMAND[*]:-}"
|
||||
IFS=$' \t\n'
|
||||
fi
|
||||
|
||||
# Exit if we already have a properly set-up hooks in PROMPT_COMMAND
|
||||
# shellcheck disable=SC2016
|
||||
local prologue='__bp_precmd_invoke_cmd "$_"'
|
||||
local epilogue='__bp_interactive_mode'
|
||||
if [[ "$prompt_command" == "$prologue"$'\n'*$'\n'"$epilogue" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
__bp_remove_command_from_prompt_command "$prologue"
|
||||
__bp_remove_command_from_prompt_command "$epilogue"
|
||||
|
||||
# Install our hooks in PROMPT_COMMAND to allow our trap to know when we've
|
||||
# actually entered something.
|
||||
# shellcheck disable=SC2128,SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0
|
||||
PROMPT_COMMAND=$prologue${PROMPT_COMMAND:+$'\n'$PROMPT_COMMAND}
|
||||
if __bp_use_array_prompt_command; then
|
||||
PROMPT_COMMAND+=("$epilogue")
|
||||
else
|
||||
# shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0
|
||||
PROMPT_COMMAND+=$'\n'$epilogue
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Sets an installation string as part of our PROMPT_COMMAND to install
|
||||
# after our session has started. This allows bash-preexec to be included
|
||||
# at any point in our bash profile.
|
||||
|
|
|
|||
|
|
@ -159,6 +159,30 @@ set_exit_code_and_run_precmd() {
|
|||
fi
|
||||
}
|
||||
|
||||
@test "__bp_install_prompt_command should adjust modified PROMPT_COMMAND" {
|
||||
unset -v PROMPT_COMMAND
|
||||
PROMPT_COMMAND="echo PREHOOK"
|
||||
|
||||
# First install
|
||||
__bp_install_prompt_command
|
||||
expected_result=$'__bp_precmd_invoke_cmd "$_"\necho PREHOOK\n__bp_interactive_mode'
|
||||
[ "$(join_PROMPT_COMMAND)" == "$expected_result" ]
|
||||
|
||||
# User modification
|
||||
if __bp_use_array_prompt_command; then
|
||||
PROMPT_COMMAND+=('echo POSTHOOK')
|
||||
else
|
||||
PROMPT_COMMAND+=$'\necho POSTHOOK'
|
||||
fi
|
||||
expected_result=$'__bp_precmd_invoke_cmd "$_"\necho PREHOOK\n__bp_interactive_mode\necho POSTHOOK'
|
||||
[ "$(join_PROMPT_COMMAND)" == "$expected_result" ]
|
||||
|
||||
# Re-adjust
|
||||
__bp_install_prompt_command
|
||||
expected_result=$'__bp_precmd_invoke_cmd "$_"\necho PREHOOK\necho POSTHOOK\n__bp_interactive_mode'
|
||||
[ "$(join_PROMPT_COMMAND)" == "$expected_result" ]
|
||||
}
|
||||
|
||||
@test "__bp_sanitize_string should remove semicolons and trim space" {
|
||||
|
||||
__bp_sanitize_string output " true1; "$'\n'
|
||||
|
|
@ -172,6 +196,39 @@ set_exit_code_and_run_precmd() {
|
|||
|
||||
}
|
||||
|
||||
@test "__bp_sanitize_string should remove no-op colons" {
|
||||
__bp_sanitize_string output ':'
|
||||
[ "$output" == "" ]
|
||||
|
||||
__bp_sanitize_string output $':\n:'
|
||||
[ "$output" == "" ]
|
||||
|
||||
__bp_sanitize_string output $':\n:;echo USER1'
|
||||
[ "$output" == "echo USER1" ]
|
||||
|
||||
__bp_sanitize_string output $'echo USER2\n:\necho USER3'
|
||||
expected_result=$'echo USER2\necho USER3'
|
||||
[ "$output" == "$expected_result" ]
|
||||
|
||||
__bp_sanitize_string output $'echo USER4;:;echo USER5'
|
||||
expected_result=$'echo USER4\necho USER5'
|
||||
[ "$output" == "$expected_result" ]
|
||||
|
||||
__bp_sanitize_string output $'echo USER6;:\necho USER7'
|
||||
expected_result=$'echo USER6\necho USER7'
|
||||
[ "$output" == "$expected_result" ]
|
||||
|
||||
__bp_sanitize_string output $':\n: ; echo USER8'
|
||||
[ "$output" == "echo USER8" ]
|
||||
|
||||
__bp_sanitize_string output $':\n: ; echo USER9'
|
||||
[ "$output" == "echo USER9" ]
|
||||
|
||||
__bp_sanitize_string output $'echo USER10 ; :\n: ; echo USER11'
|
||||
expected_result=$'echo USER10 \n echo USER11'
|
||||
[ "$output" == "$expected_result" ]
|
||||
}
|
||||
|
||||
@test "Appending to PROMPT_COMMAND should work after bp_install" {
|
||||
bp_install
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue