|
| 1 | +# bash-preexec.sh -- Bash support for ZSH-like 'preexec' and 'precmd' functions. |
| 2 | +# https://github.com/rcaloras/bash-preexec |
| 3 | +# |
| 4 | +# |
| 5 | +# 'preexec' functions are executed before each interactive command is |
| 6 | +# executed, with the interactive command as its argument. The 'precmd' |
| 7 | +# function is executed before each prompt is displayed. |
| 8 | +# |
| 9 | +# Author: Ryan Caloras (ryan@bashhub.com) |
| 10 | +# Forked from Original Author: Glyph Lefkowitz |
| 11 | +# |
| 12 | +# V0.5.0 |
| 13 | +# |
| 14 | + |
| 15 | +# General Usage: |
| 16 | +# |
| 17 | +# 1. Source this file at the end of your bash profile so as not to interfere |
| 18 | +# with anything else that's using PROMPT_COMMAND. |
| 19 | +# |
| 20 | +# 2. Add any precmd or preexec functions by appending them to their arrays: |
| 21 | +# e.g. |
| 22 | +# precmd_functions+=(my_precmd_function) |
| 23 | +# precmd_functions+=(some_other_precmd_function) |
| 24 | +# |
| 25 | +# preexec_functions+=(my_preexec_function) |
| 26 | +# |
| 27 | +# 3. Consider changing anything using the DEBUG trap or PROMPT_COMMAND |
| 28 | +# to use preexec and precmd instead. Preexisting usages will be |
| 29 | +# preserved, but doing so manually may be less surprising. |
| 30 | +# |
| 31 | +# Note: This module requires two Bash features which you must not otherwise be |
| 32 | +# using: the "DEBUG" trap, and the "PROMPT_COMMAND" variable. If you override |
| 33 | +# either of these after bash-preexec has been installed it will most likely break. |
| 34 | + |
| 35 | +# Make sure this is bash that's running and return otherwise. |
| 36 | +# Use POSIX syntax for this line: |
| 37 | +if [ -z "${BASH_VERSION-}" ]; then |
| 38 | + return 1; |
| 39 | +fi |
| 40 | + |
| 41 | +# Avoid duplicate inclusion |
| 42 | +if [[ -n "${bash_preexec_imported:-}" ]]; then |
| 43 | + return 0 |
| 44 | +fi |
| 45 | +bash_preexec_imported="defined" |
| 46 | + |
| 47 | +# WARNING: This variable is no longer used and should not be relied upon. |
| 48 | +# Use ${bash_preexec_imported} instead. |
| 49 | +__bp_imported="${bash_preexec_imported}" |
| 50 | + |
| 51 | +# Should be available to each precmd and preexec |
| 52 | +# functions, should they want it. $? and $_ are available as $? and $_, but |
| 53 | +# $PIPESTATUS is available only in a copy, $BP_PIPESTATUS. |
| 54 | +# TODO: Figure out how to restore PIPESTATUS before each precmd or preexec |
| 55 | +# function. |
| 56 | +__bp_last_ret_value="$?" |
| 57 | +BP_PIPESTATUS=("${PIPESTATUS[@]}") |
| 58 | +__bp_last_argument_prev_command="$_" |
| 59 | + |
| 60 | +__bp_inside_precmd=0 |
| 61 | +__bp_inside_preexec=0 |
| 62 | + |
| 63 | +# Initial PROMPT_COMMAND string that is removed from PROMPT_COMMAND post __bp_install |
| 64 | +__bp_install_string=$'__bp_trap_string="$(trap -p DEBUG)"\ntrap - DEBUG\n__bp_install' |
| 65 | + |
| 66 | +# Fails if any of the given variables are readonly |
| 67 | +# Reference https://stackoverflow.com/a/4441178 |
| 68 | +__bp_require_not_readonly() { |
| 69 | + local var |
| 70 | + for var; do |
| 71 | + if ! ( unset "$var" 2> /dev/null ); then |
| 72 | + echo "bash-preexec requires write access to ${var}" >&2 |
| 73 | + return 1 |
| 74 | + fi |
| 75 | + done |
| 76 | +} |
| 77 | + |
| 78 | +# Remove ignorespace and or replace ignoreboth from HISTCONTROL |
| 79 | +# so we can accurately invoke preexec with a command from our |
| 80 | +# history even if it starts with a space. |
| 81 | +__bp_adjust_histcontrol() { |
| 82 | + local histcontrol |
| 83 | + histcontrol="${HISTCONTROL:-}" |
| 84 | + histcontrol="${histcontrol//ignorespace}" |
| 85 | + # Replace ignoreboth with ignoredups |
| 86 | + if [[ "$histcontrol" == *"ignoreboth"* ]]; then |
| 87 | + histcontrol="ignoredups:${histcontrol//ignoreboth}" |
| 88 | + fi; |
| 89 | + export HISTCONTROL="$histcontrol" |
| 90 | +} |
| 91 | + |
| 92 | +# This variable describes whether we are currently in "interactive mode"; |
| 93 | +# i.e. whether this shell has just executed a prompt and is waiting for user |
| 94 | +# input. It documents whether the current command invoked by the trace hook is |
| 95 | +# run interactively by the user; it's set immediately after the prompt hook, |
| 96 | +# and unset as soon as the trace hook is run. |
| 97 | +__bp_preexec_interactive_mode="" |
| 98 | + |
| 99 | +# These arrays are used to add functions to be run before, or after, prompts. |
| 100 | +declare -a precmd_functions |
| 101 | +declare -a preexec_functions |
| 102 | + |
| 103 | +# Trims leading and trailing whitespace from $2 and writes it to the variable |
| 104 | +# name passed as $1 |
| 105 | +__bp_trim_whitespace() { |
| 106 | + local var=${1:?} text=${2:-} |
| 107 | + text="${text#"${text%%[![:space:]]*}"}" # remove leading whitespace characters |
| 108 | + text="${text%"${text##*[![:space:]]}"}" # remove trailing whitespace characters |
| 109 | + printf -v "$var" '%s' "$text" |
| 110 | +} |
| 111 | + |
| 112 | + |
| 113 | +# Trims whitespace and removes any leading or trailing semicolons from $2 and |
| 114 | +# writes the resulting string to the variable name passed as $1. Used for |
| 115 | +# manipulating substrings in PROMPT_COMMAND |
| 116 | +__bp_sanitize_string() { |
| 117 | + local var=${1:?} text=${2:-} sanitized |
| 118 | + __bp_trim_whitespace sanitized "$text" |
| 119 | + sanitized=${sanitized%;} |
| 120 | + sanitized=${sanitized#;} |
| 121 | + __bp_trim_whitespace sanitized "$sanitized" |
| 122 | + printf -v "$var" '%s' "$sanitized" |
| 123 | +} |
| 124 | + |
| 125 | +# This function is installed as part of the PROMPT_COMMAND; |
| 126 | +# It sets a variable to indicate that the prompt was just displayed, |
| 127 | +# to allow the DEBUG trap to know that the next command is likely interactive. |
| 128 | +__bp_interactive_mode() { |
| 129 | + __bp_preexec_interactive_mode="on"; |
| 130 | +} |
| 131 | + |
| 132 | + |
| 133 | +# This function is installed as part of the PROMPT_COMMAND. |
| 134 | +# It will invoke any functions defined in the precmd_functions array. |
| 135 | +__bp_precmd_invoke_cmd() { |
| 136 | + # Save the returned value from our last command, and from each process in |
| 137 | + # its pipeline. Note: this MUST be the first thing done in this function. |
| 138 | + __bp_last_ret_value="$?" BP_PIPESTATUS=("${PIPESTATUS[@]}") |
| 139 | + |
| 140 | + # Don't invoke precmds if we are inside an execution of an "original |
| 141 | + # prompt command" by another precmd execution loop. This avoids infinite |
| 142 | + # recursion. |
| 143 | + if (( __bp_inside_precmd > 0 )); then |
| 144 | + return |
| 145 | + fi |
| 146 | + local __bp_inside_precmd=1 |
| 147 | + |
| 148 | + # Invoke every function defined in our function array. |
| 149 | + local precmd_function |
| 150 | + for precmd_function in "${precmd_functions[@]}"; do |
| 151 | + |
| 152 | + # Only execute this function if it actually exists. |
| 153 | + # Test existence of functions with: declare -[Ff] |
| 154 | + if type -t "$precmd_function" 1>/dev/null; then |
| 155 | + __bp_set_ret_value "$__bp_last_ret_value" "$__bp_last_argument_prev_command" |
| 156 | + # Quote our function invocation to prevent issues with IFS |
| 157 | + "$precmd_function" |
| 158 | + fi |
| 159 | + done |
| 160 | + |
| 161 | + __bp_set_ret_value "$__bp_last_ret_value" |
| 162 | +} |
| 163 | + |
| 164 | +# Sets a return value in $?. We may want to get access to the $? variable in our |
| 165 | +# precmd functions. This is available for instance in zsh. We can simulate it in bash |
| 166 | +# by setting the value here. |
| 167 | +__bp_set_ret_value() { |
| 168 | + return ${1:-} |
| 169 | +} |
| 170 | + |
| 171 | +__bp_in_prompt_command() { |
| 172 | + |
| 173 | + local prompt_command_array |
| 174 | + IFS=$'\n;' read -rd '' -a prompt_command_array <<< "${PROMPT_COMMAND:-}" |
| 175 | + |
| 176 | + local trimmed_arg |
| 177 | + __bp_trim_whitespace trimmed_arg "${1:-}" |
| 178 | + |
| 179 | + local command trimmed_command |
| 180 | + for command in "${prompt_command_array[@]:-}"; do |
| 181 | + __bp_trim_whitespace trimmed_command "$command" |
| 182 | + if [[ "$trimmed_command" == "$trimmed_arg" ]]; then |
| 183 | + return 0 |
| 184 | + fi |
| 185 | + done |
| 186 | + |
| 187 | + return 1 |
| 188 | +} |
| 189 | + |
| 190 | +# This function is installed as the DEBUG trap. It is invoked before each |
| 191 | +# interactive prompt display. Its purpose is to inspect the current |
| 192 | +# environment to attempt to detect if the current command is being invoked |
| 193 | +# interactively, and invoke 'preexec' if so. |
| 194 | +__bp_preexec_invoke_exec() { |
| 195 | + |
| 196 | + # Save the contents of $_ so that it can be restored later on. |
| 197 | + # https://stackoverflow.com/questions/40944532/bash-preserve-in-a-debug-trap#40944702 |
| 198 | + __bp_last_argument_prev_command="${1:-}" |
| 199 | + # Don't invoke preexecs if we are inside of another preexec. |
| 200 | + if (( __bp_inside_preexec > 0 )); then |
| 201 | + return |
| 202 | + fi |
| 203 | + local __bp_inside_preexec=1 |
| 204 | + |
| 205 | + # Checks if the file descriptor is not standard out (i.e. '1') |
| 206 | + # __bp_delay_install checks if we're in test. Needed for bats to run. |
| 207 | + # Prevents preexec from being invoked for functions in PS1 |
| 208 | + if [[ ! -t 1 && -z "${__bp_delay_install:-}" ]]; then |
| 209 | + return |
| 210 | + fi |
| 211 | + |
| 212 | + if [[ -n "${COMP_LINE:-}" ]]; then |
| 213 | + # We're in the middle of a completer. This obviously can't be |
| 214 | + # an interactively issued command. |
| 215 | + return |
| 216 | + fi |
| 217 | + if [[ -z "${__bp_preexec_interactive_mode:-}" ]]; then |
| 218 | + # We're doing something related to displaying the prompt. Let the |
| 219 | + # prompt set the title instead of me. |
| 220 | + return |
| 221 | + else |
| 222 | + # If we're in a subshell, then the prompt won't be re-displayed to put |
| 223 | + # us back into interactive mode, so let's not set the variable back. |
| 224 | + # In other words, if you have a subshell like |
| 225 | + # (sleep 1; sleep 2) |
| 226 | + # You want to see the 'sleep 2' as a set_command_title as well. |
| 227 | + if [[ 0 -eq "${BASH_SUBSHELL:-}" ]]; then |
| 228 | + __bp_preexec_interactive_mode="" |
| 229 | + fi |
| 230 | + fi |
| 231 | + |
| 232 | + if __bp_in_prompt_command "${BASH_COMMAND:-}"; then |
| 233 | + # If we're executing something inside our prompt_command then we don't |
| 234 | + # want to call preexec. Bash prior to 3.1 can't detect this at all :/ |
| 235 | + __bp_preexec_interactive_mode="" |
| 236 | + return |
| 237 | + fi |
| 238 | + |
| 239 | + local this_command |
| 240 | + this_command=$( |
| 241 | + export LC_ALL=C |
| 242 | + HISTTIMEFORMAT= builtin history 1 | sed '1 s/^ *[0-9][0-9]*[* ] //' |
| 243 | + ) |
| 244 | + |
| 245 | + # Sanity check to make sure we have something to invoke our function with. |
| 246 | + if [[ -z "$this_command" ]]; then |
| 247 | + return |
| 248 | + fi |
| 249 | + |
| 250 | + # Invoke every function defined in our function array. |
| 251 | + local preexec_function |
| 252 | + local preexec_function_ret_value |
| 253 | + local preexec_ret_value=0 |
| 254 | + for preexec_function in "${preexec_functions[@]:-}"; do |
| 255 | + |
| 256 | + # Only execute each function if it actually exists. |
| 257 | + # Test existence of function with: declare -[fF] |
| 258 | + if type -t "$preexec_function" 1>/dev/null; then |
| 259 | + __bp_set_ret_value ${__bp_last_ret_value:-} |
| 260 | + # Quote our function invocation to prevent issues with IFS |
| 261 | + "$preexec_function" "$this_command" |
| 262 | + preexec_function_ret_value="$?" |
| 263 | + if [[ "$preexec_function_ret_value" != 0 ]]; then |
| 264 | + preexec_ret_value="$preexec_function_ret_value" |
| 265 | + fi |
| 266 | + fi |
| 267 | + done |
| 268 | + |
| 269 | + # Restore the last argument of the last executed command, and set the return |
| 270 | + # value of the DEBUG trap to be the return code of the last preexec function |
| 271 | + # to return an error. |
| 272 | + # If `extdebug` is enabled a non-zero return value from any preexec function |
| 273 | + # will cause the user's command not to execute. |
| 274 | + # Run `shopt -s extdebug` to enable |
| 275 | + __bp_set_ret_value "$preexec_ret_value" "$__bp_last_argument_prev_command" |
| 276 | +} |
| 277 | + |
| 278 | +__bp_install() { |
| 279 | + # Exit if we already have this installed. |
| 280 | + if [[ "${PROMPT_COMMAND:-}" == *"__bp_precmd_invoke_cmd"* ]]; then |
| 281 | + return 1; |
| 282 | + fi |
| 283 | + |
| 284 | + trap '__bp_preexec_invoke_exec "$_"' DEBUG |
| 285 | + |
| 286 | + # Preserve any prior DEBUG trap as a preexec function |
| 287 | + local prior_trap=$(sed "s/[^']*'\(.*\)'[^']*/\1/" <<<"${__bp_trap_string:-}") |
| 288 | + unset __bp_trap_string |
| 289 | + if [[ -n "$prior_trap" ]]; then |
| 290 | + eval '__bp_original_debug_trap() { |
| 291 | + '"$prior_trap"' |
| 292 | + }' |
| 293 | + preexec_functions+=(__bp_original_debug_trap) |
| 294 | + fi |
| 295 | + |
| 296 | + # Adjust our HISTCONTROL Variable if needed. |
| 297 | + __bp_adjust_histcontrol |
| 298 | + |
| 299 | + # Issue #25. Setting debug trap for subshells causes sessions to exit for |
| 300 | + # backgrounded subshell commands (e.g. (pwd)& ). Believe this is a bug in Bash. |
| 301 | + # |
| 302 | + # Disabling this by default. It can be enabled by setting this variable. |
| 303 | + if [[ -n "${__bp_enable_subshells:-}" ]]; then |
| 304 | + |
| 305 | + # Set so debug trap will work be invoked in subshells. |
| 306 | + set -o functrace > /dev/null 2>&1 |
| 307 | + shopt -s extdebug > /dev/null 2>&1 |
| 308 | + fi; |
| 309 | + |
| 310 | + local existing_prompt_command |
| 311 | + # Remove setting our trap install string and sanitize the existing prompt command string |
| 312 | + existing_prompt_command="${PROMPT_COMMAND:-}" |
| 313 | + existing_prompt_command="${existing_prompt_command//$__bp_install_string[;$'\n']}" # Edge case of appending to PROMPT_COMMAND |
| 314 | + existing_prompt_command="${existing_prompt_command//$__bp_install_string}" |
| 315 | + __bp_sanitize_string existing_prompt_command "$existing_prompt_command" |
| 316 | + |
| 317 | + # Install our hooks in PROMPT_COMMAND to allow our trap to know when we've |
| 318 | + # actually entered something. |
| 319 | + PROMPT_COMMAND=$'__bp_precmd_invoke_cmd\n' |
| 320 | + if [[ -n "$existing_prompt_command" ]]; then |
| 321 | + PROMPT_COMMAND+=${existing_prompt_command}$'\n' |
| 322 | + fi; |
| 323 | + PROMPT_COMMAND+='__bp_interactive_mode' |
| 324 | + |
| 325 | + # Add two functions to our arrays for convenience |
| 326 | + # of definition. |
| 327 | + precmd_functions+=(precmd) |
| 328 | + preexec_functions+=(preexec) |
| 329 | + |
| 330 | + # Invoke our two functions manually that were added to $PROMPT_COMMAND |
| 331 | + __bp_precmd_invoke_cmd |
| 332 | + __bp_interactive_mode |
| 333 | +} |
| 334 | + |
| 335 | +# Sets an installation string as part of our PROMPT_COMMAND to install |
| 336 | +# after our session has started. This allows bash-preexec to be included |
| 337 | +# at any point in our bash profile. |
| 338 | +__bp_install_after_session_init() { |
| 339 | + # bash-preexec needs to modify these variables in order to work correctly |
| 340 | + # if it can't, just stop the installation |
| 341 | + __bp_require_not_readonly PROMPT_COMMAND HISTCONTROL HISTTIMEFORMAT || return |
| 342 | + |
| 343 | + local sanitized_prompt_command |
| 344 | + __bp_sanitize_string sanitized_prompt_command "${PROMPT_COMMAND:-}" |
| 345 | + if [[ -n "$sanitized_prompt_command" ]]; then |
| 346 | + PROMPT_COMMAND=${sanitized_prompt_command}$'\n' |
| 347 | + fi; |
| 348 | + PROMPT_COMMAND+=${__bp_install_string} |
| 349 | +} |
| 350 | + |
| 351 | +# Run our install so long as we're not delaying it. |
| 352 | +if [[ -z "${__bp_delay_install:-}" ]]; then |
| 353 | + __bp_install_after_session_init |
| 354 | +fi; |
0 commit comments