Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0119368
Delete preexec vendor
BarbUk Feb 3, 2026
027b7ae
Squashed 'vendor/github.com/rcaloras/bash-preexec/' content from comm…
BarbUk Feb 3, 2026
b509d72
Add "preexec" from "https://github.com/rcaloras/bash-preexec@0.6.0"
BarbUk Feb 3, 2026
7ab0074
Update precommit
BarbUk Feb 3, 2026
ee821b3
Rework command duration plugin to handle locale with . or , and remov…
BarbUk Feb 3, 2026
f630441
Sh format
BarbUk Feb 3, 2026
b42c3ac
Revert "Update precommit"
BarbUk Feb 3, 2026
1857412
Fix cmd-returned-notify.plugin.bash
BarbUk Feb 3, 2026
83f11a8
Fix _shell_duration_en function call, restore original variable names
BarbUk Feb 3, 2026
64ec7e7
With recent version of bash preexec set PROMPT_COMMAND as an array
BarbUk Feb 3, 2026
4c60358
But leave test for old macos version of bash
BarbUk Feb 3, 2026
5ae5522
New version of preexec add a :
BarbUk Feb 3, 2026
f689001
Fix preexec bats tests shellcheck errors
BarbUk Feb 3, 2026
d65288f
, and . are not the only possibilities of decimal_point
BarbUk Feb 3, 2026
2339683
Ignore warning for vendors bats test files
BarbUk Feb 3, 2026
3e3ddf3
Add default for COMMAND_DURATION_START_SECONDS
BarbUk Feb 3, 2026
7373708
Fix shellcheck SC1091 for vendor bats tests
BarbUk Feb 3, 2026
9e88c2a
Ignore last shellcheck error in vendor bats tests
BarbUk Feb 3, 2026
1fb6e65
Add more information about the reason of this change
BarbUk Feb 10, 2026
f775bbd
Get the current time from command_duration lib function
BarbUk Feb 10, 2026
53de05d
Work with microseconds, truncate the output to display
BarbUk Feb 10, 2026
4d86661
Only calculate and display microseconds if precision > 0
BarbUk Feb 10, 2026
ee1c783
Add documentation about COMMAND_DURATION_PRECISION
BarbUk Feb 10, 2026
42b8086
Handle case with leading 0 in microseconds
BarbUk Feb 10, 2026
5ef3b70
Handle case with leading 0 in microseconds
BarbUk Feb 10, 2026
9e5f7ce
No quote for integer local var
BarbUk Feb 10, 2026
c0f2a5d
Add tests
BarbUk Feb 10, 2026
964e9bc
Disable shellcheck false positive for bats tests
BarbUk Feb 10, 2026
67cb1b2
local -> locale
BarbUk Feb 11, 2026
3c39331
Rephrase
BarbUk Feb 11, 2026
275c8d3
overide -> override
BarbUk Feb 11, 2026
910dbed
Fix phrasing
BarbUk Feb 11, 2026
43f1bd4
Add tests when EPOCHREALTIME is not available
BarbUk Feb 11, 2026
b848e5b
Uses string instead of int
BarbUk Feb 11, 2026
71a8a71
Reintroduce paste documentation to keep context about this change
BarbUk Feb 11, 2026
dd80054
Remove unused code branch
BarbUk Feb 12, 2026
ed314d5
Add test with precision 0 and microseconds time
BarbUk Feb 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/themes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=============
Expand Down
94 changes: 61 additions & 33 deletions lib/command_duration.bash
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,93 @@
#
# Functions for measuring and reporting how long a command takes to run.

