From a94ca71c73fc6767ed05dd86588f3858fe3e00af Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Thu, 21 Nov 2019 10:28:05 +0100 Subject: [PATCH] update iterm2 shell integration --- iterm2/iterm2_integration.sh | 264 +++++++++++++++++++++++------------ 1 file changed, 172 insertions(+), 92 deletions(-) diff --git a/iterm2/iterm2_integration.sh b/iterm2/iterm2_integration.sh index ef2bc0a..55b4677 100755 --- a/iterm2/iterm2_integration.sh +++ b/iterm2/iterm2_integration.sh @@ -1,15 +1,39 @@ #!/bin/bash +# The iTerm2 customizations fall under the following license: +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + # -- BEGIN ITERM2 CUSTOMIZATIONS -- -if [[ "$TERM" != screen && "$ITERM_SHELL_INTEGRATION_INSTALLED" = "" && "$-" == *i* ]]; then +if [[ "$ITERM_ENABLE_SHELL_INTEGRATION_WITH_TMUX""$TERM" != screen && "$ITERM_SHELL_INTEGRATION_INSTALLED" = "" && "$-" == *i* && "$TERM" != linux && "$TERM" != dumb ]]; then + +if shopt extdebug | grep on > /dev/null; then + echo "iTerm2 Shell Integration not installed." + echo "" + echo "Your shell has 'extdebug' turned on." + echo "This is incompatible with shell integration." + echo "Find 'shopt -s extdebug' in bash's rc scripts and remove it." + return 0 +fi + ITERM_SHELL_INTEGRATION_INSTALLED=Yes # Saved copy of your PS1. This is used to detect if the user changes PS1 # directly. ITERM_PREV_PS1 will hold the last value that this script set PS1 to # (including various custom escape sequences). ITERM_PREV_PS1="$PS1" -# -- END ITERM2 CUSTOMIZATIONS -- - # The following chunk of code, bash-preexec.sh, is licensed like this: # The MIT License # @@ -33,6 +57,9 @@ ITERM_PREV_PS1="$PS1" # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# Wrap bash-preexec.sh in a function so that, if it exits early due to having +# been sourced elsewhere, it doesn't exit our entire script. +_install_bash_preexec () { # -- BEGIN BASH-PREEXEC.SH -- #!/bin/bash # @@ -47,7 +74,22 @@ ITERM_PREV_PS1="$PS1" # Author: Ryan Caloras (ryan@bashhub.com) # Forked from Original Author: Glyph Lefkowitz # -# V0.3.3 +# V0.3.7 +# +# -- END ITERM2 CUSTOMIZATIONS -- + +# 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.3.7 # # General Usage: @@ -62,28 +104,43 @@ ITERM_PREV_PS1="$PS1" # # preexec_functions+=(my_preexec_function) # -# 3. If you have anything that's using the Debug Trap, change it to use -# preexec. (Optional) change anything using PROMPT_COMMAND to now use -# precmd instead. +# 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. prexec_and_precmd_install -# will override these and if you override one or the other this will most likely break. +# 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. # Avoid duplicate inclusion -if [[ "$__bp_imported" == "defined" ]]; then +if [[ "${__bp_imported:-}" == "defined" ]]; then return 0 fi __bp_imported="defined" # Should be available to each precmd and preexec -# functions, should they want it. +# 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="$_" -# 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;" +__bp_inside_precmd=0 +__bp_inside_preexec=0 + +# 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 "iTerm2 Shell Integration: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 @@ -123,11 +180,19 @@ __bp_interactive_mode() { # 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_last_ret_value="$?" BP_PIPESTATUS=("${PIPESTATUS[@]}") - # Save the returned value from our last command - __bp_last_ret_value="$?" + # 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 - # For every function defined in our function array. Invoke it. + # Invoke every function defined in our function array. local precmd_function for precmd_function in "${precmd_functions[@]}"; do @@ -135,7 +200,8 @@ __bp_precmd_invoke_cmd() { # 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" - $precmd_function + # Quote our function invocation to prevent issues with IFS + "$precmd_function" fi done } @@ -144,7 +210,7 @@ __bp_precmd_invoke_cmd() { # 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 + return ${1:-} } __bp_in_prompt_command() { @@ -153,10 +219,10 @@ __bp_in_prompt_command() { IFS=';' read -ra prompt_command_array <<< "$PROMPT_COMMAND" local trimmed_arg - trimmed_arg=$(__bp_trim_whitespace "$1") + trimmed_arg=$(__bp_trim_whitespace "${1:-}") local command - for command in "${prompt_command_array[@]}"; do + for command in "${prompt_command_array[@]:-}"; do local trimmed_command trimmed_command=$(__bp_trim_whitespace "$command") # Only execute each function if it actually exists. @@ -173,25 +239,28 @@ __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" + __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 + if [[ ! -t 1 && -z "${__bp_delay_install:-}" ]]; then return fi - if [[ -n "$COMP_LINE" ]]; then + if [[ -n "${COMP_LINE:-}" ]]; then # We're in the middle of a completer. This obviously can't be # an interactively issued command. return fi - if [[ -z "$__bp_preexec_interactive_mode" ]]; then + 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 @@ -201,12 +270,12 @@ __bp_preexec_invoke_exec() { # 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 + if [[ 0 -eq "${BASH_SUBSHELL:-}" ]]; then __bp_preexec_interactive_mode="" fi fi - if __bp_in_prompt_command "$BASH_COMMAND"; then + 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="" @@ -214,7 +283,10 @@ __bp_preexec_invoke_exec() { fi local this_command - this_command=$(HISTTIMEFORMAT= builtin history 1 | { read -r _ this_command; echo "$this_command"; }) + this_command=$( + export LC_ALL=C + HISTTIMEFORMAT= builtin history 1 | sed '1 s/^ *[0-9][0-9]*[* ] //' + ) # Sanity check to make sure we have something to invoke our function with. if [[ -z "$this_command" ]]; then @@ -225,61 +297,50 @@ __bp_preexec_invoke_exec() { # the command is in fact interactive and we should invoke the user's # preexec functions. - # For every function defined in our function array. Invoke it. + # 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 + 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 - $preexec_function "$this_command" - preexec_ret_value="$?" + __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 - # Also preserves the return value of the last function executed in preexec - # If `extdebug` is enabled a non-zero return value from the last function - # in prexec causes the command not to execute + # 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" } -# Returns PROMPT_COMMAND with a semicolon appended -# if it doesn't already have one. -__bp_prompt_command_with_semi_colon() { - - # 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="" +__bp_install() { + # Exit if we already have this installed. + if [[ "${PROMPT_COMMAND:-}" == *"__bp_precmd_invoke_cmd"* ]]; then + return 1; fi - echo -n "$existing_prompt_command" -} + trap '__bp_preexec_invoke_exec "$_"' DEBUG -__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; + # Preserve any prior DEBUG trap as a preexec function + local prior_trap=$(sed "s/[^']*'\(.*\)'[^']*/\1/" <<<"${__bp_trap_string:-}") + 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. @@ -290,59 +351,67 @@ __bp_install() { # 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 + 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 - existing_prompt_command=$(__bp_prompt_command_with_semi_colon) - # 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" - eval "$__bp_trap_install_string" + PROMPT_COMMAND="__bp_precmd_invoke_cmd; __bp_interactive_mode" # 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 + # Since this function is invoked via PROMPT_COMMAND, re-execute PC now that it's properly set + eval "$PROMPT_COMMAND" } # 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 +# 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. __bp_install_after_session_init() { # Make sure this is bash that's running this and return otherwise. - if [[ -z "$BASH_VERSION" ]]; then + if [[ -z "${BASH_VERSION:-}" ]]; then return 1; fi - local existing_prompt_command - existing_prompt_command=$(__bp_prompt_command_with_semi_colon) + # 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 - # 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;" + # 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' } # Run our install so long as we're not delaying it. if [[ -z "$__bp_delay_install" ]]; then __bp_install_after_session_init fi; - -# -- BEGIN BASH-PREEXEC.SH -- +# -- END BASH-PREEXEC.SH -- +} +_install_bash_preexec +unset -f _install_bash_preexec # -- BEGIN ITERM2 CUSTOMIZATIONS -- @@ -410,7 +479,7 @@ function iterm2_prompt_suffix() { function iterm2_print_version_number() { iterm2_begin_osc - printf "1337;ShellIntegrationVersion=9;shell=bash" + printf "1337;ShellIntegrationVersion=14;shell=bash" iterm2_end_osc } @@ -490,6 +559,15 @@ function __iterm2_precmd () { export ITERM_ORIG_PS1="$PS1" fi + # If you want to generate PS1 dynamically from PROMPT_COMMAND, the best way + # to do it is to define a function named iterm2_generate_ps1 that sets PS1. + # Issue 5964. Other shells don't have this issue because they don't need + # such extremes to get precmd and preexec. + if [ -n "$(type -t iterm2_generate_ps1)" ] && [ "$(type -t iterm2_generate_ps1)" = function ]; then + iterm2_generate_ps1 + fi + + if [[ "$PS1" != "$ITERM_PREV_PS1" ]] then export ITERM_ORIG_PS1="$PS1" @@ -499,7 +577,7 @@ function __iterm2_precmd () { \local iterm2_prompt_prefix_value="$(iterm2_prompt_prefix)" # Add the mark unless the prompt includes '$(iterm2_prompt_mark)' as a substring. - if [[ $ITERM_ORIG_PS1 != *'$(iterm2_prompt_mark)'* ]] + if [[ $ITERM_ORIG_PS1 != *'$(iterm2_prompt_mark)'* && x$ITERM2_SQUELCH_MARK = x ]] then iterm2_prompt_prefix_value="$iterm2_prompt_prefix_value$(iterm2_prompt_mark)" fi @@ -522,6 +600,8 @@ function __iterm2_precmd () { preexec_functions+=(__iterm2_preexec) precmd_functions+=(__iterm2_precmd) +iterm2_print_state_data +iterm2_print_version_number fi # -- END ITERM2 CUSTOMIZATIONS --