Skip to content

Commit

Permalink
Install bash-preexec after our session is started.
Browse files Browse the repository at this point in the history
- Invoke our install as part of PROMPT_COMMAND. This allows bash-preexec to be
  included at any point in our configuration and it should be invoked last.
  This makes sure we always have access to the DEBUG trap and that
  PROMPT_COMMAND is most likely untampered with.

- Should adress #21 and the occasional issue that gets created because
  bash-preexec is included improperly.
  • Loading branch information
rcaloras committed Apr 22, 2016
1 parent 42c7df3 commit 4744d7e
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 25 deletions.
83 changes: 63 additions & 20 deletions bash-preexec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -192,14 +196,36 @@ __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() {

# Make sure this is bash that's running this and return otherwise.
if [[ -z "$BASH_VERSION" ]]; then
return 1;
# Trim our existing PROMPT_COMMAND
local trimmed
trimmed=$(__bp_trim_whitespace "$PROMPT_COMMAND")

# Take our existing prompt command and append a semicolon to it
# if it doesn't already have one.
local existing_prompt_command
if [[ -n "$trimmed" ]]; then
existing_prompt_command=${trimmed%${trimmed##*[![: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;
Expand All @@ -208,34 +234,51 @@ __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)

# 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;
48 changes: 44 additions & 4 deletions test/bash-preexec.bats
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,60 @@ 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 "__bp_prompt_command_with_semi_colon should handle different PROMPT_COMMANDS" {
# PROMPT_COMMAND of spaces
PROMPT_COMMAND=" "

run '__bp_prompt_command_with_semi_colon'
[[ -z "$output" ]]

# PROMPT_COMMAND of one command
PROMPT_COMMAND="echo 'yo'"

run '__bp_prompt_command_with_semi_colon'
[[ "$output" == "echo 'yo';" ]]

# No PROMPT_COMMAND
unset PROMPT_COMMAND
run '__bp_prompt_command_with_semi_colon'
[[ -z "$output" ]]

# PROMPT_COMMAND of two commands and trimmed
PROMPT_COMMAND="echo 'yo'; ls "

run '__bp_prompt_command_with_semi_colon'
[[ "$output" == "echo 'yo'; ls;" ]]
}


@test "No functions defined for preexec should simply return" {
run '__bp_preexec_invoke_exec'
[[ $status == 0 ]]
Expand Down
2 changes: 1 addition & 1 deletion test/include-test.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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) ]]
}

0 comments on commit 4744d7e

Please sign in to comment.