diff --git a/bash-preexec.sh b/bash-preexec.sh index 3edd533..fcbe887 100644 --- a/bash-preexec.sh +++ b/bash-preexec.sh @@ -5,7 +5,7 @@ # # # 'preexec' functions are executed before each interactive command is -# executed, with the interactive command as its argument. The 'precmd' +# executed, with the interactive command as its argument. The 'precmd' # function is executed before each prompt is displayed. # # Author: Ryan Caloras (ryan@bashhub.com) @@ -44,6 +44,10 @@ __bp_imported="defined" # functions, should they want it. __bp_last_command_ret_value="$?" +# Command to set our preexec trap. It's invoked once via +# PROMPT_COMMAND and then removed. +__bp_trap_install_string="trap '__bp_preexec_invoke_exec' DEBUG;" + # Remove ignorespace and or replace ignoreboth from HISTCONTROL # so we can accurately invoke preexec with a command from our # history even if it starts with a space. @@ -137,7 +141,7 @@ __bp_preexec_invoke_exec() { # __bp_delay_install checks if we're in test. Needed for bats to run. # Prevents preexec from being invoked for functions in PS1 if [[ ! -t 1 && -z "$__bp_delay_install" ]]; then - return + return fi if [[ -n "$COMP_LINE" ]]; then @@ -192,14 +196,33 @@ __bp_preexec_invoke_exec() { done } -# Execute this to set up preexec and precmd execution. -__bp_preexec_and_precmd_install() { +# Returns PROMPT_COMMAND with a semicolon appended +# if it doesn't already have one. +__bp_prompt_command_with_semi_colon() { + # Take our existing prompt command and append a semicolon to it + # if it doesn't already have one. + local existing_prompt_command - # Make sure this is bash that's running this and return otherwise. - if [[ -z "$BASH_VERSION" ]]; then - return 1; + if [[ -n "$PROMPT_COMMAND" ]]; then + existing_prompt_command=${PROMPT_COMMAND%${PROMPT_COMMAND##*[![:space:]]}} + existing_prompt_command=${existing_prompt_command%;} + existing_prompt_command=${existing_prompt_command/%/;} + else + existing_prompt_command="" fi + echo -n "$existing_prompt_command" +} + +__bp_install() { + + # Remove setting our trap from PROMPT_COMMAND + PROMPT_COMMAND="${PROMPT_COMMAND//$__bp_trap_install_string}" + + # Remove this function from our PROMPT_COMMAND + PROMPT_COMMAND="${PROMPT_COMMAND//__bp_install;}" + + # Exit if we already have this installed. if [[ "$PROMPT_COMMAND" == *"__bp_precmd_invoke_cmd"* ]]; then return 1; @@ -208,34 +231,55 @@ __bp_preexec_and_precmd_install() { # Adjust our HISTCONTROL Variable if needed. __bp_adjust_histcontrol - # Set so debug trap will work be invoked in subshells. set -o functrace > /dev/null 2>&1 shopt -s extdebug > /dev/null 2>&1 - # Take our existing prompt command and append a semicolon to it - # if it doesn't already have one. - local existing_prompt_command - if [[ -n "$PROMPT_COMMAND" ]]; then - existing_prompt_command=${PROMPT_COMMAND%${PROMPT_COMMAND##*[![:space:]]}} - existing_prompt_command=${existing_prompt_command%;} - existing_prompt_command=${existing_prompt_command/%/;} - else - existing_prompt_command="" - fi + local existing_prompt_command + existing_prompt_command=$(__bp_prompt_command_with_semi_colon) - # Finally install our traps. + # Install our hooks in PROMPT_COMMAND to allow our trap to know when we've + # actually entered something. PROMPT_COMMAND="__bp_precmd_invoke_cmd; ${existing_prompt_command} __bp_interactive_mode;" trap '__bp_preexec_invoke_exec' DEBUG; + # Add two functions to our arrays for convenience # of definition. precmd_functions+=(precmd) preexec_functions+=(preexec) + + # Since this is in PROMPT_COMMAND, invoke any precmd functions we have defined. + __bp_precmd_invoke_cmd + # Put us in interactive mode for our first command. + __bp_interactive_mode +} + +# Sets our trap and __bp_install as part of our PROMPT_COMMAND to install +# after our session has started. This allows bash-preexec to be inlucded +# at any point in our bash profile. Ideally we could set our trap inside +# __bp_install, but if a trap already exists it'll only set locally to +# the function. +__bp_install_after_session_init() { + + # Make sure this is bash that's running this and return otherwise. + if [[ -z "$BASH_VERSION" ]]; then + return 1; + fi + + local existing_prompt_command + existing_prompt_command=$(__bp_prompt_command_with_semi_colon) + + + __bp_original_prompt_command="$existing_prompt_command" + + # Add our installation to be done last via our PROMPT_COMMAND. These are + # removed by __bp_install when it's invoked so it only runs once. + PROMPT_COMMAND="${existing_prompt_command} $__bp_trap_install_string __bp_install;" } # Run our install so long as we're not delaying it. if [[ -z "$__bp_delay_install" ]]; then - __bp_preexec_and_precmd_install + __bp_install_after_session_init fi; diff --git a/test/bash-preexec.bats b/test/bash-preexec.bats index 289870c..ab4a001 100644 --- a/test/bash-preexec.bats +++ b/test/bash-preexec.bats @@ -13,20 +13,35 @@ test_preexec_echo() { echo "$1" } -@test "__bp_preexec_and_precmd_install should exit with 1 if we're not using bash" { +@test "__bp_install_after_session_init should exit with 1 if we're not using bash" { unset BASH_VERSION - run '__bp_preexec_and_precmd_install' + run '__bp_install_after_session_init' [[ $status == 1 ]] [[ -z "$output" ]] } -@test "__bp_preexec_and_precmd_install should exit if it's already installed" { +@test "__bp_install should exit if it's already installed" { PROMPT_COMMAND="some_other_function; __bp_precmd_invoke_cmd;" - run '__bp_preexec_and_precmd_install' + run '__bp_install' [[ $status == 1 ]] [[ -z "$output" ]] } +@test "__bp_install should remove trap and itself from PROMPT_COMMAND" { + trap_string="trap '__bp_preexec_invoke_exec' DEBUG;" + PROMPT_COMMAND="some_other_function; $trap_string __bp_install;" + + [[ $PROMPT_COMMAND == *"$trap_string"* ]] + [[ $PROMPT_COMMAND = *"__bp_install;"* ]] + + __bp_install + + [[ $PROMPT_COMMAND != *"$trap_string"* ]] + [[ $PROMPT_COMMAND != *"__bp_install;"* ]] + [[ -z "$output" ]] +} + + @test "No functions defined for preexec should simply return" { run '__bp_preexec_invoke_exec' [[ $status == 0 ]] diff --git a/test/include-test.bats b/test/include-test.bats index 17f79ae..7bef4d4 100644 --- a/test/include-test.bats +++ b/test/include-test.bats @@ -9,6 +9,6 @@ @test "should import of not defined" { unset __bp_imported source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" - [[ -n $(type -t __bp_preexec_and_precmd_install) ]] + [[ -n $(type -t __bp_install) ]] }