From 0119368909b51b1c6bd12195f061c18695cd2b38 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 16:33:47 +0100 Subject: [PATCH 01/36] Delete preexec vendor --- .../rcaloras/bash-preexec/.travis.yml | 20 - .../rcaloras/bash-preexec/LICENSE.md | 21 - .../rcaloras/bash-preexec/README.md | 107 ----- .../rcaloras/bash-preexec/bash-preexec.sh | 351 ----------------- .../rcaloras/bash-preexec/test/README.md | 22 -- .../bash-preexec/test/bash-preexec.bats | 364 ------------------ .../bash-preexec/test/include-test.bats | 20 - 7 files changed, 905 deletions(-) delete mode 100644 vendor/github.com/rcaloras/bash-preexec/.travis.yml delete mode 100644 vendor/github.com/rcaloras/bash-preexec/LICENSE.md delete mode 100644 vendor/github.com/rcaloras/bash-preexec/README.md delete mode 100644 vendor/github.com/rcaloras/bash-preexec/bash-preexec.sh delete mode 100644 vendor/github.com/rcaloras/bash-preexec/test/README.md delete mode 100644 vendor/github.com/rcaloras/bash-preexec/test/bash-preexec.bats delete mode 100644 vendor/github.com/rcaloras/bash-preexec/test/include-test.bats 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/LICENSE.md b/vendor/github.com/rcaloras/bash-preexec/LICENSE.md deleted file mode 100644 index b4521b3947..0000000000 --- a/vendor/github.com/rcaloras/bash-preexec/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License - -Copyright (c) 2017 Ryan Caloras and contributors (see https://github.com/rcaloras/bash-preexec) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/vendor/github.com/rcaloras/bash-preexec/README.md b/vendor/github.com/rcaloras/bash-preexec/README.md deleted file mode 100644 index 3e88c844fe..0000000000 --- a/vendor/github.com/rcaloras/bash-preexec/README.md +++ /dev/null @@ -1,107 +0,0 @@ -[![Build Status](https://travis-ci.org/rcaloras/bash-preexec.svg?branch=master)](https://travis-ci.org/rcaloras/bash-preexec) -[![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). - -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! - -## Quick Start -```bash -# Pull down our file from GitHub and write it to our 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 -# Define a couple functions. -preexec() { echo "just typed $1"; } -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. -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 -``` - -## Usage -Two functions **preexec** and **precmd** can now be defined and they'll be automatically invoked by bash-preexec if they exist. - -* `preexec` Executed just after a command has been read and is about to be executed. The string that the user typed is passed as the first argument. -* `precmd` Executed just before each prompt. Equivalent to PROMPT_COMMAND, but more flexible and resilient. -```bash -source ~/.bash-preexec.sh -preexec() { echo "just typed $1"; } -precmd() { echo "printing the prompt"; } -``` -Should output something like: -``` -elementz@Kashmir:~/git/bash-preexec (master)$ ls -just typed ls -bash-preexec.sh README.md test -printing the prompt -``` -#### Function Arrays -You can also define functions to be invoked by appending them to two different arrays. This is great if you want to have many functions invoked for either hook. Both preexec and precmd functions are added to these by default and don't need to be added manually. -* `$preexec_functions` Array of functions invoked by preexec. -* `$precmd_functions` Array of functions invoked by precmd. - -#### preexec -```bash -# Define some function to use preexec -preexec_hello_world() { echo "You just entered $1"; } -# Add it to the array of functions to be invoked each time. -preexec_functions+=(preexec_hello_world) -``` - -#### precmd -```bash -precmd_hello_world() { echo "This is invoked before the prompt is displayed"; } -precmd_functions+=(precmd_hello_world) -``` - -You can also define multiple functions to be invoked like so. - -```bash -precmd_hello_one() { echo "This is invoked on precmd first"; } -precmd_hello_two() { echo "This is invoked on precmd second"; } -precmd_functions+=(precmd_hello_one) -precmd_functions+=(precmd_hello_two) -``` - -You can check the functions set for each by echoing its contents. - -```bash -echo ${preexec_functions[@]} -echo ${precmd_functions[@]} -``` - -## Subshells -bash-preexec does not support invoking preexec() for subshells by default. It must be enabled by setting -`__bp_enable_subshells`. -```bash -# Enable experimental subshell support -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) - -## Tests -You can run tests using [Bats](https://github.com/bats-core/bats-core). -```bash -bats test -``` -Should output something like: -``` -elementz@Kashmir:~/git/bash-preexec(master)$ bats test - ✓ No functions defined for preexec should simply return - ✓ precmd should execute a function once - ✓ preexec should execute a function with the last command in our history - ✓ preexec should execute multiple functions in the order added to their arrays - ✓ preecmd should execute multiple functions in the order added to their arrays -``` diff --git a/vendor/github.com/rcaloras/bash-preexec/bash-preexec.sh b/vendor/github.com/rcaloras/bash-preexec/bash-preexec.sh deleted file mode 100644 index 5f1208c33e..0000000000 --- a/vendor/github.com/rcaloras/bash-preexec/bash-preexec.sh +++ /dev/null @@ -1,351 +0,0 @@ -# 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.4.1 -# - -# 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. - -# Make sure this is bash that's running and return otherwise. -if [[ -z "${BASH_VERSION:-}" ]]; then - return 1; -fi - -# Avoid duplicate inclusion -if [[ -n "${bash_preexec_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. -__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_functions -declare -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_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 -} - -# 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:-} -} - -__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_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 - # 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=$( - 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 - 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 - 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. - __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:-}" - 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}" - __bp_sanitize_string existing_prompt_command "$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\n' - if [[ -n "$existing_prompt_command" ]]; then - PROMPT_COMMAND+=${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) - - # 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 - PROMPT_COMMAND=${sanitized_prompt_command}$'\n' - fi; - 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; diff --git a/vendor/github.com/rcaloras/bash-preexec/test/README.md b/vendor/github.com/rcaloras/bash-preexec/test/README.md deleted file mode 100644 index fd6613c047..0000000000 --- a/vendor/github.com/rcaloras/bash-preexec/test/README.md +++ /dev/null @@ -1,22 +0,0 @@ -Testing `bash-preexec` -====================== - -**Note on test conditions** - -When writing test conditions, use `[ ... ]` instead of `[[ ... ]]` since the -former are supported by Bats on Bash versions before 4.1. In particular, macOS -uses Bash 3.2, and `[[ ... ]]` tests always pass on macOS. - -In some cases, you may want to use a feature unique to `[[ ... ]]` such as -pattern matching (`[[ $name = a* ]]`) or regular expressions (`[[ $(date) =~ -^Fri\ ...\ 13 ]]`). In those cases, use the following pattern to replace “bare” -`[[ ... ]]`. - -``` -[[ ... ]] || return 1 -``` - -References: -* [Differences between `[` and `[[`](http://mywiki.wooledge.org/BashFAQ/031) -* [Problems with `[[` in Bats](https://github.com/sstephenson/bats/issues/49) -* [Using `|| return 1` instead of `|| false`](https://github.com/bats-core/bats-core/commit/e5695a673faad4d4d33446ed5c99d70dbfa6d8be) diff --git a/vendor/github.com/rcaloras/bash-preexec/test/bash-preexec.bats b/vendor/github.com/rcaloras/bash-preexec/test/bash-preexec.bats deleted file mode 100644 index 84a30caea4..0000000000 --- a/vendor/github.com/rcaloras/bash-preexec/test/bash-preexec.bats +++ /dev/null @@ -1,364 +0,0 @@ -#!/usr/bin/env bats - -setup() { - PROMPT_COMMAND='' # in case the invoking shell has set this - history -s fake command # preexec requires there be some history - set -o nounset # in case the user has this set - __bp_delay_install="true" - source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" -} - -bp_install() { - __bp_install_after_session_init - eval "$PROMPT_COMMAND" -} - -test_echo() { - echo "test echo" -} - -test_preexec_echo() { - printf "%s\n" "$1" -} - -@test "__bp_install_after_session_init should exit with 1 if we're not using bash" { - unset BASH_VERSION - run '__bp_install_after_session_init' - [ $status -eq 1 ] - [ -z "$output" ] -} - -@test "__bp_install should exit if it's already installed" { - bp_install - - run '__bp_install' - [ $status -eq 1 ] - [ -z "$output" ] -} - -@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 - - eval "$PROMPT_COMMAND" - - [[ "$PROMPT_COMMAND" != *"trap DEBUG"* ]] || return 1 - [[ "$PROMPT_COMMAND" != *"__bp_install"* ]] || return 1 -} - -@test "__bp_install should preserve an existing DEBUG trap" { - 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 DEBUG - [ "$(trap -p DEBUG | cut -d' ' -f3)" == "'foo'" ] - - 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' - [ "$output" == "true1" ] - - __bp_sanitize_string output " ; true2; " - [ "$output" == "true2" ] - - __bp_sanitize_string output $'\n'" ; true3; " - [ "$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 - - run '__bp_preexec_invoke_exec' 'true' - [ $status -eq 0 ] - [ -z "$output" ] -} - -@test "precmd should execute a function once" { - precmd_functions+=(test_echo) - run '__bp_precmd_invoke_cmd' - [ $status -eq 0 ] - [ "$output" == "test echo" ] -} - -@test "precmd should set \$? to be the previous exit code" { - 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 ] - [ "$output" == "251" ] -} - -@test "precmd should set \$BP_PIPESTATUS to the previous \$PIPESTATUS" { - echo_pipestatus() { - echo "${BP_PIPESTATUS[*]}" - } - # Helper function is necessary because Bats' run doesn't preserve $PIPESTATUS - set_pipestatus_and_run_precmd() { - false | true - __bp_precmd_invoke_cmd - } - - precmd_functions+=(echo_pipestatus) - run 'set_pipestatus_and_run_precmd' - [ $status -eq 0 ] - [ "$output" == "1 0" ] -} - -@test "precmd should set \$_ to be the previous last arg" { - echo_last_arg() { - echo "$_" - } - precmd_functions+=(echo_last_arg) - - bats_trap=$(trap -p DEBUG) - trap DEBUG # remove the Bats stack-trace trap so $_ doesn't get overwritten - : "last-arg" - __bp_preexec_invoke_exec "$_" - eval "$bats_trap" # Restore trap - run '__bp_precmd_invoke_cmd' - [ $status -eq 0 ] - [ "$output" == "last-arg" ] -} - -@test "preexec should execute a function with the last command in our history" { - preexec_functions+=(test_preexec_echo) - __bp_interactive_mode - git_command="git commit -a -m 'committing some stuff'" - history -s $git_command - - run '__bp_preexec_invoke_exec' - [ $status -eq 0 ] - [ "$output" == "$git_command" ] -} - -@test "preexec should execute multiple functions in the order added to their arrays" { - fun_1() { echo "$1 one"; } - fun_2() { echo "$1 two"; } - preexec_functions+=(fun_1) - preexec_functions+=(fun_2) - __bp_interactive_mode - - run '__bp_preexec_invoke_exec' - [ $status -eq 0 ] - [ "${#lines[@]}" == '2' ] - [ "${lines[0]}" == "fake command one" ] - [ "${lines[1]}" == "fake command two" ] -} - -@test "preecmd should execute multiple functions in the order added to their arrays" { - fun_1() { echo "one"; } - fun_2() { echo "two"; } - precmd_functions+=(fun_1) - precmd_functions+=(fun_2) - - run '__bp_precmd_invoke_cmd' - [ $status -eq 0 ] - [ "${#lines[@]}" == '2' ] - [ "${lines[0]}" == "one" ] - [ "${lines[1]}" == "two" ] -} - -@test "preexec should execute a function with IFS defined to local scope" { - IFS=_ - name_with_underscores_1() { parts=(1_2); echo $parts; } - preexec_functions+=(name_with_underscores_1) - - __bp_interactive_mode - run '__bp_preexec_invoke_exec' - [ $status -eq 0 ] - [ "$output" == "1 2" ] -} - -@test "precmd should execute a function with IFS defined to local scope" { - IFS=_ - name_with_underscores_2() { parts=(2_2); echo $parts; } - precmd_functions+=(name_with_underscores_2) - run '__bp_precmd_invoke_cmd' - [ $status -eq 0 ] - [ "$output" == "2 2" ] -} - -@test "preexec should set \$? to be the exit code of preexec_functions" { - return_nonzero() { - return 1 - } - preexec_functions+=(return_nonzero) - - __bp_interactive_mode - - run '__bp_preexec_invoke_exec' - [ $status -eq 1 ] -} - -@test "in_prompt_command should detect if a command is part of PROMPT_COMMAND" { - - PROMPT_COMMAND=$'precmd_invoke_cmd\n something; echo yo\n __bp_interactive_mode' - run '__bp_in_prompt_command' "something" - [ $status -eq 0 ] - - run '__bp_in_prompt_command' "something_else" - [ $status -eq 1 ] - - # Should trim commands and arguments here. - PROMPT_COMMAND=" precmd_invoke_cmd ; something ; some_stuff_here;" - run '__bp_in_prompt_command' " precmd_invoke_cmd " - [ $status -eq 0 ] - - PROMPT_COMMAND=" precmd_invoke_cmd ; something ; some_stuff_here;" - run '__bp_in_prompt_command' " not_found" - [ $status -eq 1 ] - -} - -@test "__bp_adjust_histcontrol should remove ignorespace and ignoreboth" { - - # Should remove ignorespace - HISTCONTROL="ignorespace:ignoredups:*" - __bp_adjust_histcontrol - [ "$HISTCONTROL" == ":ignoredups:*" ] - - # Should remove ignoreboth and replace it with ignoredups - HISTCONTROL="ignoreboth" - __bp_adjust_histcontrol - [ "$HISTCONTROL" == "ignoredups:" ] - - # Handle a few inputs - HISTCONTROL="ignoreboth:ignorespace:some_thing_else" - __bp_adjust_histcontrol - echo "$HISTCONTROL" - [ "$HISTCONTROL" == "ignoredups:::some_thing_else" ] - -} - -@test "preexec should respect HISTTIMEFORMAT" { - preexec_functions+=(test_preexec_echo) - __bp_interactive_mode - git_command="git commit -a -m 'committing some stuff'" - HISTTIMEFORMAT='%F %T ' - history -s $git_command - - run '__bp_preexec_invoke_exec' - [ $status -eq 0 ] - [ "$output" == "$git_command" ] -} - -@test "preexec should not strip whitespace from commands" { - preexec_functions+=(test_preexec_echo) - __bp_interactive_mode - history -s " this command has whitespace " - - run '__bp_preexec_invoke_exec' - [ $status -eq 0 ] - [ "$output" == " this command has whitespace " ] -} - -@test "preexec should preserve multi-line strings in commands" { - preexec_functions+=(test_preexec_echo) - __bp_interactive_mode - history -s "this 'command contains -a multiline string'" - run '__bp_preexec_invoke_exec' - [ $status -eq 0 ] - [ "$output" == "this 'command contains -a multiline string'" ] -} - -@test "preexec should work on options to 'echo' commands" { - preexec_functions+=(test_preexec_echo) - __bp_interactive_mode - history -s -- '-n' - run '__bp_preexec_invoke_exec' - [ $status -eq 0 ] - [ "$output" == '-n' ] -} diff --git a/vendor/github.com/rcaloras/bash-preexec/test/include-test.bats b/vendor/github.com/rcaloras/bash-preexec/test/include-test.bats deleted file mode 100644 index bd1e3b5bbe..0000000000 --- a/vendor/github.com/rcaloras/bash-preexec/test/include-test.bats +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bats - -@test "should not import if it's already defined" { - __bp_imported="defined" - source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" - [ -z $(type -t __bp_preexec_and_precmd_install) ] -} - -@test "should import if not defined" { - unset __bp_imported - source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" - [ -n $(type -t __bp_install) ] -} - -@test "bp should stop installation if HISTTIMEFORMAT is readonly" { - readonly HISTTIMEFORMAT - run source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" - [ $status -ne 0 ] - [[ "$output" =~ "HISTTIMEFORMAT" ]] || return 1 -} From 027b7ae056031f7688241c5582418f527cb01547 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 16:34:04 +0100 Subject: [PATCH 02/36] Squashed 'vendor/github.com/rcaloras/bash-preexec/' content from commit b73ed5f7 git-subtree-dir: vendor/github.com/rcaloras/bash-preexec git-subtree-split: b73ed5f7f953207b958f15b1773721dded697ac3 --- .github/workflows/bats.yaml | 15 ++ LICENSE.md | 21 ++ README.md | 116 ++++++++++ bash-preexec.sh | 376 +++++++++++++++++++++++++++++++++ test/README.md | 22 ++ test/bash-preexec.bats | 411 ++++++++++++++++++++++++++++++++++++ test/include-test.bats | 26 +++ 7 files changed, 987 insertions(+) create mode 100644 .github/workflows/bats.yaml create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 bash-preexec.sh create mode 100644 test/README.md create mode 100644 test/bash-preexec.bats create mode 100644 test/include-test.bats diff --git a/.github/workflows/bats.yaml b/.github/workflows/bats.yaml new file mode 100644 index 0000000000..8507cfef71 --- /dev/null +++ b/.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/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000..b4521b3947 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2017 Ryan Caloras and contributors (see https://github.com/rcaloras/bash-preexec) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..9efdbe40f5 --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +[![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 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), [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 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 +# Define a couple functions. +preexec() { echo "just typed $1"; } +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 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 +``` + +## Usage +Two functions **preexec** and **precmd** can now be defined and they'll be automatically invoked by bash-preexec if they exist. + +* `preexec` Executed just after a command has been read and is about to be executed. The string that the user typed is passed as the first argument. +* `precmd` Executed just before each prompt. Equivalent to PROMPT_COMMAND, but more flexible and resilient. +```bash +source ~/.bash-preexec.sh +preexec() { echo "just typed $1"; } +precmd() { echo "printing the prompt"; } +``` +Should output something like: +``` +elementz@Kashmir:~/git/bash-preexec (master)$ ls +just typed ls +bash-preexec.sh README.md test +printing the prompt +``` +#### Function Arrays +You can also define functions to be invoked by appending them to two different arrays. This is great if you want to have many functions invoked for either hook. Both preexec and precmd functions are added to these by default and don't need to be added manually. +* `$preexec_functions` Array of functions invoked by preexec. +* `$precmd_functions` Array of functions invoked by precmd. + +#### preexec +```bash +# Define some function to use preexec +preexec_hello_world() { echo "You just entered $1"; } +# Add it to the array of functions to be invoked each time. +preexec_functions+=(preexec_hello_world) +``` + +#### precmd +```bash +precmd_hello_world() { echo "This is invoked before the prompt is displayed"; } +precmd_functions+=(precmd_hello_world) +``` + +You can also define multiple functions to be invoked like so. + +```bash +precmd_hello_one() { echo "This is invoked on precmd first"; } +precmd_hello_two() { echo "This is invoked on precmd second"; } +precmd_functions+=(precmd_hello_one) +precmd_functions+=(precmd_hello_two) +``` + +You can check the functions set for each by echoing its contents. + +```bash +echo ${preexec_functions[@]} +echo ${precmd_functions[@]} +``` + +## Subshells +bash-preexec does not support invoking preexec() for subshells by default. It must be enabled by setting +`__bp_enable_subshells`. +```bash +# Enable experimental subshell support +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 +bats test +``` +Should output something like: +``` +elementz@Kashmir:~/git/bash-preexec(master)$ bats test + ✓ No functions defined for preexec should simply return + ✓ precmd should execute a function once + ✓ preexec should execute a function with the last command in our history + ✓ preexec should execute multiple functions in the order added to their arrays + ✓ preecmd should execute multiple functions in the order added to their arrays +``` diff --git a/bash-preexec.sh b/bash-preexec.sh new file mode 100644 index 0000000000..e0d2fa0fc6 --- /dev/null +++ b/bash-preexec.sh @@ -0,0 +1,376 @@ +# 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 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:-}" || -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 +# 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_functions +declare -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. + __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_init +fi diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000000..fd6613c047 --- /dev/null +++ b/test/README.md @@ -0,0 +1,22 @@ +Testing `bash-preexec` +====================== + +**Note on test conditions** + +When writing test conditions, use `[ ... ]` instead of `[[ ... ]]` since the +former are supported by Bats on Bash versions before 4.1. In particular, macOS +uses Bash 3.2, and `[[ ... ]]` tests always pass on macOS. + +In some cases, you may want to use a feature unique to `[[ ... ]]` such as +pattern matching (`[[ $name = a* ]]`) or regular expressions (`[[ $(date) =~ +^Fri\ ...\ 13 ]]`). In those cases, use the following pattern to replace “bare” +`[[ ... ]]`. + +``` +[[ ... ]] || return 1 +``` + +References: +* [Differences between `[` and `[[`](http://mywiki.wooledge.org/BashFAQ/031) +* [Problems with `[[` in Bats](https://github.com/sstephenson/bats/issues/49) +* [Using `|| return 1` instead of `|| false`](https://github.com/bats-core/bats-core/commit/e5695a673faad4d4d33446ed5c99d70dbfa6d8be) diff --git a/test/bash-preexec.bats b/test/bash-preexec.bats new file mode 100644 index 0000000000..7f2ed8b077 --- /dev/null +++ b/test/bash-preexec.bats @@ -0,0 +1,411 @@ +#!/usr/bin/env bats + +setup() { + PROMPT_COMMAND='' # in case the invoking shell has set this + history -s fake command # preexec requires there be some history + set -o nounset # in case the user has this set + __bp_delay_install="true" + 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 +} + +test_echo() { + echo "test echo" +} + +test_preexec_echo() { + printf "%s\n" "$1" +} + +# 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 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 + + run '__bp_install' + [ $status -eq 1 ] + [ -z "$output" ] +} + +@test "__bp_install should remove trap logic and itself from PROMPT_COMMAND" { + __bp_install_after_session_init + + # Assert that before running, the command contains the install string, and + # afterwards it does not + [[ "$PROMPT_COMMAND" == *"$__bp_install_string"* ]] || return 1 + + eval_PROMPT_COMMAND + + [[ "$PROMPT_COMMAND" != *"$__bp_install_string"* ]] || return 1 +} + +@test "__bp_install should preserve an existing DEBUG trap" { + 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 DEBUG + [ "$(trap -p DEBUG | cut -d' ' -f3)" == "'foo'" ] + + 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_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' + [ "$output" == "true1" ] + + __bp_sanitize_string output " ; true2; " + [ "$output" == "true2" ] + + __bp_sanitize_string output $'\n'" ; true3; " + [ "$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' + [ "$(join_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' + [ "$(join_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 + + run '__bp_preexec_invoke_exec' 'true' + [ $status -eq 0 ] + [ -z "$output" ] +} + +@test "precmd should execute a function once" { + precmd_functions+=(test_echo) + run set_exit_code_and_run_precmd + [ $status -eq 0 ] + [ "$output" == "test echo" ] +} + +@test "precmd should set \$? to be the previous exit code" { + echo_exit_code() { + echo "$?" + } + + precmd_functions+=(echo_exit_code) + run set_exit_code_and_run_precmd 251 + [ $status -eq 251 ] + [ "$output" == "251" ] +} + +@test "precmd should set \$BP_PIPESTATUS to the previous \$PIPESTATUS" { + echo_pipestatus() { + echo "${BP_PIPESTATUS[*]}" + } + # Helper function is necessary because Bats' run doesn't preserve $PIPESTATUS + set_pipestatus_and_run_precmd() { + false | true + __bp_precmd_invoke_cmd + } + + precmd_functions+=(echo_pipestatus) + run 'set_pipestatus_and_run_precmd' + [ $status -eq 0 ] + [ "$output" == "1 0" ] +} + +@test "precmd should set \$_ to be the previous last arg" { + echo_last_arg() { + echo "$_" + } + precmd_functions+=(echo_last_arg) + + bats_trap=$(trap -p DEBUG) + trap DEBUG # remove the Bats stack-trace trap so $_ doesn't get overwritten + : "last-arg" + __bp_preexec_invoke_exec "$_" + eval "$bats_trap" # Restore trap + run set_exit_code_and_run_precmd + [ $status -eq 0 ] + [ "$output" == "last-arg" ] +} + +@test "preexec should execute a function with the last command in our history" { + preexec_functions+=(test_preexec_echo) + __bp_interactive_mode + git_command="git commit -a -m 'committing some stuff'" + history -s $git_command + + run '__bp_preexec_invoke_exec' + [ $status -eq 0 ] + [ "$output" == "$git_command" ] +} + +@test "preexec should execute multiple functions in the order added to their arrays" { + fun_1() { echo "$1 one"; } + fun_2() { echo "$1 two"; } + preexec_functions+=(fun_1) + preexec_functions+=(fun_2) + __bp_interactive_mode + + run '__bp_preexec_invoke_exec' + [ $status -eq 0 ] + [ "${#lines[@]}" == '2' ] + [ "${lines[0]}" == "fake command one" ] + [ "${lines[1]}" == "fake command two" ] +} + +@test "preecmd should execute multiple functions in the order added to their arrays" { + fun_1() { echo "one"; } + fun_2() { echo "two"; } + precmd_functions+=(fun_1) + precmd_functions+=(fun_2) + + run set_exit_code_and_run_precmd + [ $status -eq 0 ] + [ "${#lines[@]}" == '2' ] + [ "${lines[0]}" == "one" ] + [ "${lines[1]}" == "two" ] +} + +@test "preexec should execute a function with IFS defined to local scope" { + IFS=_ + name_with_underscores_1() { parts=(1_2); echo $parts; } + preexec_functions+=(name_with_underscores_1) + + __bp_interactive_mode + run '__bp_preexec_invoke_exec' + [ $status -eq 0 ] + [ "$output" == "1 2" ] +} + +@test "precmd should execute a function with IFS defined to local scope" { + IFS=_ + name_with_underscores_2() { parts=(2_2); echo $parts; } + precmd_functions+=(name_with_underscores_2) + run set_exit_code_and_run_precmd + [ $status -eq 0 ] + [ "$output" == "2 2" ] +} + +@test "preexec should set \$? to be the exit code of preexec_functions" { + return_nonzero() { + return 1 + } + preexec_functions+=(return_nonzero) + + __bp_interactive_mode + + run '__bp_preexec_invoke_exec' + [ $status -eq 1 ] +} + +@test "in_prompt_command should detect if a command is part of PROMPT_COMMAND" { + + PROMPT_COMMAND=$'precmd_invoke_cmd\n something; echo yo\n __bp_interactive_mode' + run '__bp_in_prompt_command' "something" + [ $status -eq 0 ] + + run '__bp_in_prompt_command' "something_else" + [ $status -eq 1 ] + + # Should trim commands and arguments here. + PROMPT_COMMAND=" precmd_invoke_cmd ; something ; some_stuff_here;" + run '__bp_in_prompt_command' " precmd_invoke_cmd " + [ $status -eq 0 ] + + PROMPT_COMMAND=" precmd_invoke_cmd ; something ; some_stuff_here;" + run '__bp_in_prompt_command' " not_found" + [ $status -eq 1 ] + +} + +@test "__bp_adjust_histcontrol should remove ignorespace and ignoreboth" { + + # Should remove ignorespace + HISTCONTROL="ignorespace:ignoredups:*" + __bp_adjust_histcontrol + [ "$HISTCONTROL" == ":ignoredups:*" ] + + # Should remove ignoreboth and replace it with ignoredups + HISTCONTROL="ignoreboth" + __bp_adjust_histcontrol + [ "$HISTCONTROL" == "ignoredups:" ] + + # Handle a few inputs + HISTCONTROL="ignoreboth:ignorespace:some_thing_else" + __bp_adjust_histcontrol + echo "$HISTCONTROL" + [ "$HISTCONTROL" == "ignoredups:::some_thing_else" ] + +} + +@test "preexec should respect HISTTIMEFORMAT" { + preexec_functions+=(test_preexec_echo) + __bp_interactive_mode + git_command="git commit -a -m 'committing some stuff'" + HISTTIMEFORMAT='%F %T ' + history -s $git_command + + run '__bp_preexec_invoke_exec' + [ $status -eq 0 ] + [ "$output" == "$git_command" ] +} + +@test "preexec should not strip whitespace from commands" { + preexec_functions+=(test_preexec_echo) + __bp_interactive_mode + history -s " this command has whitespace " + + run '__bp_preexec_invoke_exec' + [ $status -eq 0 ] + [ "$output" == " this command has whitespace " ] +} + +@test "preexec should preserve multi-line strings in commands" { + preexec_functions+=(test_preexec_echo) + __bp_interactive_mode + history -s "this 'command contains +a multiline string'" + run '__bp_preexec_invoke_exec' + [ $status -eq 0 ] + [ "$output" == "this 'command contains +a multiline string'" ] +} + +@test "preexec should work on options to 'echo' commands" { + preexec_functions+=(test_preexec_echo) + __bp_interactive_mode + history -s -- '-n' + run '__bp_preexec_invoke_exec' + [ $status -eq 0 ] + [ "$output" == '-n' ] +} diff --git a/test/include-test.bats b/test/include-test.bats new file mode 100644 index 0000000000..36a5f2569e --- /dev/null +++ b/test/include-test.bats @@ -0,0 +1,26 @@ +#!/usr/bin/env bats + +@test "should not import if it's already defined" { + bash_preexec_imported="defined" + 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!)" { + __bp_imported="defined" + source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" + [ -z $(type -t __bp_install) ] +} + +@test "should import if not defined" { + unset bash_preexec_imported + source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" + [ -n $(type -t __bp_install) ] +} + +@test "bp should stop installation if HISTTIMEFORMAT is readonly" { + readonly HISTTIMEFORMAT + run source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" + [ $status -ne 0 ] + [[ "$output" =~ "HISTTIMEFORMAT" ]] || return 1 +} From 7ab0074e08e45a1bf04118af4609d1aafa09294b Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 16:34:57 +0100 Subject: [PATCH 03/36] Update precommit --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b8615c4d14..777a1ec49f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ minimum_pre_commit_version: 1.18.1 exclude: "docs/_build/|vendor/" repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 + rev: v6.0.0 hooks: - id: trailing-whitespace exclude: ".(md|rst)$" @@ -15,14 +15,14 @@ repos: - id: mixed-line-ending - id: check-added-large-files - repo: https://github.com/jumanjihouse/pre-commit-hooks - rev: 2.1.5 + rev: 3.0.0 hooks: - id: git-check # Configure in .gitattributes - id: shellcheck files: "\\.(bash)$" - id: shfmt - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.1.7 + rev: v1.5.6 hooks: # - id: forbid-crlf - id: remove-crlf From ee821b3fec82fbb5fc4a79cf40ac40892b7ede03 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 16:38:25 +0100 Subject: [PATCH 04/36] Rework command duration plugin to handle locale with . or , and remove subshell --- lib/command_duration.bash | 68 ++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index 850c67649e..5e73169d7f 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -3,25 +3,17 @@ # Functions for measuring and reporting how long a command takes to run. # 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)}" -: "${COMMAND_DURATION_ICON:=🕘}" -: "${COMMAND_DURATION_MIN_SECONDS:=1}" - function _command_duration_pre_exec() { - COMMAND_DURATION_START_SECONDS="$(_shell_duration_en)" + if [[ -n "${EPOCHREALTIME:-}" ]]; then + COMMAND_DURATION_START_SECONDS="${EPOCHREALTIME//,/.}" + else + COMMAND_DURATION_START_SECONDS="$SECONDS" + fi } +: "${COMMAND_DURATION_ICON:=🕘}" +: "${COMMAND_DURATION_MIN_SECONDS:=1}" + function _command_duration_pre_cmd() { COMMAND_DURATION_START_SECONDS="" } @@ -38,31 +30,41 @@ 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}" + if [[ -n "${EPOCHREALTIME:-}" ]]; then + current_time="${EPOCHREALTIME//,/.}" + else + current_time="$SECONDS" + fi + + local -i command_duration=0 + local -i minutes=0 seconds=0 deciseconds=0 + + local -i start_s=${COMMAND_DURATION_START_SECONDS%.*} + local -i curr_s=${current_time%.*} - if [[ "${command_start_seconds:-0}" -gt 0 ]]; then - # seconds - command_duration="$((current_time_seconds - command_start_seconds))" + # Calculate seconds difference + command_duration=$((curr_s - start_s)) - if ((current_time_deciseconds >= command_start_deciseconds)); then - deciseconds="$((current_time_deciseconds - command_start_deciseconds))" + # Calculate deciseconds if both timestamps have fractional parts + if [[ "$COMMAND_DURATION_START_SECONDS" == *.* ]] && [[ "$current_time" == *.* ]]; then + local start_fs="${COMMAND_DURATION_START_SECONDS#*.}" + local curr_fs="${current_time#*.}" + + # Take first digit for deciseconds + local -i start_ds="${start_fs:0:1}" + local -i curr_ds="${curr_fs:0:1}" + + if ((curr_ds >= start_ds)); then + deciseconds=$((curr_ds - start_ds)) else ((command_duration -= 1)) - deciseconds="$((10 - (command_start_deciseconds - current_time_deciseconds)))" + deciseconds=$((10 + curr_ds - start_ds)) fi - else - command_duration=0 fi + if ((command_duration < 0)); then command_duration=0; deciseconds=0; fi + if ((command_duration >= COMMAND_DURATION_MIN_SECONDS)); then minutes=$((command_duration / 60)) seconds=$((command_duration % 60)) From f630441b9f46ce5fdb930a4f1fb1cdca3aad55cd Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 16:38:43 +0100 Subject: [PATCH 05/36] Sh format --- lib/command_duration.bash | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index 5e73169d7f..b37636500e 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -63,7 +63,10 @@ function _command_duration() { fi fi - if ((command_duration < 0)); then command_duration=0; deciseconds=0; fi + if ((command_duration < 0)); then + command_duration=0 + deciseconds=0 + fi if ((command_duration >= COMMAND_DURATION_MIN_SECONDS)); then minutes=$((command_duration / 60)) From b42c3acbe77e87aba1805e2e228fa627be767256 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 17:55:53 +0100 Subject: [PATCH 06/36] Revert "Update precommit" This reverts commit 7ab0074e08e45a1bf04118af4609d1aafa09294b. --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 777a1ec49f..b8615c4d14 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ minimum_pre_commit_version: 1.18.1 exclude: "docs/_build/|vendor/" repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 + rev: v2.3.0 hooks: - id: trailing-whitespace exclude: ".(md|rst)$" @@ -15,14 +15,14 @@ repos: - id: mixed-line-ending - id: check-added-large-files - repo: https://github.com/jumanjihouse/pre-commit-hooks - rev: 3.0.0 + rev: 2.1.5 hooks: - id: git-check # Configure in .gitattributes - id: shellcheck files: "\\.(bash)$" - id: shfmt - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.6 + rev: v1.1.7 hooks: # - id: forbid-crlf - id: remove-crlf From 185741277359227a3ceb8ba683a65cb9cd5d8d14 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 17:58:47 +0100 Subject: [PATCH 07/36] Fix cmd-returned-notify.plugin.bash --- plugins/available/cmd-returned-notify.plugin.bash | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/available/cmd-returned-notify.plugin.bash b/plugins/available/cmd-returned-notify.plugin.bash index 28c5abc10f..321e93bcd9 100644 --- a/plugins/available/cmd-returned-notify.plugin.bash +++ b/plugins/available/cmd-returned-notify.plugin.bash @@ -6,7 +6,11 @@ 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)" + if [[ -n "${EPOCHREALTIME:-}" ]]; then + current_time="${EPOCHREALTIME//,/.}" + else + current_time="$SECONDS" + fi local -i command_duration="$((${current_time%.*} - ${command_start%.*}))" if [[ "${command_duration}" -gt "${NOTIFY_IF_COMMAND_RETURNS_AFTER:-5}" ]]; then printf '\a' From 83f11a83463d7c45fabacd163f351b7c770f5f45 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 18:34:57 +0100 Subject: [PATCH 08/36] Fix _shell_duration_en function call, restore original variable names --- lib/command_duration.bash | 49 ++++++++++++-------- test/plugins/cmd-returned-notify.plugin.bats | 6 +-- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index b37636500e..fef5e1c007 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -3,12 +3,19 @@ # Functions for measuring and reporting how long a command takes to run. # Get shell duration in decimal format regardless of runtime locale. -function _command_duration_pre_exec() { +function _command_duration_current_time() { + local current_time if [[ -n "${EPOCHREALTIME:-}" ]]; then - COMMAND_DURATION_START_SECONDS="${EPOCHREALTIME//,/.}" + current_time="${EPOCHREALTIME//,/.}" else - COMMAND_DURATION_START_SECONDS="$SECONDS" + current_time="$SECONDS" fi + + echo "$current_time" +} + +function _command_duration_pre_exec() { + COMMAND_DURATION_START_SECONDS="$(_command_duration_current_time)" } : "${COMMAND_DURATION_ICON:=🕘}" @@ -19,10 +26,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" } @@ -31,35 +44,31 @@ function _command_duration() { [[ -n "${COMMAND_DURATION_START_SECONDS:-}" ]] || return local current_time - if [[ -n "${EPOCHREALTIME:-}" ]]; then - current_time="${EPOCHREALTIME//,/.}" - else - current_time="$SECONDS" - fi + current_time="$(_command_duration_current_time)" local -i command_duration=0 local -i minutes=0 seconds=0 deciseconds=0 - local -i start_s=${COMMAND_DURATION_START_SECONDS%.*} - local -i curr_s=${current_time%.*} + local -i command_start_seconds=${COMMAND_DURATION_START_SECONDS%.*} + local -i current_time_seconds=${current_time%.*} # Calculate seconds difference - command_duration=$((curr_s - start_s)) + command_duration=$((current_time_seconds - command_start_seconds)) # Calculate deciseconds if both timestamps have fractional parts if [[ "$COMMAND_DURATION_START_SECONDS" == *.* ]] && [[ "$current_time" == *.* ]]; then - local start_fs="${COMMAND_DURATION_START_SECONDS#*.}" - local curr_fs="${current_time#*.}" + local -i command_start_deciseconds=$((10#${COMMAND_DURATION_START_SECONDS##*.})) + local -i current_time_deciseconds="$((10#${current_time##*.}))" # Take first digit for deciseconds - local -i start_ds="${start_fs:0:1}" - local -i curr_ds="${curr_fs:0:1}" + command_start_deciseconds="${command_start_deciseconds:0:1}" + current_time_deciseconds="${current_time_deciseconds:0:1}" - if ((curr_ds >= start_ds)); then - deciseconds=$((curr_ds - start_ds)) + if ((current_time_deciseconds >= command_start_deciseconds)); then + deciseconds=$((current_time_deciseconds - command_start_deciseconds)) else ((command_duration -= 1)) - deciseconds=$((10 + curr_ds - start_ds)) + deciseconds=$((10 + current_time_deciseconds - command_start_deciseconds)) fi fi 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%.*}" From 64ec7e7391b28b5fafcb433bce37c6b9533ca43a Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 20:17:52 +0100 Subject: [PATCH 09/36] With recent version of bash preexec set PROMPT_COMMAND as an array --- test/lib/preexec.bats | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/lib/preexec.bats b/test/lib/preexec.bats index 3c5ed4b041..29543591cc 100644 --- a/test/lib/preexec.bats +++ b/test/lib/preexec.bats @@ -60,7 +60,7 @@ function local_setup { assert_success __bp_install - assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n__bp_interactive_mode' + assert_equal "${PROMPT_COMMAND[*]}" $'__bp_precmd_invoke_cmd __bp_interactive_mode' } @test "vendor preexec: __bp_install() with existing" { @@ -75,7 +75,7 @@ function local_setup { assert_success __bp_install - assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n'"$test_prompt_string"$'\n__bp_interactive_mode' + assert_equal "${PROMPT_COMMAND[*]}" $'__bp_precmd_invoke_cmd\n'"$test_prompt_string"$'\n: __bp_interactive_mode' } @test "lib preexec: __bp_require_not_readonly()" { From 4c60358b15fcbee8db0317e5e4280554d9d16e06 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 20:23:02 +0100 Subject: [PATCH 10/36] But leave test for old macos version of bash --- test/lib/preexec.bats | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/lib/preexec.bats b/test/lib/preexec.bats index 29543591cc..4fc5530913 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 __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__bp_interactive_mode' + fi } @test "lib preexec: __bp_require_not_readonly()" { From 5ae5522145fa35012f028ec26440e78fa4a6eafb Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 20:25:32 +0100 Subject: [PATCH 11/36] New version of preexec add a : --- test/lib/preexec.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/preexec.bats b/test/lib/preexec.bats index 4fc5530913..5132991fb1 100644 --- a/test/lib/preexec.bats +++ b/test/lib/preexec.bats @@ -82,7 +82,7 @@ function local_setup { 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__bp_interactive_mode' + assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n'"$test_prompt_string"$'\n:\n__bp_interactive_mode' fi } From f6890013d5620384a1ed165273146d27591b7eb0 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 20:32:50 +0100 Subject: [PATCH 12/36] Fix preexec bats tests shellcheck errors --- .../github.com/rcaloras/bash-preexec/test/bash-preexec.bats | 2 +- .../github.com/rcaloras/bash-preexec/test/include-test.bats | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 7f2ed8b077..8879e8a938 100644 --- a/vendor/github.com/rcaloras/bash-preexec/test/bash-preexec.bats +++ b/vendor/github.com/rcaloras/bash-preexec/test/bash-preexec.bats @@ -41,7 +41,7 @@ return_exit_code() { } set_exit_code_and_run_precmd() { - return_exit_code ${1:-0} + return_exit_code "${1:-0}" __bp_precmd_invoke_cmd } 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 36a5f2569e..94551c1c87 100644 --- a/vendor/github.com/rcaloras/bash-preexec/test/include-test.bats +++ b/vendor/github.com/rcaloras/bash-preexec/test/include-test.bats @@ -3,19 +3,19 @@ @test "should not import if it's already defined" { bash_preexec_imported="defined" source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" - [ -z $(type -t __bp_install) ] + [ -z "$(type -t __bp_install)" ] } @test "should not import if it's already defined (old guard, don't use elsewhere!)" { __bp_imported="defined" source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" - [ -z $(type -t __bp_install) ] + [ -z "$(type -t __bp_install)" ] } @test "should import if not defined" { unset bash_preexec_imported 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" { From d65288f4d194ae67682e02d641a7c32536c9ccce Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 20:42:34 +0100 Subject: [PATCH 13/36] , and . are not the only possibilities of decimal_point Technically, one can define a locale with decimal_point being an arbitrary string. For example, ps_AF seems to use U+066B as the decimal point Thanks @akinomyoga for the feedback --- lib/command_duration.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index fef5e1c007..22714ed75c 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -6,7 +6,7 @@ function _command_duration_current_time() { local current_time if [[ -n "${EPOCHREALTIME:-}" ]]; then - current_time="${EPOCHREALTIME//,/.}" + current_time="${EPOCHREALTIME//[!0-9]/.}" else current_time="$SECONDS" fi From 2339683f15a2d5cabb2a3bde706c4ab78e1e4eaf Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 20:44:42 +0100 Subject: [PATCH 14/36] Ignore warning for vendors bats test files --- vendor/github.com/rcaloras/bash-preexec/test/bash-preexec.bats | 1 + vendor/github.com/rcaloras/bash-preexec/test/include-test.bats | 1 + 2 files changed, 2 insertions(+) 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 8879e8a938..08e112d761 100644 --- a/vendor/github.com/rcaloras/bash-preexec/test/bash-preexec.bats +++ b/vendor/github.com/rcaloras/bash-preexec/test/bash-preexec.bats @@ -76,6 +76,7 @@ set_exit_code_and_run_precmd() { # 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 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 94551c1c87..db9ec561af 100644 --- a/vendor/github.com/rcaloras/bash-preexec/test/include-test.bats +++ b/vendor/github.com/rcaloras/bash-preexec/test/include-test.bats @@ -1,6 +1,7 @@ #!/usr/bin/env bats @test "should not import if it's already defined" { + # shellcheck disable=SC2034 bash_preexec_imported="defined" source "${BATS_TEST_DIRNAME}/../bash-preexec.sh" [ -z "$(type -t __bp_install)" ] From 3e3ddf39f244b69796a8ee4e65cc22feb4f3c275 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 21:10:22 +0100 Subject: [PATCH 15/36] Add default for COMMAND_DURATION_START_SECONDS --- lib/command_duration.bash | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index 22714ed75c..7f5e6ecad0 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -14,13 +14,14 @@ function _command_duration_current_time() { echo "$current_time" } +: "${COMMAND_DURATION_START_SECONDS:=$(_command_duration_current_time)}" +: "${COMMAND_DURATION_ICON:=🕘}" +: "${COMMAND_DURATION_MIN_SECONDS:=1}" + function _command_duration_pre_exec() { COMMAND_DURATION_START_SECONDS="$(_command_duration_current_time)" } -: "${COMMAND_DURATION_ICON:=🕘}" -: "${COMMAND_DURATION_MIN_SECONDS:=1}" - function _command_duration_pre_cmd() { COMMAND_DURATION_START_SECONDS="" } From 73737086524ace4a67aa68f23ab47f82d9b70950 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 21:10:47 +0100 Subject: [PATCH 16/36] Fix shellcheck SC1091 for vendor bats tests --- vendor/github.com/rcaloras/bash-preexec/test/include-test.bats | 3 +++ 1 file changed, 3 insertions(+) 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 db9ec561af..04746c5b80 100644 --- a/vendor/github.com/rcaloras/bash-preexec/test/include-test.bats +++ b/vendor/github.com/rcaloras/bash-preexec/test/include-test.bats @@ -9,18 +9,21 @@ @test "should not import if it's already defined (old guard, don't use elsewhere!)" { __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_install)" ] } @test "should import if not defined" { 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)" ] } @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 From 9e88c2a345d84713e1f314dfd333674c299344d6 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 3 Feb 2026 21:15:40 +0100 Subject: [PATCH 17/36] Ignore last shellcheck error in vendor bats tests --- .../github.com/rcaloras/bash-preexec/test/include-test.bats | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 04746c5b80..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,13 +1,15 @@ #!/usr/bin/env bats @test "should not import if it's already defined" { - # shellcheck disable=SC2034 + # 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" From 1fb6e652fbf83340625a0bc602137064e3c9fa88 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 10 Feb 2026 15:38:00 +0100 Subject: [PATCH 18/36] Add more information about the reason of this change --- lib/command_duration.bash | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index 7f5e6ecad0..53b329ad32 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -2,6 +2,21 @@ # # 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 +# +# This was done to: +# - enforce the decimal point to be a period +# - use the local 'C' which is ensured by the C and POSIX standards +# - not overide the user defined locale +# +# We now use EPOCHREALTIME, while replacing any char different that a digit by a period +# +# Technically, one can define a locale with decimal_point being an arbitrary string. +# For example, ps_AF seems to use 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. function _command_duration_current_time() { local current_time From f775bbdcc555a16d07ee0f6b60caf5a4da045f52 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 10 Feb 2026 16:23:49 +0100 Subject: [PATCH 19/36] Get the current time from command_duration lib function --- plugins/available/cmd-returned-notify.plugin.bash | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/plugins/available/cmd-returned-notify.plugin.bash b/plugins/available/cmd-returned-notify.plugin.bash index 321e93bcd9..b98bac57ac 100644 --- a/plugins/available/cmd-returned-notify.plugin.bash +++ b/plugins/available/cmd-returned-notify.plugin.bash @@ -4,13 +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 - if [[ -n "${EPOCHREALTIME:-}" ]]; then - current_time="${EPOCHREALTIME//,/.}" - else - current_time="$SECONDS" - fi + 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' From 53de05d2a80ee9e70fe4ac4233af9a18bbc92f79 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 10 Feb 2026 16:26:59 +0100 Subject: [PATCH 20/36] Work with microseconds, truncate the output to display --- lib/command_duration.bash | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index 53b329ad32..a2bb9b38b1 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -32,6 +32,7 @@ function _command_duration_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="$(_command_duration_current_time)" @@ -63,7 +64,7 @@ function _command_duration() { current_time="$(_command_duration_current_time)" local -i command_duration=0 - local -i minutes=0 seconds=0 deciseconds=0 + local -i minutes=0 seconds=0 microseconds=0 local -i command_start_seconds=${COMMAND_DURATION_START_SECONDS%.*} local -i current_time_seconds=${current_time%.*} @@ -71,26 +72,20 @@ function _command_duration() { # Calculate seconds difference command_duration=$((current_time_seconds - command_start_seconds)) - # Calculate deciseconds if both timestamps have fractional parts + # Calculate microseconds if both timestamps have fractional parts if [[ "$COMMAND_DURATION_START_SECONDS" == *.* ]] && [[ "$current_time" == *.* ]]; then - local -i command_start_deciseconds=$((10#${COMMAND_DURATION_START_SECONDS##*.})) - local -i current_time_deciseconds="$((10#${current_time##*.}))" + local -i command_start_microseconds=$((10#${COMMAND_DURATION_START_SECONDS##*.})) + local -i current_time_microseconds="$((10#${current_time##*.}))" - # Take first digit for deciseconds - command_start_deciseconds="${command_start_deciseconds:0:1}" - current_time_deciseconds="${current_time_deciseconds:0:1}" - - if ((current_time_deciseconds >= command_start_deciseconds)); then - deciseconds=$((current_time_deciseconds - command_start_deciseconds)) + if ((current_time_microseconds >= command_start_microseconds)); then + microseconds=$((current_time_microseconds - command_start_microseconds)) else ((command_duration -= 1)) - deciseconds=$((10 + current_time_deciseconds - command_start_deciseconds)) + microseconds=$((1000000 + current_time_microseconds - command_start_microseconds)) fi - fi - if ((command_duration < 0)); then - command_duration=0 - deciseconds=0 + # Take first N digits + microseconds="${microseconds:0:$COMMAND_DURATION_PRECISION}" fi if ((command_duration >= COMMAND_DURATION_MIN_SECONDS)); then @@ -101,7 +96,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%d.%01ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds" "$microseconds" fi fi } From 4d8666177c46459a58851114ddd0343a91807841 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 10 Feb 2026 16:36:32 +0100 Subject: [PATCH 21/36] Only calculate and display microseconds if precision > 0 --- lib/command_duration.bash | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index a2bb9b38b1..099fa9b9cf 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -73,7 +73,7 @@ function _command_duration() { command_duration=$((current_time_seconds - command_start_seconds)) # Calculate microseconds if both timestamps have fractional parts - if [[ "$COMMAND_DURATION_START_SECONDS" == *.* ]] && [[ "$current_time" == *.* ]]; then + 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##*.}))" @@ -95,8 +95,10 @@ function _command_duration() { _dynamic_clock_icon "${command_duration}" if ((minutes > 0)); then printf "%s %s%dm %ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$minutes" "$seconds" - else + elif ((COMMAND_DURATION_PRECISION > 0)); then printf "%s %s%d.%01ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds" "$microseconds" + else + printf "%s %s%ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds" fi fi } From ee1c7830514716a4392be0a004de3ec5cde7fcbb Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 10 Feb 2026 16:36:58 +0100 Subject: [PATCH 22/36] Add documentation about COMMAND_DURATION_PRECISION --- docs/themes.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 ============= From 42b80862040883cfef9904ed270a53c9e7f9cfe9 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 10 Feb 2026 19:36:36 +0100 Subject: [PATCH 23/36] Handle case with leading 0 in microseconds --- lib/command_duration.bash | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index 099fa9b9cf..8f5a800b1f 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -84,7 +84,8 @@ function _command_duration() { microseconds=$((1000000 + current_time_microseconds - command_start_microseconds)) fi - # Take first N digits + # Pad with leading zeros to 6 digits, then take first N digits + printf -v microseconds '%06d' "$microseconds" microseconds="${microseconds:0:$COMMAND_DURATION_PRECISION}" fi From 5ef3b70c7cec0ee72571662a5bd9d52cc14b58e0 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 10 Feb 2026 20:46:23 +0100 Subject: [PATCH 24/36] Handle case with leading 0 in microseconds --- lib/command_duration.bash | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index 8f5a800b1f..6d1b901ceb 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -64,7 +64,8 @@ function _command_duration() { current_time="$(_command_duration_current_time)" local -i command_duration=0 - local -i minutes=0 seconds=0 microseconds=0 + local -i minutes=0 seconds=0 + local microseconds=0 local -i command_start_seconds=${COMMAND_DURATION_START_SECONDS%.*} local -i current_time_seconds=${current_time%.*} @@ -86,7 +87,7 @@ function _command_duration() { # Pad with leading zeros to 6 digits, then take first N digits printf -v microseconds '%06d' "$microseconds" - microseconds="${microseconds:0:$COMMAND_DURATION_PRECISION}" + microseconds="$((10#${microseconds:0:$COMMAND_DURATION_PRECISION}))" fi if ((command_duration >= COMMAND_DURATION_MIN_SECONDS)); then @@ -97,7 +98,7 @@ function _command_duration() { if ((minutes > 0)); then printf "%s %s%dm %ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$minutes" "$seconds" elif ((COMMAND_DURATION_PRECISION > 0)); then - printf "%s %s%d.%01ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds" "$microseconds" + printf "%s %s%d.%0${COMMAND_DURATION_PRECISION}ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds" "$microseconds" else printf "%s %s%ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds" fi From 9e5f7ce62e25f1dad9ef3206202a7e9c0b4ceeb7 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 10 Feb 2026 20:59:26 +0100 Subject: [PATCH 25/36] No quote for integer local var --- lib/command_duration.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index 6d1b901ceb..a083d3a101 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -76,7 +76,7 @@ function _command_duration() { # 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##*.}))" + local -i current_time_microseconds=$((10#${current_time##*.})) if ((current_time_microseconds >= command_start_microseconds)); then microseconds=$((current_time_microseconds - command_start_microseconds)) From c0f2a5d0d1d342b1b3b3c819d4d48051a42cb541 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 10 Feb 2026 21:01:35 +0100 Subject: [PATCH 26/36] Add tests --- test/lib/command_duration.bats | 107 +++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 test/lib/command_duration.bats diff --git a/test/lib/command_duration.bats b/test/lib/command_duration.bats new file mode 100644 index 0000000000..fdbd755d25 --- /dev/null +++ b/test/lib/command_duration.bats @@ -0,0 +1,107 @@ +# shellcheck shell=bats + +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_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 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$" +} From 964e9bc6a248511dbe698a1d4c5b36ab8268559c Mon Sep 17 00:00:00 2001 From: BarbUk Date: Tue, 10 Feb 2026 21:09:31 +0100 Subject: [PATCH 27/36] Disable shellcheck false positive for bats tests --- test/lib/command_duration.bats | 1 + 1 file changed, 1 insertion(+) diff --git a/test/lib/command_duration.bats b/test/lib/command_duration.bats index fdbd755d25..38f2b6e840 100644 --- a/test/lib/command_duration.bats +++ b/test/lib/command_duration.bats @@ -1,4 +1,5 @@ # shellcheck shell=bats +# shellcheck disable=2034,2329 load "${MAIN_BASH_IT_DIR?}/test/test_helper.bash" From 67cb1b23ae83803763290bf11306083474cad779 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Wed, 11 Feb 2026 06:55:18 +0100 Subject: [PATCH 28/36] local -> locale --- lib/command_duration.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index a083d3a101..0b29f1bd3b 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -7,7 +7,7 @@ # # This was done to: # - enforce the decimal point to be a period -# - use the local 'C' which is ensured by the C and POSIX standards +# - use the locale 'C' which is ensured by the C and POSIX standards # - not overide the user defined locale # # We now use EPOCHREALTIME, while replacing any char different that a digit by a period From 3c39331b9efa5d9c13cb399fabf13bee646dbc3f Mon Sep 17 00:00:00 2001 From: BarbUk Date: Wed, 11 Feb 2026 06:56:03 +0100 Subject: [PATCH 29/36] Rephrase --- lib/command_duration.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index 0b29f1bd3b..4d597009d3 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -10,7 +10,7 @@ # - use the locale 'C' which is ensured by the C and POSIX standards # - not overide the user defined locale # -# We now use EPOCHREALTIME, while replacing any char different that a digit by a period +# 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 seems to use U+066B as the decimal point. From 275c8d3779e4de44893a30aada83b26bfe4f5d63 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Wed, 11 Feb 2026 06:56:40 +0100 Subject: [PATCH 30/36] overide -> override --- lib/command_duration.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index 4d597009d3..8465593583 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -8,7 +8,7 @@ # This was done to: # - enforce the decimal point to be a period # - use the locale 'C' which is ensured by the C and POSIX standards -# - not overide the user defined locale +# - not override the user-defined locale # # We now use EPOCHREALTIME, while replacing any non-digit character by a period. # From 910dbed1f6a6b8dacf057418b9fb9db348642ad6 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Wed, 11 Feb 2026 19:31:08 +0100 Subject: [PATCH 31/36] Fix phrasing --- lib/command_duration.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index 8465593583..8c75c45e81 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -13,7 +13,7 @@ # 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 seems to use U+066B as the decimal point. +# For example, ps_AF uses U+066B as the decimal point. # # cf: https://github.com/Bash-it/bash-it/pull/2366#discussion_r2760681820 # From 43f1bd4fad1bda6a8b8648b73a7728c51850cd83 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Wed, 11 Feb 2026 20:03:13 +0100 Subject: [PATCH 32/36] Add tests when EPOCHREALTIME is not available --- test/lib/command_duration.bats | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/lib/command_duration.bats b/test/lib/command_duration.bats index 38f2b6e840..21c9fdd61e 100644 --- a/test/lib/command_duration.bats +++ b/test/lib/command_duration.bats @@ -13,6 +13,18 @@ function local_setup_file() { 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" ] @@ -106,3 +118,14 @@ function local_setup_file() { 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 ".* 5.0s$" +} From b848e5b4f6500fb2509454da07b3bab0907a3759 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Wed, 11 Feb 2026 20:08:15 +0100 Subject: [PATCH 33/36] Uses string instead of int --- lib/command_duration.bash | 6 +++--- test/lib/command_duration.bats | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index 8c75c45e81..fd6e4c0bc1 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -65,7 +65,7 @@ function _command_duration() { local -i command_duration=0 local -i minutes=0 seconds=0 - local microseconds=0 + local microseconds="" local -i command_start_seconds=${COMMAND_DURATION_START_SECONDS%.*} local -i current_time_seconds=${current_time%.*} @@ -87,7 +87,7 @@ function _command_duration() { # Pad with leading zeros to 6 digits, then take first N digits printf -v microseconds '%06d' "$microseconds" - microseconds="$((10#${microseconds:0:$COMMAND_DURATION_PRECISION}))" + microseconds="${microseconds:0:$COMMAND_DURATION_PRECISION}" fi if ((command_duration >= COMMAND_DURATION_MIN_SECONDS)); then @@ -98,7 +98,7 @@ function _command_duration() { if ((minutes > 0)); then printf "%s %s%dm %ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$minutes" "$seconds" elif ((COMMAND_DURATION_PRECISION > 0)); then - printf "%s %s%d.%0${COMMAND_DURATION_PRECISION}ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds" "$microseconds" + printf "%s %s%ss" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds${microseconds:+.$microseconds}" else printf "%s %s%ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds" fi diff --git a/test/lib/command_duration.bats b/test/lib/command_duration.bats index 21c9fdd61e..66490bb885 100644 --- a/test/lib/command_duration.bats +++ b/test/lib/command_duration.bats @@ -127,5 +127,5 @@ function local_setup_file() { _command_duration_current_time() { echo 105; } COMMAND_DURATION_START_SECONDS=100 run _command_duration - assert_output --regexp ".* 5.0s$" + assert_output --regexp ".* 5s$" } From 71a8a71a2618b2aa3b64cac7e82e9f6ee9d87e62 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Wed, 11 Feb 2026 20:12:12 +0100 Subject: [PATCH 34/36] Reintroduce paste documentation to keep context about this change --- lib/command_duration.bash | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index fd6e4c0bc1..deed96e89b 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -5,10 +5,11 @@ # Notice: This function used to run as a sub-shell while defining: # local LC_ALL=C # -# This was done to: -# - enforce the decimal point to be a period -# - use the locale 'C' which is ensured by the C and POSIX standards -# - not override the user-defined locale +# 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. # From dd800546eea9972bde8d3d504f67f60aa83f4ba5 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Thu, 12 Feb 2026 22:58:04 +0400 Subject: [PATCH 35/36] Remove unused code branch --- lib/command_duration.bash | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/command_duration.bash b/lib/command_duration.bash index deed96e89b..23b4cc232d 100644 --- a/lib/command_duration.bash +++ b/lib/command_duration.bash @@ -98,10 +98,8 @@ function _command_duration() { _dynamic_clock_icon "${command_duration}" if ((minutes > 0)); then printf "%s %s%dm %ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$minutes" "$seconds" - elif ((COMMAND_DURATION_PRECISION > 0)); then - printf "%s %s%ss" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds${microseconds:+.$microseconds}" else - printf "%s %s%ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds" + printf "%s %s%ss" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds${microseconds:+.$microseconds}" fi fi } From ed314d5ed9066022347ef8c8092940e6e80a6d35 Mon Sep 17 00:00:00 2001 From: BarbUk Date: Thu, 12 Feb 2026 22:59:33 +0400 Subject: [PATCH 36/36] Add test with precision 0 and microseconds time --- test/lib/command_duration.bats | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/lib/command_duration.bats b/test/lib/command_duration.bats index 66490bb885..691ab984d0 100644 --- a/test/lib/command_duration.bats +++ b/test/lib/command_duration.bats @@ -76,6 +76,17 @@ function local_setup_file() { 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