# Notice: This function used to run as a sub-shell while defining:
# local LC_ALL=C
#
# DFARREL You would think LC_NUMERIC would do it, but not working in my local.
# Note: LC_ALL='en_US.UTF-8' has been used to enforce the decimal point to be
# a period, but the specific locale 'en_US.UTF-8' is not ensured to exist in
# the system. One should instead use the locale 'C', which is ensured by the
# C and POSIX standards.
#
# We now use EPOCHREALTIME, while replacing any non-digit character by a period.
#
# Technically, one can define a locale with decimal_point being an arbitrary string.
# For example, ps_AF uses U+066B as the decimal point.
#
# cf: https://github.com/Bash-it/bash-it/pull/2366#discussion_r2760681820
#
# Get shell duration in decimal format regardless of runtime locale.
# Notice: This function runs as a sub-shell - notice '(' vs '{'.
function _shell_duration_en() (
# DFARREL You would think LC_NUMERIC would do it, but not working in my local.
# Note: LC_ALL='en_US.UTF-8' has been used to enforce the decimal point to be
# a period, but the specific locale 'en_US.UTF-8' is not ensured to exist in
# the system. One should instead use the locale 'C', which is ensured by the
# C and POSIX standards.
local LC_ALL=C
printf "%s" "${EPOCHREALTIME:-$SECONDS}"
)

: "${COMMAND_DURATION_START_SECONDS:=$(_shell_duration_en)}"
function _command_duration_current_time() {
local current_time
if [[ -n "${EPOCHREALTIME:-}" ]]; then
current_time="${EPOCHREALTIME//[!0-9]/.}"
else
current_time="$SECONDS"
fi

echo "$current_time"
}

: "${COMMAND_DURATION_START_SECONDS:=$(_command_duration_current_time)}"
: "${COMMAND_DURATION_ICON:=🕘}"
: "${COMMAND_DURATION_MIN_SECONDS:=1}"
: "${COMMAND_DURATION_PRECISION:=1}"

function _command_duration_pre_exec() {
COMMAND_DURATION_START_SECONDS="$(_shell_duration_en)"
COMMAND_DURATION_START_SECONDS="$(_command_duration_current_time)"
}

function _command_duration_pre_cmd() {
COMMAND_DURATION_START_SECONDS=""
}

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"
}

