Skip to content

Commit 020416e

Browse files
committed
Add APIs for external precmd/preexec integrations
* Rename public functions and variables as "bash_preexec_*" * Remove the compatibility variable name "__bp_install_string" * Add a note on the "trace" function attribute * Preserve the previous exit status and argument * Test the installation of convenience functions * Test "bash_preexec_uninstall"
1 parent 44a4406 commit 020416e

File tree

2 files changed

+122
-26
lines changed

2 files changed

+122
-26
lines changed

bash-preexec.sh

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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)"\ntrap - DEBUG\n__bp_install'
74+
bash_preexec_install_string=$'__bp_trap_string="$(trap -p DEBUG)"\ntrap - 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,7 +161,7 @@ __bp_precmd_invoke_cmd() {
157161
return
158162
fi
159163
local __bp_inside_precmd=1
160-
__bp_invoke_precmd_functions "$__bp_last_ret_value" "$__bp_last_argument_prev_command"
164+
bash_preexec_invoke_precmd_functions "$__bp_last_ret_value" "$__bp_last_argument_prev_command"
161165

162166
__bp_set_ret_value "$__bp_last_ret_value" "$__bp_last_argument_prev_command"
163167
}
@@ -167,7 +171,7 @@ __bp_precmd_invoke_cmd() {
167171
# $_, respectively, which will be set for each precmd function. This function
168172
# returns the last non-zero exit status of the hook functions. If there is no
169173
# error, this function returns 0.
170-
__bp_invoke_precmd_functions() {
174+
bash_preexec_invoke_precmd_functions() {
171175
local lastexit=$1 lastarg=$2
172176
# Invoke every function defined in our function array.
173177
local precmd_function
@@ -275,7 +279,7 @@ __bp_preexec_invoke_exec() {
275279
return
276280
fi
277281

278-
__bp_invoke_preexec_functions "${__bp_last_ret_value:-}" "$__bp_last_argument_prev_command" "$this_command"
282+
bash_preexec_invoke_preexec_functions "${__bp_last_ret_value:-}" "$__bp_last_argument_prev_command" "$this_command"
279283
local preexec_ret_value=$?
280284

281285
# Restore the last argument of the last executed command, and set the return
@@ -294,7 +298,7 @@ __bp_preexec_invoke_exec() {
294298
# (corresponding to BASH_COMMAND in the DEBUG trap). This function returns the
295299
# last non-zero exit status from the preexec functions. If there is no error,
296300
# this function returns `0`.
297-
__bp_invoke_preexec_functions() {
301+
bash_preexec_invoke_preexec_functions() {
298302
local lastexit=$1 lastarg=$2 this_command=$3
299303
local preexec_function
300304
local preexec_function_ret_value
@@ -322,7 +326,8 @@ __bp_install() {
322326
return 1
323327
fi
324328

325-
trap '__bp_preexec_invoke_exec "$_"' DEBUG
329+
# shellcheck disable=SC2064
330+
trap "$bash_preexec_trapdebug_string" DEBUG
326331

327332
# Preserve any prior DEBUG trap as a preexec function
328333
eval "local trap_argv=(${__bp_trap_string:-})"
@@ -353,7 +358,7 @@ __bp_install() {
353358
# Remove setting our trap install string and sanitize the existing prompt command string
354359
existing_prompt_command="${PROMPT_COMMAND:-}"
355360
# Edge case of appending to PROMPT_COMMAND
356-
existing_prompt_command="${existing_prompt_command//$__bp_install_string/:}" # no-op
361+
existing_prompt_command="${existing_prompt_command//$bash_preexec_install_string/:}" # no-op
357362
existing_prompt_command="${existing_prompt_command//$'\n':$'\n'/$'\n'}" # remove known-token only
358363
existing_prompt_command="${existing_prompt_command//$'\n':;/$'\n'}" # remove known-token only
359364
__bp_sanitize_string existing_prompt_command "$existing_prompt_command"
@@ -372,10 +377,13 @@ __bp_install() {
372377
PROMPT_COMMAND+=$'\n__bp_interactive_mode'
373378
fi
374379

375-
# Add two functions to our arrays for convenience
376-
# of definition.
377-
precmd_functions+=(precmd)
378-
preexec_functions+=(preexec)
380+
# Add two functions to our arrays for convenience of definition only when
381+
# the functions have not yet added.
382+
if [[ ! ${__bp_installed_convenience_functions-} ]]; then
383+
__bp_installed_convenience_functions=1
384+
precmd_functions+=(precmd)
385+
preexec_functions+=(preexec)
386+
fi
379387

380388
# Invoke our two functions manually that were added to $PROMPT_COMMAND
381389
__bp_precmd_invoke_cmd
@@ -397,8 +405,46 @@ __bp_install_after_session_init() {
397405
PROMPT_COMMAND=${sanitized_prompt_command}$'\n'
398406
fi
399407
# shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0
400-
PROMPT_COMMAND+=${__bp_install_string}
408+
PROMPT_COMMAND+=${bash_preexec_install_string}
409+
}
410+
411+
# Remove hooks installed in the DEBUG trap and PROMPT_COMMAND.
412+
bash_preexec_uninstall() {
413+
# Remove __bp_install hook from PROMPT_COMMAND
414+
# shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0
415+
if [[ ${PROMPT_COMMAND-} == *"$bash_preexec_install_string"* ]]; then
416+
PROMPT_COMMAND="${PROMPT_COMMAND//${bash_preexec_install_string}[;$'\n']}" # Edge case of appending to PROMPT_COMMAND
417+
PROMPT_COMMAND="${PROMPT_COMMAND//$bash_preexec_install_string}"
418+
fi
419+
420+
# Remove precmd hook from PROMPT_COMMAND
421+
local i prompt_command
422+
for i in "${!PROMPT_COMMAND[@]}"; do
423+
prompt_command=${PROMPT_COMMAND[i]}
424+
case $prompt_command in
425+
__bp_precmd_invoke_cmd | __bp_interactive_mode)
426+
prompt_command= ;;
427+
*)
428+
prompt_command=${prompt_command/#$'__bp_precmd_invoke_cmd\n'/$'\n'}
429+
prompt_command=${prompt_command%$'\n__bp_interactive_mode'}
430+
prompt_command=${prompt_command#$'\n'}
431+
esac
432+
PROMPT_COMMAND[i]=$prompt_command
433+
done
434+
435+
# Remove preexec hook in the DEBUG trap
436+
local q="'" Q="'\''"
437+
if [[ $(trap -p DEBUG) == "trap -- '${bash_preexec_trapdebug_string//$q/$Q}' DEBUG" ]]; then
438+
if [[ ${__bp_trap_string-} ]]; then
439+
eval -- "$__bp_trap_string"
440+
else
441+
trap - DEBUG
442+
fi
443+
fi
401444
}
445+
# Note: We need to add "trace" attribute to the function so that "trap - DEBUG"
446+
# inside the function takes an effect.
447+
declare -ft bash_preexec_uninstall
402448

403449
# Run our install so long as we're not delaying it.
404450
if [[ -z "${__bp_delay_install:-}" ]]; then

test/bash-preexec.bats

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ set_exit_code_and_run_precmd() {
7676

7777
# Assert that before running, the command contains the install string, and
7878
# afterwards it does not
79-
[[ "$PROMPT_COMMAND" == *"$__bp_install_string"* ]] || return 1
79+
[[ "$PROMPT_COMMAND" == *"$bash_preexec_install_string"* ]] || return 1
8080

8181
eval_PROMPT_COMMAND
8282

83-
[[ "$PROMPT_COMMAND" != *"$__bp_install_string"* ]] || return 1
83+
[[ "$PROMPT_COMMAND" != *"$bash_preexec_install_string"* ]] || return 1
8484
}
8585

8686
@test "__bp_install should preserve an existing DEBUG trap" {
@@ -123,6 +123,56 @@ set_exit_code_and_run_precmd() {
123123
(( trap_count_snapshot < trap_invoked_count ))
124124
}
125125

126+
@test "__bp_install should register convenience functions \"preexec\" and \"precmd\" only once" {
127+
precmd_functions=()
128+
preexec_functions=()
129+
__bp_install
130+
bash_preexec_uninstall
131+
__bp_install
132+
133+
count=0
134+
for hook in "${precmd_functions[@]}"; do
135+
if [[ "$hook" == precmd ]] ; then
136+
count=$((count+1))
137+
fi
138+
done
139+
[ "$count" == 1 ]
140+
141+
count=0
142+
for hook in "${preexec_functions[@]}"; do
143+
if [[ "$hook" == preexec ]] ; then
144+
count=$((count+1))
145+
fi
146+
done
147+
[ "$count" == 1 ]
148+
}
149+
150+
@test "bash_preexec_uninstall should remove the hooks in DEBUG and PROMPT_COMMAND" {
151+
__bp_install
152+
153+
q="'" Q="'\''"
154+
[[ "$(join_PROMPT_COMMAND)" == *"__bp_precmd_invoke_cmd"* ]] || return 1
155+
[[ "$(join_PROMPT_COMMAND)" == *"__bp_interactive_mode"* ]] || return 1
156+
[ "$(trap -p DEBUG)" == "trap -- '${bash_preexec_trapdebug_string//$q/$Q}' DEBUG" ]
157+
158+
bash_preexec_uninstall
159+
160+
q="'" Q="'\''"
161+
[[ "$(join_PROMPT_COMMAND)" != *"__bp_precmd_invoke_cmd"* ]] || return 1
162+
[[ "$(join_PROMPT_COMMAND)" != *"__bp_interactive_mode"* ]] || return 1
163+
[ "$(trap -p DEBUG)" != "trap -- '${bash_preexec_trapdebug_string//$q/$Q}' DEBUG" ]
164+
}
165+
166+
@test "bash_preexec_uninstall should remove the unprocessed __bp_install hook in PROMPT_COMMAND" {
167+
__bp_install_after_session_init
168+
169+
[[ "$PROMPT_COMMAND" == *"$bash_preexec_install_string"* ]]
170+
171+
bash_preexec_uninstall
172+
173+
[[ "$PROMPT_COMMAND" != *"$bash_preexec_install_string"* ]]
174+
}
175+
126176
@test "__bp_sanitize_string should remove semicolons and trim space" {
127177

128178
__bp_sanitize_string output " true1; "$'\n'
@@ -328,71 +378,71 @@ set_exit_code_and_run_precmd() {
328378
[ $status -eq 1 ]
329379
}
330380

331-
@test "__bp_invoke_precmd_functions should be transparent for \$? and \$_" {
381+
@test "bash_preexec_invoke_precmd_functions should be transparent for \$? and \$_" {
332382
tester1() { test1_lastexit=$? test1_lastarg=$_; }
333383
tester2() { test2_lastexit=$? test2_lastarg=$_; }
334384
precmd_functions=(tester1 tester2)
335385
trap - DEBUG # remove the Bats stack-trace trap so $_ doesn't get overwritten
336-
__bp_invoke_precmd_functions 111 'vxxJlwNx9VPJDA' || true
386+
bash_preexec_invoke_precmd_functions 111 'vxxJlwNx9VPJDA' || true
337387

338388
[ "$test1_lastexit" == 111 ]
339389
[ "$test1_lastarg" == 'vxxJlwNx9VPJDA' ]
340390
[ "$test2_lastexit" == 111 ]
341391
[ "$test2_lastarg" == 'vxxJlwNx9VPJDA' ]
342392
}
343393

344-
@test "__bp_invoke_precmd_functions returns the last non-zero exit status" {
394+
@test "bash_preexec_invoke_precmd_functions returns the last non-zero exit status" {
345395
tester1() { return 91; }
346396
tester2() { return 38; }
347397
tester3() { return 0; }
348398
precmd_functions=(tester1 tester2 tester3)
349399
status=0
350-
__bp_invoke_precmd_functions 1 'lastarg' || status=$?
400+
bash_preexec_invoke_precmd_functions 1 'lastarg' || status=$?
351401

352402
[ "$status" == 38 ]
353403

354404
precmd_functions=(tester3)
355405
status=0
356-
__bp_invoke_precmd_functions 1 'lastarg' || status=$?
406+
bash_preexec_invoke_precmd_functions 1 'lastarg' || status=$?
357407

358408
[ "$status" == 0 ]
359409
}
360410

361-
@test "__bp_invoke_preexec_functions should be transparent for \$? and \$_" {
411+
@test "bash_preexec_invoke_preexec_functions should be transparent for \$? and \$_" {
362412
tester1() { test1_lastexit=$? test1_lastarg=$_; }
363413
tester2() { test2_lastexit=$? test2_lastarg=$_; }
364414
preexec_functions=(tester1 tester2)
365415
trap - DEBUG # remove the Bats stack-trace trap so $_ doesn't get overwritten
366-
__bp_invoke_preexec_functions 87 'ehQrzHTHtE2E7Q' 'command' || true
416+
bash_preexec_invoke_preexec_functions 87 'ehQrzHTHtE2E7Q' 'command' || true
367417

368418
[ "$test1_lastexit" == 87 ]
369419
[ "$test1_lastarg" == 'ehQrzHTHtE2E7Q' ]
370420
[ "$test2_lastexit" == 87 ]
371421
[ "$test2_lastarg" == 'ehQrzHTHtE2E7Q' ]
372422
}
373423

374-
@test "__bp_invoke_preexec_functions returns the last non-zero exit status" {
424+
@test "bash_preexec_invoke_preexec_functions returns the last non-zero exit status" {
375425
tester1() { return 52; }
376426
tester2() { return 112; }
377427
tester3() { return 0; }
378428
preexec_functions=(tester1 tester2 tester3)
379429
status=0
380-
__bp_invoke_preexec_functions 1 'lastarg' 'command' || status=$?
430+
bash_preexec_invoke_preexec_functions 1 'lastarg' 'command' || status=$?
381431

382432
[ "$status" == 112 ]
383433

384434
preexec_functions=(tester3)
385435
status=0
386-
__bp_invoke_preexec_functions 1 'lastarg' 'command' || status=$?
436+
bash_preexec_invoke_preexec_functions 1 'lastarg' 'command' || status=$?
387437

388438
[ "$status" == 0 ]
389439
}
390440

391-
@test "__bp_invoke_preexec_functions should supply a current command in the first argument" {
441+
@test "bash_preexec_invoke_preexec_functions should supply a current command in the first argument" {
392442
tester1() { test1_bash_command=$1; }
393443
tester2() { test2_bash_command=$1; }
394444
preexec_functions=(tester1 tester2)
395-
__bp_invoke_preexec_functions 1 'lastarg' 'UEVkErELArSwjA' || true
445+
bash_preexec_invoke_preexec_functions 1 'lastarg' 'UEVkErELArSwjA' || true
396446

397447
[ "$test1_bash_command" == 'UEVkErELArSwjA' ]
398448
[ "$test2_bash_command" == 'UEVkErELArSwjA' ]

0 commit comments

Comments
 (0)