diff --git a/docs/themes.rst b/docs/themes.rst index 8cfbeb2346..e1ce8073a9 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -53,12 +53,15 @@ Command duration can be enabled by exporting ``BASH_IT_COMMAND_DURATION``: export BASH_IT_COMMAND_DURATION=true -The default configuration display last command duration for command lasting one second or more. -You can customize the minimum time in seconds before command duration is displayed in your ``.bashrc``: +The default configuration display last command duration for command lasting one second or more, +with deciseconds precision. + +You can customize the minimum time in seconds before command duration is displayed or the precison in your ``.bashrc``: .. code-block:: bash export COMMAND_DURATION_MIN_SECONDS=5 + export COMMAND_DURATION_PRECISION=2 Clock Related ============= diff --git a/lib/command_duration.bash b/lib/command_duration.bash index 850c67649e..23b4cc232d 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -2,24 +2,41 @@ # # Functions for measuring and reporting how long a command takes to run. +# Notice: This function used to run as a sub-shell while defining: +# local LC_ALL=C +# +# DFARREL You would think LC_NUMERIC would do it, but not working in my local. +# Note: LC_ALL='en_US.UTF-8' has been used to enforce the decimal point to be +# a period, but the specific locale 'en_US.UTF-8' is not ensured to exist in +# the system. One should instead use the locale 'C', which is ensured by the +# C and POSIX standards. +# +# We now use EPOCHREALTIME, while replacing any non-digit character by a period. +# +# Technically, one can define a locale with decimal_point being an arbitrary string. +# For example, ps_AF uses U+066B as the decimal point. +# +# cf: https://github.com/Bash-it/bash-it/pull/2366#discussion_r2760681820 +# # Get shell duration in decimal format regardless of runtime locale. -# Notice: This function runs as a sub-shell - notice '(' vs '{'. -function _shell_duration_en() ( - # DFARREL You would think LC_NUMERIC would do it, but not working in my local. - # Note: LC_ALL='en_US.UTF-8' has been used to enforce the decimal point to be - # a period, but the specific locale 'en_US.UTF-8' is not ensured to exist in - # the system. One should instead use the locale 'C', which is ensured by the - # C and POSIX standards. - local LC_ALL=C - printf "%s" "${EPOCHREALTIME:-$SECONDS}" -) - -: "${COMMAND_DURATION_START_SECONDS:=$(_shell_duration_en)}" +function _command_duration_current_time() { + local current_time + if [[ -n "${EPOCHREALTIME:-}" ]]; then + current_time="${EPOCHREALTIME//[!0-9]/.}" + else + current_time="$SECONDS" + fi + + echo "$current_time" +} + +: "${COMMAND_DURATION_START_SECONDS:=$(_command_duration_current_time)}" : "${COMMAND_DURATION_ICON:=🕘}" : "${COMMAND_DURATION_MIN_SECONDS:=1}" +: "${COMMAND_DURATION_PRECISION:=1}" function _command_duration_pre_exec() { - COMMAND_DURATION_START_SECONDS="$(_shell_duration_en)" + COMMAND_DURATION_START_SECONDS="$(_command_duration_current_time)" } function _command_duration_pre_cmd() { @@ -27,10 +44,16 @@ function _command_duration_pre_cmd() { } function _dynamic_clock_icon { - local clock_hand + local clock_hand duration="$1" + + # Clock only work for time >= 1s + if ((duration < 1)); then + duration=1 + fi + # clock hand value is between 90 and 9b in hexadecimal. # so between 144 and 155 in base 10. - printf -v clock_hand '%x' $((((${1:-${SECONDS}} - 1) % 12) + 144)) + printf -v clock_hand '%x' $((((${duration:-${SECONDS}} - 1) % 12) + 144)) printf -v 'COMMAND_DURATION_ICON' '%b' "\xf0\x9f\x95\x$clock_hand" } @@ -38,29 +61,34 @@ function _command_duration() { [[ -n "${BASH_IT_COMMAND_DURATION:-}" ]] || return [[ -n "${COMMAND_DURATION_START_SECONDS:-}" ]] || return - local command_duration=0 command_start="${COMMAND_DURATION_START_SECONDS:-0}" - local -i minutes=0 seconds=0 deciseconds=0 - local -i command_start_seconds="${command_start%.*}" - local -i command_start_deciseconds=$((10#${command_start##*.})) - command_start_deciseconds="${command_start_deciseconds:0:1}" local current_time - current_time="$(_shell_duration_en)" - local -i current_time_seconds="${current_time%.*}" - local -i current_time_deciseconds="$((10#${current_time##*.}))" - current_time_deciseconds="${current_time_deciseconds:0:1}" + current_time="$(_command_duration_current_time)" + + local -i command_duration=0 + local -i minutes=0 seconds=0 + local microseconds="" - if [[ "${command_start_seconds:-0}" -gt 0 ]]; then - # seconds - command_duration="$((current_time_seconds - command_start_seconds))" + local -i command_start_seconds=${COMMAND_DURATION_START_SECONDS%.*} + local -i current_time_seconds=${current_time%.*} - if ((current_time_deciseconds >= command_start_deciseconds)); then - deciseconds="$((current_time_deciseconds - command_start_deciseconds))" + # Calculate seconds difference + command_duration=$((current_time_seconds - command_start_seconds)) + + # Calculate microseconds if both timestamps have fractional parts + if [[ "$COMMAND_DURATION_START_SECONDS" == *.* ]] && [[ "$current_time" == *.* ]] && ((COMMAND_DURATION_PRECISION > 0)); then + local -i command_start_microseconds=$((10#${COMMAND_DURATION_START_SECONDS##*.})) + local -i current_time_microseconds=$((10#${current_time##*.})) + + if ((current_time_microseconds >= command_start_microseconds)); then + microseconds=$((current_time_microseconds - command_start_microseconds)) else ((command_duration -= 1)) - deciseconds="$((10 - (command_start_deciseconds - current_time_deciseconds)))" + microseconds=$((1000000 + current_time_microseconds - command_start_microseconds)) fi - else - command_duration=0 + + # Pad with leading zeros to 6 digits, then take first N digits + printf -v microseconds '%06d' "$microseconds" + microseconds="${microseconds:0:$COMMAND_DURATION_PRECISION}" fi if ((command_duration >= COMMAND_DURATION_MIN_SECONDS)); then @@ -71,7 +99,7 @@ function _command_duration() { if ((minutes > 0)); then printf "%s %s%dm %ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$minutes" "$seconds" else - printf "%s %s%d.%01ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds" "$deciseconds" + printf "%s %s%ss" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds${microseconds:+.$microseconds}" fi fi } diff --git a/plugins/available/cmd-returned-notify.plugin.bash b/plugins/available/cmd-returned-notify.plugin.bash index 28c5abc10f..b98bac57ac 100644 --- a/plugins/available/cmd-returned-notify.plugin.bash +++ b/plugins/available/cmd-returned-notify.plugin.bash @@ -4,9 +4,9 @@ about-plugin 'Alert (BEL) when process ends after a threshold of seconds' url "https://github.com/Bash-it/bash-it" function precmd_return_notification() { - local command_start="${COMMAND_DURATION_START_SECONDS:=0}" - local current_time - current_time="$(_shell_duration_en)" + local command_start="${COMMAND_DURATION_START_SECONDS:=0}" current_time + current_time="$(_command_duration_current_time)" + local -i command_duration="$((${current_time%.*} - ${command_start%.*}))" if [[ "${command_duration}" -gt "${NOTIFY_IF_COMMAND_RETURNS_AFTER:-5}" ]]; then printf '\a' diff --git a/test/lib/command_duration.bats b/test/lib/command_duration.bats new file mode 100644 index 0000000000..691ab984d0 --- /dev/null +++ b/test/lib/command_duration.bats @@ -0,0 +1,142 @@ +# shellcheck shell=bats +# shellcheck disable=2034,2329 + +load "${MAIN_BASH_IT_DIR?}/test/test_helper.bash" + +function local_setup_file() { + setup_libs "command_duration" +} + +@test "command_duration: _command_duration_current_time" { + run _command_duration_current_time + assert_success + assert_output --regexp '^[0-9]+(\.[0-9]+)?$' +} + +@test "command_duration: _command_duration_current_time without EPOCHREALTIME" { + _command_duration_current_time_no_epoch() { + local EPOCHREALTIME + unset EPOCHREALTIME + local SECONDS=123 + _command_duration_current_time + } + run _command_duration_current_time_no_epoch + assert_success + assert_output "123" +} + +@test "command_duration: _command_duration_pre_exec" { + _command_duration_pre_exec + assert [ -n "$COMMAND_DURATION_START_SECONDS" ] +} + +@test "command_duration: _command_duration_pre_cmd" { + COMMAND_DURATION_START_SECONDS="1234.567" + _command_duration_pre_cmd + assert [ -z "$COMMAND_DURATION_START_SECONDS" ] +} + +@test "command_duration: _dynamic_clock_icon" { + _dynamic_clock_icon 1 + assert [ -n "$COMMAND_DURATION_ICON" ] +} + +@test "command_duration: _command_duration disabled" { + unset BASH_IT_COMMAND_DURATION + COMMAND_DURATION_START_SECONDS="100" + run _command_duration + assert_output "" +} + +@test "command_duration: _command_duration no start time" { + BASH_IT_COMMAND_DURATION=true + unset COMMAND_DURATION_START_SECONDS + run _command_duration + assert_output "" +} + +@test "command_duration: _command_duration below threshold" { + BASH_IT_COMMAND_DURATION=true + COMMAND_DURATION_MIN_SECONDS=2 + # Mock _command_duration_current_time + _command_duration_current_time() { echo 101; } + COMMAND_DURATION_START_SECONDS=100 + run _command_duration + assert_output "" +} + +@test "command_duration: _command_duration above threshold (seconds)" { + BASH_IT_COMMAND_DURATION=true + COMMAND_DURATION_MIN_SECONDS=1 + COMMAND_DURATION_PRECISION=0 + # Mock _command_duration_current_time + _command_duration_current_time() { echo 105; } + COMMAND_DURATION_START_SECONDS=100 + run _command_duration + assert_output --regexp ".* 5s$" +} + +@test "command_duration: _command_duration precision 0 with microseconds time" { + BASH_IT_COMMAND_DURATION=true + COMMAND_DURATION_MIN_SECONDS=1 + COMMAND_DURATION_PRECISION=0 + # Mock _command_duration_current_time + _command_duration_current_time() { echo 105.600005; } + COMMAND_DURATION_START_SECONDS=100.200007 + run _command_duration + assert_output --regexp ".* 5s$" +} + +@test "command_duration: _command_duration with precision" { + BASH_IT_COMMAND_DURATION=true + COMMAND_DURATION_MIN_SECONDS=1 + COMMAND_DURATION_PRECISION=1 + # Mock _command_duration_current_time + _command_duration_current_time() { echo 105.600000; } + COMMAND_DURATION_START_SECONDS=100.200000 + run _command_duration + assert_output --regexp ".* 5.4s$" +} + +@test "command_duration: _command_duration with minutes" { + BASH_IT_COMMAND_DURATION=true + COMMAND_DURATION_MIN_SECONDS=1 + # Mock _command_duration_current_time + _command_duration_current_time() { echo 200; } + COMMAND_DURATION_START_SECONDS=70 + run _command_duration + assert_output --regexp ".* 2m 10s$" +} + +@test "command_duration: _command_duration with microsecond rollover" { + BASH_IT_COMMAND_DURATION=true + COMMAND_DURATION_MIN_SECONDS=0 + COMMAND_DURATION_PRECISION=1 + # Mock _command_duration_current_time + # 105.1 - 100.2 = 4.9 + _command_duration_current_time() { echo 105.100000; } + COMMAND_DURATION_START_SECONDS=100.200000 + run _command_duration + assert_output --regexp ".* 4.9s$" +} + +@test "command_duration: _command_duration with precision and leading zeros" { + BASH_IT_COMMAND_DURATION=true + COMMAND_DURATION_MIN_SECONDS=0 + COMMAND_DURATION_PRECISION=3 + COMMAND_DURATION_START_SECONDS=100.001000 + _command_duration_current_time() { echo 105.002000; } + run _command_duration + assert_output --regexp ".* 5.001s$" +} + +@test "command_duration: _command_duration without EPOCHREALTIME (SECONDS only)" { + BASH_IT_COMMAND_DURATION=true + COMMAND_DURATION_MIN_SECONDS=1 + COMMAND_DURATION_PRECISION=1 + # Mock _command_duration_current_time to return integer (like SECONDS would) + _command_duration_current_time() { echo 105; } + COMMAND_DURATION_START_SECONDS=100 + run _command_duration + assert_output --regexp ".* 5s$" +} diff --git a/test/lib/preexec.bats b/test/lib/preexec.bats index 3c5ed4b041..5132991fb1 100644 --- a/test/lib/preexec.bats +++ b/test/lib/preexec.bats @@ -60,7 +60,11 @@ function local_setup { assert_success __bp_install - assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n__bp_interactive_mode' + if ((BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1))); then + assert_equal "${PROMPT_COMMAND[*]}" $'__bp_precmd_invoke_cmd __bp_interactive_mode' + else + assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n__bp_interactive_mode' + fi } @test "vendor preexec: __bp_install() with existing" { @@ -75,7 +79,11 @@ function local_setup { assert_success __bp_install - assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n'"$test_prompt_string"$'\n__bp_interactive_mode' + if ((BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1))); then + assert_equal "${PROMPT_COMMAND[*]}" $'__bp_precmd_invoke_cmd\n'"$test_prompt_string"$'\n: __bp_interactive_mode' + else + assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n'"$test_prompt_string"$'\n:\n__bp_interactive_mode' + fi } @test "lib preexec: __bp_require_not_readonly()" { diff --git a/test/plugins/cmd-returned-notify.plugin.bats b/test/plugins/cmd-returned-notify.plugin.bats index 28a3666d67..bd36945040 100644 --- a/test/plugins/cmd-returned-notify.plugin.bats +++ b/test/plugins/cmd-returned-notify.plugin.bats @@ -10,7 +10,7 @@ function local_setup_file() { @test "plugins cmd-returned-notify: notify after elapsed time" { NOTIFY_IF_COMMAND_RETURNS_AFTER=0 - COMMAND_DURATION_START_SECONDS="$(_shell_duration_en)" + COMMAND_DURATION_START_SECONDS="$(_command_duration_current_time)" export COMMAND_DURATION_START_SECONDS NOTIFY_IF_COMMAND_RETURNS_AFTER sleep 1 run precmd_return_notification @@ -20,7 +20,7 @@ function local_setup_file() { @test "plugins cmd-returned-notify: do not notify before elapsed time" { NOTIFY_IF_COMMAND_RETURNS_AFTER=10 - COMMAND_DURATION_START_SECONDS="$(_shell_duration_en)" + COMMAND_DURATION_START_SECONDS="$(_command_duration_current_time)" export COMMAND_DURATION_START_SECONDS NOTIFY_IF_COMMAND_RETURNS_AFTER sleep 1 run precmd_return_notification @@ -37,7 +37,7 @@ function local_setup_file() { @test "lib command_duration: preexec set COMMAND_DURATION_START_SECONDS" { COMMAND_DURATION_START_SECONDS= assert_equal "${COMMAND_DURATION_START_SECONDS}" "" - NOW="$(_shell_duration_en)" + NOW="$(_command_duration_current_time)" _command_duration_pre_exec # We need to make sure to account for nanoseconds... assert_equal "${COMMAND_DURATION_START_SECONDS%.*}" "${NOW%.*}" diff --git a/vendor/github.com/rcaloras/bash-preexec/.github/workflows/bats.yaml b/vendor/github.com/rcaloras/bash-preexec/.github/workflows/bats.yaml new file mode 100644 index 0000000000..8507cfef71 --- /dev/null +++ b/vendor/github.com/rcaloras/bash-preexec/.github/workflows/bats.yaml @@ -0,0 +1,15 @@ +name: Bats tests +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-24.04 + steps: + - name: Install Bats + run: | + sudo apt-get update + sudo apt-get install --assume-yes bats + - name: Check out repository + uses: actions/checkout@v2 + - name: Run tests + run: bats test diff --git a/vendor/github.com/rcaloras/bash-preexec/.travis.yml b/vendor/github.com/rcaloras/bash-preexec/.travis.yml deleted file mode 100644 index 4f0c8610a5..0000000000 --- a/vendor/github.com/rcaloras/bash-preexec/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: bash - -before_install: - # To install bats and test our shell/bash functions - - git clone -b "v1.1.0" "https://github.com/bats-core/bats-core.git" - - sudo ./bats-core/install.sh /usr/local - - rm -rf ./bats-core - - sudo apt-get install -qq zsh - -# For bats functional tests -env: - - functional_test="true" - -# command to run tests -script: - - /usr/local/bin/bats test - -notifications: - email: - on_success: never diff --git a/vendor/github.com/rcaloras/bash-preexec/README.md b/vendor/github.com/rcaloras/bash-preexec/README.md index 3e88c844fe..9efdbe40f5 100644 --- a/vendor/github.com/rcaloras/bash-preexec/README.md +++ b/vendor/github.com/rcaloras/bash-preexec/README.md @@ -1,18 +1,18 @@ -[![Build Status](https://travis-ci.org/rcaloras/bash-preexec.svg?branch=master)](https://travis-ci.org/rcaloras/bash-preexec) +[![Build Status](https://github.com/rcaloras/bash-preexec/actions/workflows/bats.yaml/badge.svg)](https://github.com/rcaloras/bash-preexec/actions/) [![GitHub version](https://badge.fury.io/gh/rcaloras%2Fbash-preexec.svg)](https://badge.fury.io/gh/rcaloras%2Fbash-preexec) Bash-Preexec ============ -**preexec** and **precmd** hook functions for Bash in the style of Zsh. They aim to emulate the behavior [as described for Zsh](http://zsh.sourceforge.net/Doc/Release/Functions.html#Hook-Functions). +**preexec** and **precmd** hook functions for Bash 3.1+ in the style of Zsh. They aim to emulate the behavior [as described for Zsh](http://zsh.sourceforge.net/Doc/Release/Functions.html#Hook-Functions). Bashhub Logo -This project is currently being used in production by [Bashhub](https://github.com/rcaloras/bashhub-client) and [iTerm2](https://github.com/gnachman/iTerm2). Hype! +This project is currently being used in production by [Bashhub](https://github.com/rcaloras/bashhub-client), [iTerm2](https://github.com/gnachman/iTerm2), and [Fig](https://fig.io). Hype! ## Quick Start ```bash -# Pull down our file from GitHub and write it to our home directory as a hidden file. +# Pull down our file from GitHub and write it to your home directory as a hidden file. curl https://raw.githubusercontent.com/rcaloras/bash-preexec/master/bash-preexec.sh -o ~/.bash-preexec.sh # Source our file to bring it into our environment source ~/.bash-preexec.sh @@ -24,7 +24,7 @@ precmd() { echo "printing the prompt"; } ## Install You'll want to pull down the file and add it to your bash profile/configuration (i.e ~/.bashrc, ~/.profile, ~/.bash_profile, etc). **It must be the last thing imported in your bash profile.** ```bash -# Pull down our file from GitHub and write it to our home directory as a hidden file. +# Pull down our file from GitHub and write it to your home directory as a hidden file. curl https://raw.githubusercontent.com/rcaloras/bash-preexec/master/bash-preexec.sh -o ~/.bash-preexec.sh # Source our file at the end of our bash profile (e.g. ~/.bashrc, ~/.profile, or ~/.bash_profile) echo '[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' >> ~/.bashrc @@ -91,6 +91,15 @@ export __bp_enable_subshells="true" ``` This is disabled by default due to buggy situations related to to `functrace` and Bash's `DEBUG trap`. See [Issue #25](https://github.com/rcaloras/bash-preexec/issues/25) +## Library authors +If you want to detect bash-preexec in your library (for example, to add hooks to `preexec_functions` when available), use the Bash variable `bash_preexec_imported`: + +```bash +if [[ -n "${bash_preexec_imported:-}" ]]; then + echo "Bash-preexec is loaded." +fi +``` + ## Tests You can run tests using [Bats](https://github.com/bats-core/bats-core). ```bash diff --git a/vendor/github.com/rcaloras/bash-preexec/bash-preexec.sh b/vendor/github.com/rcaloras/bash-preexec/bash-preexec.sh index 5f1208c33e..e0d2fa0fc6 100644 --- a/vendor/github.com/rcaloras/bash-preexec/bash-preexec.sh +++ b/vendor/github.com/rcaloras/bash-preexec/bash-preexec.sh @@ -9,7 +9,7 @@ # Author: Ryan Caloras (ryan@bashhub.com) # Forked from Original Author: Glyph Lefkowitz # -# V0.4.1 +# V0.6.0 # # General Usage: @@ -32,19 +32,30 @@ # 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. -if [[ -z "${BASH_VERSION:-}" ]]; then - return 1; +# Use POSIX syntax for this line: +if [ -z "${BASH_VERSION-}" ]; then + return 1 +fi + +# 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 1 fi # Avoid duplicate inclusion -if [[ -n "${bash_preexec_imported:-}" ]]; then +if [[ -n "${bash_preexec_imported:-}" || -n "${__bp_imported:-}" ]]; then return 0 fi bash_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 @@ -65,13 +76,13 @@ __bp_install_string=$'__bp_trap_string="$(trap -p DEBUG)"\ntrap - DEBUG\n__bp_in # 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 + 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 @@ -84,7 +95,7 @@ __bp_adjust_histcontrol() { # Replace ignoreboth with ignoredups if [[ "$histcontrol" == *"ignoreboth"* ]]; then histcontrol="ignoredups:${histcontrol//ignoreboth}" - fi; + fi export HISTCONTROL="$histcontrol" } @@ -125,7 +136,7 @@ __bp_sanitize_string() { # 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"; + __bp_preexec_interactive_mode="on" } @@ -134,13 +145,16 @@ __bp_interactive_mode() { __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 + return fi local __bp_inside_precmd=1 @@ -156,19 +170,21 @@ __bp_precmd_invoke_cmd() { "$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:-} + return ${1:+"$1"} } __bp_in_prompt_command() { - local prompt_command_array - IFS=$'\n;' read -rd '' -a prompt_command_array <<< "${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:-}" @@ -195,7 +211,7 @@ __bp_preexec_invoke_exec() { __bp_last_argument_prev_command="${1:-}" # Don't invoke preexecs if we are inside of another preexec. if (( __bp_inside_preexec > 0 )); then - return + return fi local __bp_inside_preexec=1 @@ -206,9 +222,9 @@ __bp_preexec_invoke_exec() { return fi - if [[ -n "${COMP_LINE:-}" ]]; then - # We're in the middle of a completer. This obviously can't be - # an interactively issued command. + 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 @@ -234,10 +250,8 @@ __bp_preexec_invoke_exec() { fi local this_command - this_command=$( - export LC_ALL=C - HISTTIMEFORMAT= builtin history 1 | sed '1 s/^ *[0-9][0-9]*[* ] //' - ) + 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 @@ -253,7 +267,7 @@ __bp_preexec_invoke_exec() { # 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:-} + __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="$?" @@ -274,18 +288,19 @@ __bp_preexec_invoke_exec() { __bp_install() { # Exit if we already have this installed. - if [[ "${PROMPT_COMMAND:-}" == *"__bp_precmd_invoke_cmd"* ]]; then - return 1; + 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 - local prior_trap=$(sed "s/[^']*'\(.*\)'[^']*/\1/" <<<"${__bp_trap_string:-}") + 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"' + '"$prior_trap"' }' preexec_functions+=(__bp_original_debug_trap) fi @@ -302,22 +317,30 @@ __bp_install() { # 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; + fi local existing_prompt_command # Remove setting our trap install string and sanitize the existing prompt command string existing_prompt_command="${PROMPT_COMMAND:-}" - existing_prompt_command="${existing_prompt_command//$__bp_install_string[;$'\n']}" # Edge case of appending to PROMPT_COMMAND - existing_prompt_command="${existing_prompt_command//$__bp_install_string}" + # 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\n' - if [[ -n "$existing_prompt_command" ]]; then - PROMPT_COMMAND+=${existing_prompt_command}$'\n' - fi; - PROMPT_COMMAND+='__bp_interactive_mode' + 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. @@ -340,12 +363,14 @@ __bp_install_after_session_init() { 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; + 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_init -fi; +fi diff --git a/vendor/github.com/rcaloras/bash-preexec/test/bash-preexec.bats b/vendor/github.com/rcaloras/bash-preexec/test/bash-preexec.bats index 84a30caea4..08e112d761 100644 --- a/vendor/github.com/rcaloras/bash-preexec/test/bash-preexec.bats +++ b/vendor/github.com/rcaloras/bash-preexec/test/bash-preexec.bats @@ -8,9 +8,23 @@ setup() { source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" } +# Evaluates all the elements of PROMPT_COMMAND +eval_PROMPT_COMMAND() { + local prompt_command + for prompt_command in "${PROMPT_COMMAND[@]}"; do + eval "$prompt_command" + done +} + +# Joins the elements of PROMPT_COMMAND with $'\n' +join_PROMPT_COMMAND() { + local IFS=$'\n' + echo "${PROMPT_COMMAND[*]}" +} + bp_install() { __bp_install_after_session_init - eval "$PROMPT_COMMAND" + eval_PROMPT_COMMAND } test_echo() { @@ -21,13 +35,34 @@ test_preexec_echo() { printf "%s\n" "$1" } -@test "__bp_install_after_session_init should exit with 1 if we're not using bash" { +# Helper functions necessary because Bats' run doesn't preserve $? +return_exit_code() { + return $1 +} + +set_exit_code_and_run_precmd() { + return_exit_code "${1:-0}" + __bp_precmd_invoke_cmd +} + + +@test "sourcing bash-preexec should exit with 1 if we're not using bash" { unset BASH_VERSION - run '__bp_install_after_session_init' + run source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" [ $status -eq 1 ] [ -z "$output" ] } +@test "sourcing bash-preexec should exit with 1 if we're using an older version of bash" { + if type -p bash-3.0 &>/dev/null; then + run bash-3.0 -c "source \"${BATS_TEST_DIRNAME}/../bash-preexec.sh\"" + [ "$status" -eq 1 ] + [ -z "$output" ] + else + skip + fi +} + @test "__bp_install should exit if it's already installed" { bp_install @@ -39,13 +74,14 @@ test_preexec_echo() { @test "__bp_install should remove trap logic and itself from PROMPT_COMMAND" { __bp_install_after_session_init - [[ "$PROMPT_COMMAND" == *"trap - DEBUG"* ]] || return 1 - [[ "$PROMPT_COMMAND" == *"__bp_install"* ]] || return 1 + # Assert that before running, the command contains the install string, and + # afterwards it does not + # shellcheck disable=SC2154 + [[ "$PROMPT_COMMAND" == *"$__bp_install_string"* ]] || return 1 - eval "$PROMPT_COMMAND" + eval_PROMPT_COMMAND - [[ "$PROMPT_COMMAND" != *"trap DEBUG"* ]] || return 1 - [[ "$PROMPT_COMMAND" != *"__bp_install"* ]] || return 1 + [[ "$PROMPT_COMMAND" != *"$__bp_install_string"* ]] || return 1 } @test "__bp_install should preserve an existing DEBUG trap" { @@ -68,6 +104,26 @@ test_preexec_echo() { (( trap_count_snapshot < trap_invoked_count )) } +@test "__bp_install should preserve an existing DEBUG trap containing quotes" { + trap_invoked_count=0 + foo() { (( trap_invoked_count += 1 )); } + + # note setting this causes BATS to mis-report the failure line when this test fails + trap "foo && echo 'hello' >/dev/null" debug + [ "$(trap -p DEBUG | cut -d' ' -f3-7)" == "'foo && echo '\''hello'\'' >/dev/null'" ] + + bp_install + trap_count_snapshot=$trap_invoked_count + + [ "$(trap -p DEBUG | cut -d' ' -f3)" == "'__bp_preexec_invoke_exec" ] + [[ "${preexec_functions[*]}" == *"__bp_original_debug_trap"* ]] || return 1 + + __bp_interactive_mode # triggers the DEBUG trap + + # ensure the trap count is still being incremented after the trap's been overwritten + (( trap_count_snapshot < trap_invoked_count )) +} + @test "__bp_sanitize_string should remove semicolons and trim space" { __bp_sanitize_string output " true1; "$'\n' @@ -85,7 +141,7 @@ test_preexec_echo() { bp_install PROMPT_COMMAND="$PROMPT_COMMAND; true" - eval "$PROMPT_COMMAND" + eval_PROMPT_COMMAND } @test "Appending or prepending to PROMPT_COMMAND should work after bp_install_after_session_init" { @@ -98,7 +154,7 @@ test_preexec_echo() { PROMPT_COMMAND="true; $PROMPT_COMMAND" PROMPT_COMMAND="true; $PROMPT_COMMAND" PROMPT_COMMAND="true $nl $PROMPT_COMMAND" - eval "$PROMPT_COMMAND" + eval_PROMPT_COMMAND } # Case where a user is appending or prepending to PROMPT_COMMAND. @@ -111,10 +167,10 @@ test_preexec_echo() { PROMPT_COMMAND="$PROMPT_COMMAND"$'\n echo after' PROMPT_COMMAND="echo after2; $PROMPT_COMMAND;" - eval "$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" ] + [ "$(join_PROMPT_COMMAND)" == "$expected_result" ] } @test "Adding to PROMPT_COMMAND after with semicolon" { @@ -122,10 +178,10 @@ test_preexec_echo() { __bp_install_after_session_init PROMPT_COMMAND="$PROMPT_COMMAND; echo after" - eval "$PROMPT_COMMAND" + eval_PROMPT_COMMAND expected_result=$'__bp_precmd_invoke_cmd\necho before\n echo after\n__bp_interactive_mode' - [ "$PROMPT_COMMAND" == "$expected_result" ] + [ "$(join_PROMPT_COMMAND)" == "$expected_result" ] } @test "during install PROMPT_COMMAND and precmd functions should be executed each once" { @@ -136,7 +192,7 @@ test_preexec_echo() { PROMPT_COMMAND="echo after2; $PROMPT_COMMAND;" precmd() { echo "inside precmd"; } - run eval "$PROMPT_COMMAND" + run eval_PROMPT_COMMAND [ "${lines[0]}" == "after2" ] [ "${lines[1]}" == "before" ] [ "${lines[2]}" == "before2" ] @@ -155,7 +211,7 @@ test_preexec_echo() { @test "precmd should execute a function once" { precmd_functions+=(test_echo) - run '__bp_precmd_invoke_cmd' + run set_exit_code_and_run_precmd [ $status -eq 0 ] [ "$output" == "test echo" ] } @@ -164,18 +220,10 @@ test_preexec_echo() { echo_exit_code() { echo "$?" } - return_exit_code() { - return $1 - } - # Helper function is necessary because Bats' run doesn't preserve $? - set_exit_code_and_run_precmd() { - return_exit_code 251 - __bp_precmd_invoke_cmd - } precmd_functions+=(echo_exit_code) - run 'set_exit_code_and_run_precmd' - [ $status -eq 0 ] + run set_exit_code_and_run_precmd 251 + [ $status -eq 251 ] [ "$output" == "251" ] } @@ -206,7 +254,7 @@ test_preexec_echo() { : "last-arg" __bp_preexec_invoke_exec "$_" eval "$bats_trap" # Restore trap - run '__bp_precmd_invoke_cmd' + run set_exit_code_and_run_precmd [ $status -eq 0 ] [ "$output" == "last-arg" ] } @@ -242,7 +290,7 @@ test_preexec_echo() { precmd_functions+=(fun_1) precmd_functions+=(fun_2) - run '__bp_precmd_invoke_cmd' + run set_exit_code_and_run_precmd [ $status -eq 0 ] [ "${#lines[@]}" == '2' ] [ "${lines[0]}" == "one" ] @@ -264,7 +312,7 @@ test_preexec_echo() { IFS=_ name_with_underscores_2() { parts=(2_2); echo $parts; } precmd_functions+=(name_with_underscores_2) - run '__bp_precmd_invoke_cmd' + run set_exit_code_and_run_precmd [ $status -eq 0 ] [ "$output" == "2 2" ] } diff --git a/vendor/github.com/rcaloras/bash-preexec/test/include-test.bats b/vendor/github.com/rcaloras/bash-preexec/test/include-test.bats index bd1e3b5bbe..43383a3b91 100644 --- a/vendor/github.com/rcaloras/bash-preexec/test/include-test.bats +++ b/vendor/github.com/rcaloras/bash-preexec/test/include-test.bats @@ -1,19 +1,31 @@ #!/usr/bin/env bats @test "should not import if it's already defined" { + # shellcheck disable=SC2034,SC2030 + bash_preexec_imported="defined" + # shellcheck source=vendor/github.com/rcaloras/bash-preexec/bash-preexec.sh + source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" + [ -z "$(type -t __bp_install)" ] +} + +@test "should not import if it's already defined (old guard, don't use elsewhere!)" { + # shellcheck disable=SC2030 __bp_imported="defined" + # shellcheck source=vendor/github.com/rcaloras/bash-preexec/bash-preexec.sh source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" - [ -z $(type -t __bp_preexec_and_precmd_install) ] + [ -z "$(type -t __bp_install)" ] } @test "should import if not defined" { - unset __bp_imported + unset bash_preexec_imported + # shellcheck source=vendor/github.com/rcaloras/bash-preexec/bash-preexec.sh source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" - [ -n $(type -t __bp_install) ] + [ -n "$(type -t __bp_install)" ] } @test "bp should stop installation if HISTTIMEFORMAT is readonly" { readonly HISTTIMEFORMAT + # shellcheck source=vendor/github.com/rcaloras/bash-preexec/bash-preexec.sh run source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" [ $status -ne 0 ] [[ "$output" =~ "HISTTIMEFORMAT" ]] || return 1