Something went wrong. Try again.
Something went wrong. Try again.
15 kB · 382 lines
Shell
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383# bash-preexec.sh -- Bash support for ZSH-like 'preexec' and 'precmd' functions.# https://github.com/rcaloras/bash-preexec### 'preexec' functions are executed before each interactive command is# executed, with the interactive command as its argument. The 'precmd'# function is executed before each prompt is displayed.## Author: Ryan Caloras (ryan@bashhub.com)# Forked from Original Author: Glyph Lefkowitz## V0.6.0#
# General Usage:## 1. Source this file at the end of your bash profile so as not to interfere# with anything else that's using PROMPT_COMMAND.## 2. Add any precmd or preexec functions by appending them to their arrays:# e.g.# precmd_functions+=(my_precmd_function)# precmd_functions+=(some_other_precmd_function)## preexec_functions+=(my_preexec_function)## 3. Consider changing anything using the DEBUG trap or PROMPT_COMMAND# to use preexec and precmd instead. Preexisting usages will be# preserved, but doing so manually may be less surprising.## Note: This module requires two Bash features which you must not otherwise be# using: the "DEBUG" trap, and the "PROMPT_COMMAND" variable. If you override# either of these after bash-preexec has been installed it will most likely break.
# Tell shellcheck what kind of file this is.# shellcheck shell=bash
# Make sure this is bash that's running and return otherwise.# Use POSIX syntax for this line:if [ -z "${BASH_VERSION-}" ]; then return 1fi
# We only support Bash 3.1+.# Note: BASH_VERSINFO is first available in Bash-2.0.if [[ -z "${BASH_VERSINFO-}" ]] || (( BASH_VERSINFO[0] < 3 || (BASH_VERSINFO[0] == 3 && BASH_VERSINFO[1] < 1) )); then return 1fi
# Avoid duplicate inclusionif [[ -n "${bash_preexec_imported:-}" || -n "${__bp_imported:-}" ]]; then return 0fibash_preexec_imported="defined"
# WARNING: This variable is no longer used and should not be relied upon.# Use ${bash_preexec_imported} instead.# shellcheck disable=SC2034__bp_imported="${bash_preexec_imported}"
# Should be available to each precmd and preexec# functions, should they want it. $? and $_ are available as $? and $_, but# $PIPESTATUS is available only in a copy, $BP_PIPESTATUS.# TODO: Figure out how to restore PIPESTATUS before each precmd or preexec# function.__bp_last_ret_value="$?"BP_PIPESTATUS=("${PIPESTATUS[@]}")__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() { local var for var; do if ! ( unset "$var" 2> /dev/null ); then echo "bash-preexec requires write access to ${var}" >&2 return 1 fi done}
# 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.__bp_adjust_histcontrol() { local histcontrol histcontrol="${HISTCONTROL:-}" histcontrol="${histcontrol//ignorespace}" # Replace ignoreboth with ignoredups if [[ "$histcontrol" == *"ignoreboth"* ]]; then histcontrol="ignoredups:${histcontrol//ignoreboth}" fi export HISTCONTROL="$histcontrol"}
# This variable describes whether we are currently in "interactive mode";# i.e. whether this shell has just executed a prompt and is waiting for user# input. It documents whether the current command invoked by the trace hook is# run interactively by the user; it's set immediately after the prompt hook,# and unset as soon as the trace hook is run.__bp_preexec_interactive_mode=""
# These arrays are used to add functions to be run before, or after, prompts.declare -a precmd_functionsdeclare -a preexec_functions
# Trims leading and trailing whitespace from $2 and writes it to the variable# name passed as $1__bp_trim_whitespace() { local var=${1:?} text=${2:-} text="${text#"${text%%[![:space:]]*}"}" # remove leading whitespace characters text="${text%"${text##*[![:space:]]}"}" # remove trailing whitespace characters printf -v "$var" '%s' "$text"}
# Trims whitespace and removes any leading or trailing semicolons from $2 and# writes the resulting string to the variable name passed as $1. Used for# manipulating substrings in PROMPT_COMMAND__bp_sanitize_string() { local var=${1:?} text=${2:-} sanitized __bp_trim_whitespace sanitized "$text" sanitized=${sanitized%;} sanitized=${sanitized#;} __bp_trim_whitespace sanitized "$sanitized" printf -v "$var" '%s' "$sanitized"}
# 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.__bp_interactive_mode() { __bp_preexec_interactive_mode="on"}
# This function is installed as part of the PROMPT_COMMAND.# It will invoke any functions defined in the precmd_functions array.__bp_precmd_invoke_cmd() { # Save the returned value from our last command, and from each process in # its pipeline. Note: this MUST be the first thing done in this function. # BP_PIPESTATUS may be unused, ignore # shellcheck disable=SC2034
__bp_last_ret_value="$?" BP_PIPESTATUS=("${PIPESTATUS[@]}")
# Don't invoke precmds if we are inside an execution of an "original # prompt command" by another precmd execution loop. This avoids infinite # recursion. if (( __bp_inside_precmd > 0 )); then return fi local __bp_inside_precmd=1
# Invoke every function defined in our function array. local precmd_function for precmd_function in "${precmd_functions[@]}"; do
# Only execute this function if it actually exists. # Test existence of functions with: declare -[Ff] if type -t "$precmd_function" 1>/dev/null; then __bp_set_ret_value "$__bp_last_ret_value" "$__bp_last_argument_prev_command" # Quote our function invocation to prevent issues with IFS "$precmd_function" fi done
__bp_set_ret_value "$__bp_last_ret_value"}
# Sets a return value in $?. We may want to get access to the $? variable in our# precmd functions. This is available for instance in zsh. We can simulate it in bash# by setting the value here.__bp_set_ret_value() { return ${1:+"$1"}}
__bp_in_prompt_command() {
local prompt_command_array IFS=$'\n;' read -rd '' -a prompt_command_array <<< "${PROMPT_COMMAND[*]:-}"
local trimmed_arg __bp_trim_whitespace trimmed_arg "${1:-}"
local command trimmed_command for command in "${prompt_command_array[@]:-}"; do __bp_trim_whitespace trimmed_command "$command" if [[ "$trimmed_command" == "$trimmed_arg" ]]; then return 0 fi done
return 1}
# This function is installed as the DEBUG trap. It is invoked before each# interactive prompt display. Its purpose is to inspect the current# 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:-}" # Don't invoke preexecs if we are inside of another preexec. if (( __bp_inside_preexec > 0 )); then return fi local __bp_inside_preexec=1
# Checks if the file descriptor is not standard out (i.e. '1') # __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 fi
if [[ -n "${COMP_POINT:-}" || -n "${READLINE_POINT:-}" ]]; then # We're in the middle of a completer or a keybinding set up by "bind # -x". This obviously can't be an interactively issued command. return fi if [[ -z "${__bp_preexec_interactive_mode:-}" ]]; then # We're doing something related to displaying the prompt. Let the # prompt set the title instead of me. return else # If we're in a subshell, then the prompt won't be re-displayed to put # us back into interactive mode, so let's not set the variable back. # In other words, if you have a subshell like # (sleep 1; sleep 2) # You want to see the 'sleep 2' as a set_command_title as well. if [[ 0 -eq "${BASH_SUBSHELL:-}" ]]; then __bp_preexec_interactive_mode="" fi fi
if __bp_in_prompt_command "${BASH_COMMAND:-}"; then # If we're executing something inside our prompt_command then we don't # want to call preexec. Bash prior to 3.1 can't detect this at all :/ __bp_preexec_interactive_mode="" return fi
local this_command this_command=$(LC_ALL=C HISTTIMEFORMAT='' builtin history 1) this_command="${this_command#*[[:digit:]][* ] }"
# Sanity check to make sure we have something to invoke our function with. if [[ -z "$this_command" ]]; then return fi
# Invoke every function defined in our function array. local preexec_function local preexec_function_ret_value local preexec_ret_value=0 for preexec_function in "${preexec_functions[@]:-}"; do
# Only execute each function if it actually exists. # Test existence of function with: declare -[fF] if type -t "$preexec_function" 1>/dev/null; then __bp_set_ret_value "${__bp_last_ret_value:-}" # Quote our function invocation to prevent issues with IFS "$preexec_function" "$this_command" preexec_function_ret_value="$?" if [[ "$preexec_function_ret_value" != 0 ]]; then preexec_ret_value="$preexec_function_ret_value" fi fi done
# Restore the last argument of the last executed command, and set the return # value of the DEBUG trap to be the return code of the last preexec function # to return an error. # If `extdebug` is enabled a non-zero return value from any preexec function # will cause the user's command not to execute. # Run `shopt -s extdebug` to enable __bp_set_ret_value "$preexec_ret_value" "$__bp_last_argument_prev_command"}
__bp_install() { # Exit if we already have this installed. if [[ "${PROMPT_COMMAND[*]:-}" == *"__bp_precmd_invoke_cmd"* ]]; then return 1 fi
trap '__bp_preexec_invoke_exec "$_"' DEBUG
# Preserve any prior DEBUG trap as a preexec function eval "local trap_argv=(${__bp_trap_string:-})" local prior_trap=${trap_argv[2]:-} unset __bp_trap_string if [[ -n "$prior_trap" ]]; then eval '__bp_original_debug_trap() { '"$prior_trap"' }' preexec_functions+=(__bp_original_debug_trap) fi
# Adjust our HISTCONTROL Variable if needed. # # GHOSTTY: Don't modify HISTCONTROL. This hack is only needed to improve the # accuracy of the command argument passed to the preexec functions, and we # don't use that argument in our bash shell integration script (and nor does # the __bp_original_debug_trap function above, which is the only other active # preexec function). #__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. # # Disabling this by default. It can be enabled by setting this variable. if [[ -n "${__bp_enable_subshells:-}" ]]; then
# Set so debug trap will work be invoked in subshells. set -o functrace > /dev/null 2>&1 shopt -s extdebug > /dev/null 2>&1 fi
local existing_prompt_command # Remove setting our trap install string and sanitize the existing prompt command string existing_prompt_command="${PROMPT_COMMAND:-}" # Edge case of appending to PROMPT_COMMAND existing_prompt_command="${existing_prompt_command//$__bp_install_string/:}" # no-op existing_prompt_command="${existing_prompt_command//$'\n':$'\n'/$'\n'}" # remove known-token only existing_prompt_command="${existing_prompt_command//$'\n':;/$'\n'}" # remove known-token only __bp_sanitize_string existing_prompt_command "$existing_prompt_command" if [[ "${existing_prompt_command:-:}" == ":" ]]; then existing_prompt_command= fi
# Install our hooks in PROMPT_COMMAND to allow our trap to know when we've # actually entered something. PROMPT_COMMAND='__bp_precmd_invoke_cmd' PROMPT_COMMAND+=${existing_prompt_command:+$'\n'$existing_prompt_command} if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then PROMPT_COMMAND+=('__bp_interactive_mode') else # shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0 PROMPT_COMMAND+=$'\n__bp_interactive_mode' fi
# Add two functions to our arrays for convenience # of definition. precmd_functions+=(precmd) preexec_functions+=(preexec)
# Invoke our two functions manually that were added to $PROMPT_COMMAND __bp_precmd_invoke_cmd __bp_interactive_mode}
# 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.__bp_install_after_session_init() { # bash-preexec needs to modify these variables in order to work correctly # if it can't, just stop the installation __bp_require_not_readonly PROMPT_COMMAND HISTCONTROL HISTTIMEFORMAT || return
local sanitized_prompt_command __bp_sanitize_string sanitized_prompt_command "${PROMPT_COMMAND:-}" if [[ -n "$sanitized_prompt_command" ]]; then # shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0 PROMPT_COMMAND=${sanitized_prompt_command}$'\n' fi # shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0 PROMPT_COMMAND+=${__bp_install_string}}
# Run our install so long as we're not delaying it.if [[ -z "${__bp_delay_install:-}" ]]; then __bp_install_after_session_initfi