Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow bash-preexec to be included at any point #110

Merged
merged 5 commits into from
Aug 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 40 additions & 24 deletions bash-preexec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ __bp_last_argument_prev_command="$_"
__bp_inside_precmd=0
__bp_inside_preexec=0

# Initial PROMPT_COMMAND string that is removed from PROMPT_COMMAND post __bp_install
__bp_install_string=$'__bp_trap_string="$(trap -p DEBUG)"\ntrap - DEBUG\n__bp_install'

# Fails if any of the given variables are readonly
# Reference https://stackoverflow.com/a/4441178
__bp_require_not_readonly() {
Expand Down Expand Up @@ -89,6 +92,19 @@ __bp_trim_whitespace() {
echo -n "$var"
}


# Returns a copy of the passed in string trimmed of whitespace
# and removes any leading or trailing semi colons.
# Used for manipulating substrings in PROMPT_COMMAND
__bp_sanitize_string() {
local sanitized_string
sanitized_string=$(__bp_trim_whitespace "${1:-}")
sanitized_string=${sanitized_string%;}
sanitized_string=${sanitized_string#;}
sanitized_string=$(__bp_trim_whitespace "$sanitized_string")
echo -n "$sanitized_string"
}

# 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.
Expand Down Expand Up @@ -136,7 +152,7 @@ __bp_set_ret_value() {
__bp_in_prompt_command() {

local prompt_command_array
IFS=';' read -ra prompt_command_array <<< "$PROMPT_COMMAND"
IFS=$'\n;' read -rd '' -a prompt_command_array <<< "$PROMPT_COMMAND"

local trimmed_arg
trimmed_arg=$(__bp_trim_whitespace "${1:-}")
Expand All @@ -159,6 +175,7 @@ __bp_in_prompt_command() {
# environment to attempt to detect if the current command is being invoked
# interactively, and invoke 'preexec' if so.
__bp_preexec_invoke_exec() {

# Save the contents of $_ so that it can be restored later on.
# https://stackoverflow.com/questions/40944532/bash-preserve-in-a-debug-trap#40944702
__bp_last_argument_prev_command="${1:-}"
Expand Down Expand Up @@ -266,7 +283,6 @@ __bp_install() {
# Adjust our HISTCONTROL Variable if needed.
__bp_adjust_histcontrol


# Issue #25. Setting debug trap for subshells causes sessions to exit for
# backgrounded subshell commands (e.g. (pwd)& ). Believe this is a bug in Bash.
#
Expand All @@ -278,24 +294,33 @@ __bp_install() {
shopt -s extdebug > /dev/null 2>&1
fi;

local __bp_existing_prompt_command
# Remove setting our trap install string and sanitize the existing prompt command string
__bp_existing_prompt_command="${PROMPT_COMMAND//$__bp_install_string[;$'\n']}" # Edge case of appending to PROMPT_COMMAND
__bp_existing_prompt_command="${__bp_existing_prompt_command//$__bp_install_string}"
__bp_existing_prompt_command=$(__bp_sanitize_string "$__bp_existing_prompt_command")

# Install our hooks in PROMPT_COMMAND to allow our trap to know when we've
# actually entered something.
PROMPT_COMMAND="__bp_precmd_invoke_cmd; __bp_interactive_mode"
PROMPT_COMMAND=$'__bp_precmd_invoke_cmd\n'
if [[ -n "$__bp_existing_prompt_command" ]]; then
PROMPT_COMMAND+=${__bp_existing_prompt_command}$'\n'
fi;
PROMPT_COMMAND+='__bp_interactive_mode'

# Add two functions to our arrays for convenience
# of definition.
precmd_functions+=(precmd)
preexec_functions+=(preexec)

# Since this function is invoked via PROMPT_COMMAND, re-execute PC now that it's properly set
eval "$PROMPT_COMMAND"
# Invoke our two functions manually that were added to $PROMPT_COMMAND
__bp_precmd_invoke_cmd
__bp_interactive_mode
}

# Sets our trap and __bp_install as part of our PROMPT_COMMAND to install
# 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. Ideally we could set our trap inside
# __bp_install, but if a trap already exists it'll only set locally to
# the function.
# at any point in our bash profile.
__bp_install_after_session_init() {

# Make sure this is bash that's running this and return otherwise.
Expand All @@ -307,21 +332,12 @@ __bp_install_after_session_init() {
# if it can't, just stop the installation
__bp_require_not_readonly PROMPT_COMMAND HISTCONTROL HISTTIMEFORMAT || return

# If there's an existing PROMPT_COMMAND capture it and convert it into a function
# So it is preserved and invoked during precmd.
if [[ -n "${PROMPT_COMMAND:-}" ]]; then
eval '__bp_original_prompt_command() {
'"$PROMPT_COMMAND"'
}'
precmd_functions+=(__bp_original_prompt_command)
fi

# Installation is finalized in PROMPT_COMMAND, which allows us to override the DEBUG
# trap. __bp_install sets PROMPT_COMMAND to its final value, so these are only
# invoked once.
# It's necessary to clear any existing DEBUG trap in order to set it from the install function.
# Using \n as it's the most universal delimiter of bash commands
PROMPT_COMMAND=$'\n__bp_trap_string="$(trap -p DEBUG)"\ntrap - DEBUG\n__bp_install\n'
local sanitized_prompt_command
sanitized_prompt_command=$(__bp_sanitize_string "$PROMPT_COMMAND")
if [[ -n "$sanitized_prompt_command" ]]; then
PROMPT_COMMAND=${sanitized_prompt_command}$'\n'
fi;
PROMPT_COMMAND+=${__bp_install_string}
}

# Run our install so long as we're not delaying it.
Expand Down
77 changes: 75 additions & 2 deletions test/bash-preexec.bats
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,86 @@ test_preexec_echo() {
(( trap_count_snapshot < trap_invoked_count ))
}

@test "PROMPT_COMMAND=\"\$PROMPT_COMMAND; foo\" should work" {
@test "__bp_sanitize_string should remove semicolons and trim space" {

run '__bp_sanitize_string' " true1; "$'\n'
[ $status -eq 0 ]
[ "$output" == "true1" ]

run '__bp_sanitize_string' " ; true2; "
[ $status -eq 0 ]
[ "$output" == "true2" ]

run '__bp_sanitize_string' $'\n'" ; true3; "
[ $status -eq 0 ]
[ "$output" == "true3" ]

}

@test "Appending to PROMPT_COMMAND should work after bp_install" {
bp_install

PROMPT_COMMAND="$PROMPT_COMMAND; true"
eval "$PROMPT_COMMAND"
}

@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"
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"
}

# 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"
@test "Adding to PROMPT_COMMAND before and after initiating install" {
PROMPT_COMMAND="echo before"
PROMPT_COMMAND="$PROMPT_COMMAND; echo before2"
__bp_install_after_session_init
PROMPT_COMMAND="$PROMPT_COMMAND"$'\n echo after'
PROMPT_COMMAND="echo after2; $PROMPT_COMMAND;"

eval "$PROMPT_COMMAND"

expected_result=$'__bp_precmd_invoke_cmd\necho after2; echo before; echo before2\n echo after\n__bp_interactive_mode'
[ "$PROMPT_COMMAND" == "$expected_result" ]
}

@test "Adding to PROMPT_COMMAND after with semicolon" {
PROMPT_COMMAND="echo before"
__bp_install_after_session_init
PROMPT_COMMAND="$PROMPT_COMMAND; echo after"

eval "$PROMPT_COMMAND"

expected_result=$'__bp_precmd_invoke_cmd\necho before\n echo after\n__bp_interactive_mode'
[ "$PROMPT_COMMAND" == "$expected_result" ]
}

@test "during install PROMPT_COMMAND and precmd functions should be executed each once" {
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[0]}" == "after2" ]
[ "${lines[1]}" == "before" ]
[ "${lines[2]}" == "before2" ]
[ "${lines[3]}" == "inside precmd" ]
[ "${lines[4]}" == "after" ]
[ "${#lines[@]}" == '5' ]
}

@test "No functions defined for preexec should simply return" {
__bp_interactive_mode

Expand Down Expand Up @@ -213,7 +286,7 @@ test_preexec_echo() {

@test "in_prompt_command should detect if a command is part of PROMPT_COMMAND" {

PROMPT_COMMAND="precmd_invoke_cmd; something;"
PROMPT_COMMAND=$'precmd_invoke_cmd\n something; echo yo\n __bp_interactive_mode'
run '__bp_in_prompt_command' "something"
[ $status -eq 0 ]

Expand Down