function _command_duration() {
[[ -n "${BASH_IT_COMMAND_DURATION:-}" ]] || return
[[ -n "${COMMAND_DURATION_START_SECONDS:-}" ]] || return

local command_duration=0 command_start="${COMMAND_DURATION_START_SECONDS:-0}"
local -i minutes=0 seconds=0 deciseconds=0
local -i command_start_seconds="${command_start%.*}"
local -i command_start_deciseconds=$((10#${command_start##*.}))
command_start_deciseconds="${command_start_deciseconds:0:1}"
local current_time
current_time="$(_shell_duration_en)"
local -i current_time_seconds="${current_time%.*}"
local -i current_time_deciseconds="$((10#${current_time##*.}))"
current_time_deciseconds="${current_time_deciseconds:0:1}"
current_time="$(_command_duration_current_time)"

local -i command_duration=0
local -i minutes=0 seconds=0
local microseconds=""

if [[ "${command_start_seconds:-0}" -gt 0 ]]; then
# seconds
command_duration="$((current_time_seconds - command_start_seconds))"
local -i command_start_seconds=${COMMAND_DURATION_START_SECONDS%.*}
local -i current_time_seconds=${current_time%.*}

if ((current_time_deciseconds >= command_start_deciseconds)); then
deciseconds="$((current_time_deciseconds - command_start_deciseconds))"
# Calculate seconds difference
command_duration=$((current_time_seconds - command_start_seconds))

# Calculate microseconds if both timestamps have fractional parts
if [[ "$COMMAND_DURATION_START_SECONDS" == *.* ]] && [[ "$current_time" == *.* ]] && ((COMMAND_DURATION_PRECISION > 0)); then
local -i command_start_microseconds=$((10#${COMMAND_DURATION_START_SECONDS##*.}))
local -i current_time_microseconds=$((10#${current_time##*.}))

if ((current_time_microseconds >= command_start_microseconds)); then
microseconds=$((current_time_microseconds - command_start_microseconds))
else
((command_duration -= 1))
deciseconds="$((10 - (command_start_deciseconds - current_time_deciseconds)))"
microseconds=$((1000000 + current_time_microseconds - command_start_microseconds))
fi
else
command_duration=0

# Pad with leading zeros to 6 digits, then take first N digits
printf -v microseconds '%06d' "$microseconds"
microseconds="${microseconds:0:$COMMAND_DURATION_PRECISION}"
fi

if ((command_duration >= COMMAND_DURATION_MIN_SECONDS)); then
Expand All @@ -71,7 +99,7 @@ function _command_duration() {
if ((minutes > 0)); then
printf "%s %s%dm %ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$minutes" "$seconds"
else
printf "%s %s%d.%01ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds" "$deciseconds"
printf "%s %s%ss" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds${microseconds:+.$microseconds}"
fi
fi
}
Expand Down
6 changes: 3 additions & 3 deletions plugins/available/cmd-returned-notify.plugin.bash
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ about-plugin 'Alert (BEL) when process ends after a threshold of seconds'
url "https://github.com/Bash-it/bash-it"

function precmd_return_notification() {
local command_start="${COMMAND_DURATION_START_SECONDS:=0}"
local current_time
current_time="$(_shell_duration_en)"
local command_start="${COMMAND_DURATION_START_SECONDS:=0}" current_time
current_time="$(_command_duration_current_time)"

local -i command_duration="$((${current_time%.*} - ${command_start%.*}))"
if [[ "${command_duration}" -gt "${NOTIFY_IF_COMMAND_RETURNS_AFTER:-5}" ]]; then
printf '\a'
Expand Down
142 changes: 142 additions & 0 deletions test/lib/command_duration.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# shellcheck shell=bats
# shellcheck disable=2034,2329

load "${MAIN_BASH_IT_DIR?}/test/test_helper.bash"

function local_setup_file() {
setup_libs "command_duration"
}

@test "command_duration: _command_duration_current_time" {
run _command_duration_current_time
assert_success
assert_output --regexp '^[0-9]+(\.[0-9]+)?$'
}

@test "command_duration: _command_duration_current_time without EPOCHREALTIME" {
_command_duration_current_time_no_epoch() {
local EPOCHREALTIME
unset EPOCHREALTIME
local SECONDS=123
_command_duration_current_time
}
run _command_duration_current_time_no_epoch
assert_success
assert_output "123"
}

@test "command_duration: _command_duration_pre_exec" {
_command_duration_pre_exec
assert [ -n "$COMMAND_DURATION_START_SECONDS" ]
}

@test "command_duration: _command_duration_pre_cmd" {
COMMAND_DURATION_START_SECONDS="1234.567"
_command_duration_pre_cmd
assert [ -z "$COMMAND_DURATION_START_SECONDS" ]
}

@test "command_duration: _dynamic_clock_icon" {
_dynamic_clock_icon 1
assert [ -n "$COMMAND_DURATION_ICON" ]
}

@test "command_duration: _command_duration disabled" {
unset BASH_IT_COMMAND_DURATION
COMMAND_DURATION_START_SECONDS="100"
run _command_duration
assert_output ""
}

@test "command_duration: _command_duration no start time" {
BASH_IT_COMMAND_DURATION=true
unset COMMAND_DURATION_START_SECONDS
run _command_duration
assert_output ""
}

@test "command_duration: _command_duration below threshold" {
BASH_IT_COMMAND_DURATION=true
COMMAND_DURATION_MIN_SECONDS=2
# Mock _command_duration_current_time
_command_duration_current_time() { echo 101; }
COMMAND_DURATION_START_SECONDS=100
run _command_duration
assert_output ""
}

@test "command_duration: _command_duration above threshold (seconds)" {
BASH_IT_COMMAND_DURATION=true
COMMAND_DURATION_MIN_SECONDS=1
COMMAND_DURATION_PRECISION=0
# Mock _command_duration_current_time
_command_duration_current_time() { echo 105; }
COMMAND_DURATION_START_SECONDS=100
run _command_duration
assert_output --regexp ".* 5s$"
}

@test "command_duration: _command_duration precision 0 with microseconds time" {
BASH_IT_COMMAND_DURATION=true
COMMAND_DURATION_MIN_SECONDS=1
COMMAND_DURATION_PRECISION=0
# Mock _command_duration_current_time
_command_duration_current_time() { echo 105.600005; }
COMMAND_DURATION_START_SECONDS=100.200007
run _command_duration
assert_output --regexp ".* 5s$"
}

@test "command_duration: _command_duration with precision" {
BASH_IT_COMMAND_DURATION=true
COMMAND_DURATION_MIN_SECONDS=1
COMMAND_DURATION_PRECISION=1
# Mock _command_duration_current_time
_command_duration_current_time() { echo 105.600000; }
COMMAND_DURATION_START_SECONDS=100.200000
run _command_duration
assert_output --regexp ".* 5.4s$"
}

@test "command_duration: _command_duration with minutes" {
BASH_IT_COMMAND_DURATION=true
COMMAND_DURATION_MIN_SECONDS=1
# Mock _command_duration_current_time
_command_duration_current_time() { echo 200; }
COMMAND_DURATION_START_SECONDS=70
run _command_duration
assert_output --regexp ".* 2m 10s$"
}

@test "command_duration: _command_duration with microsecond rollover" {
BASH_IT_COMMAND_DURATION=true
COMMAND_DURATION_MIN_SECONDS=0
COMMAND_DURATION_PRECISION=1
# Mock _command_duration_current_time
# 105.1 - 100.2 = 4.9
_command_duration_current_time() { echo 105.100000; }
COMMAND_DURATION_START_SECONDS=100.200000
run _command_duration
assert_output --regexp ".* 4.9s$"
}

@test "command_duration: _command_duration with precision and leading zeros" {
BASH_IT_COMMAND_DURATION=true
COMMAND_DURATION_MIN_SECONDS=0
COMMAND_DURATION_PRECISION=3
COMMAND_DURATION_START_SECONDS=100.001000
_command_duration_current_time() { echo 105.002000; }
run _command_duration
assert_output --regexp ".* 5.001s$"
}

@test "command_duration: _command_duration without EPOCHREALTIME (SECONDS only)" {
BASH_IT_COMMAND_DURATION=true
COMMAND_DURATION_MIN_SECONDS=1
COMMAND_DURATION_PRECISION=1
# Mock _command_duration_current_time to return integer (like SECONDS would)
_command_duration_current_time() { echo 105; }
COMMAND_DURATION_START_SECONDS=100
run _command_duration
assert_output --regexp ".* 5s$"
}
12 changes: 10 additions & 2 deletions test/lib/preexec.bats
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ function local_setup {
assert_success

__bp_install
assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n__bp_interactive_mode'
if ((BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1))); then
assert_equal "${PROMPT_COMMAND[*]}" $'__bp_precmd_invoke_cmd __bp_interactive_mode'
else
assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n__bp_interactive_mode'
fi
}

@test "vendor preexec: __bp_install() with existing" {
Expand All @@ -75,7 +79,11 @@ function local_setup {
assert_success

__bp_install
assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n'"$test_prompt_string"$'\n__bp_interactive_mode'
if ((BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1))); then
assert_equal "${PROMPT_COMMAND[*]}" $'__bp_precmd_invoke_cmd\n'"$test_prompt_string"$'\n: __bp_interactive_mode'
else
assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n'"$test_prompt_string"$'\n:\n__bp_interactive_mode'
fi
}

@test "lib preexec: __bp_require_not_readonly()" {
Expand Down
6 changes: 3 additions & 3 deletions test/plugins/cmd-returned-notify.plugin.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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%.*}"
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 0 additions & 20 deletions vendor/github.com/rcaloras/bash-preexec/.travis.yml

This file was deleted.

Loading
Loading