Skip to content

Commit bc7e726

Browse files
authored
command duration fix (#2366)
1 parent f4443b1 commit bc7e726

File tree

12 files changed

+410
-140
lines changed

12 files changed

+410
-140
lines changed

docs/themes.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,15 @@ Command duration can be enabled by exporting ``BASH_IT_COMMAND_DURATION``:
5353
5454
export BASH_IT_COMMAND_DURATION=true
5555
56-
The default configuration display last command duration for command lasting one second or more.
57-
You can customize the minimum time in seconds before command duration is displayed in your ``.bashrc``:
56+
The default configuration display last command duration for command lasting one second or more,
57+
with deciseconds precision.
58+
59+
You can customize the minimum time in seconds before command duration is displayed or the precison in your ``.bashrc``:
5860

5961
.. code-block:: bash
6062
6163
export COMMAND_DURATION_MIN_SECONDS=5
64+
export COMMAND_DURATION_PRECISION=2
6265
6366
Clock Related
6467
=============

lib/command_duration.bash

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,65 +2,93 @@
22
#
33
# Functions for measuring and reporting how long a command takes to run.
44

5+
# Notice: This function used to run as a sub-shell while defining:
6+
# local LC_ALL=C
7+
#
8+
# DFARREL You would think LC_NUMERIC would do it, but not working in my local.
9+
# Note: LC_ALL='en_US.UTF-8' has been used to enforce the decimal point to be
10+
# a period, but the specific locale 'en_US.UTF-8' is not ensured to exist in
11+
# the system. One should instead use the locale 'C', which is ensured by the
12+
# C and POSIX standards.
13+
#
14+
# We now use EPOCHREALTIME, while replacing any non-digit character by a period.
15+
#
16+
# Technically, one can define a locale with decimal_point being an arbitrary string.
17+
# For example, ps_AF uses U+066B as the decimal point.
18+
#
19+
# cf: https://github.com/Bash-it/bash-it/pull/2366#discussion_r2760681820
20+
#
521
# Get shell duration in decimal format regardless of runtime locale.
6-
# Notice: This function runs as a sub-shell - notice '(' vs '{'.
7-
function _shell_duration_en() (
8-
# DFARREL You would think LC_NUMERIC would do it, but not working in my local.
9-
# Note: LC_ALL='en_US.UTF-8' has been used to enforce the decimal point to be
10-
# a period, but the specific locale 'en_US.UTF-8' is not ensured to exist in
11-
# the system. One should instead use the locale 'C', which is ensured by the
12-
# C and POSIX standards.
13-
local LC_ALL=C
14-
printf "%s" "${EPOCHREALTIME:-$SECONDS}"
15-
)
16-
17-
: "${COMMAND_DURATION_START_SECONDS:=$(_shell_duration_en)}"
22+
function _command_duration_current_time() {
23+
local current_time
24+
if [[ -n "${EPOCHREALTIME:-}" ]]; then
25+
current_time="${EPOCHREALTIME//[!0-9]/.}"
26+
else
27+
current_time="$SECONDS"
28+
fi
29+
30+
echo "$current_time"
31+
}
32+
33+
: "${COMMAND_DURATION_START_SECONDS:=$(_command_duration_current_time)}"
1834
: "${COMMAND_DURATION_ICON:=🕘}"
1935
: "${COMMAND_DURATION_MIN_SECONDS:=1}"
36+
: "${COMMAND_DURATION_PRECISION:=1}"
2037

2138
function _command_duration_pre_exec() {
22-
COMMAND_DURATION_START_SECONDS="$(_shell_duration_en)"
39+
COMMAND_DURATION_START_SECONDS="$(_command_duration_current_time)"
2340
}
2441

2542
function _command_duration_pre_cmd() {
2643
COMMAND_DURATION_START_SECONDS=""
2744
}
2845

2946
function _dynamic_clock_icon {
30-
local clock_hand
47+
local clock_hand duration="$1"
48+
49+
# Clock only work for time >= 1s
50+
if ((duration < 1)); then
51+
duration=1
52+
fi
53+
3154
# clock hand value is between 90 and 9b in hexadecimal.
3255
# so between 144 and 155 in base 10.
33-
printf -v clock_hand '%x' $((((${1:-${SECONDS}} - 1) % 12) + 144))
56+
printf -v clock_hand '%x' $((((${duration:-${SECONDS}} - 1) % 12) + 144))
3457
printf -v 'COMMAND_DURATION_ICON' '%b' "\xf0\x9f\x95\x$clock_hand"
3558
}
3659

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

41-
local command_duration=0 command_start="${COMMAND_DURATION_START_SECONDS:-0}"
42-
local -i minutes=0 seconds=0 deciseconds=0
43-
local -i command_start_seconds="${command_start%.*}"
44-
local -i command_start_deciseconds=$((10#${command_start##*.}))
45-
command_start_deciseconds="${command_start_deciseconds:0:1}"
4664
local current_time
47-
current_time="$(_shell_duration_en)"
48-
local -i current_time_seconds="${current_time%.*}"
49-
local -i current_time_deciseconds="$((10#${current_time##*.}))"
50-
current_time_deciseconds="${current_time_deciseconds:0:1}"
65+
current_time="$(_command_duration_current_time)"
66+
67+
local -i command_duration=0
68+
local -i minutes=0 seconds=0
69+
local microseconds=""
5170

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

56-
if ((current_time_deciseconds >= command_start_deciseconds)); then
57-
deciseconds="$((current_time_deciseconds - command_start_deciseconds))"
74+
# Calculate seconds difference
75+
command_duration=$((current_time_seconds - command_start_seconds))
76+
77+
# Calculate microseconds if both timestamps have fractional parts
78+
if [[ "$COMMAND_DURATION_START_SECONDS" == *.* ]] && [[ "$current_time" == *.* ]] && ((COMMAND_DURATION_PRECISION > 0)); then
79+
local -i command_start_microseconds=$((10#${COMMAND_DURATION_START_SECONDS##*.}))
80+
local -i current_time_microseconds=$((10#${current_time##*.}))
81+
82+
if ((current_time_microseconds >= command_start_microseconds)); then
83+
microseconds=$((current_time_microseconds - command_start_microseconds))
5884
else
5985
((command_duration -= 1))
60-
deciseconds="$((10 - (command_start_deciseconds - current_time_deciseconds)))"
86+
microseconds=$((1000000 + current_time_microseconds - command_start_microseconds))
6187
fi
62-
else
63-
command_duration=0
88+
89+
# Pad with leading zeros to 6 digits, then take first N digits
90+
printf -v microseconds '%06d' "$microseconds"
91+
microseconds="${microseconds:0:$COMMAND_DURATION_PRECISION}"
6492
fi
6593

6694
if ((command_duration >= COMMAND_DURATION_MIN_SECONDS)); then
@@ -71,7 +99,7 @@ function _command_duration() {
7199
if ((minutes > 0)); then
72100
printf "%s %s%dm %ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$minutes" "$seconds"
73101
else
74-
printf "%s %s%d.%01ds" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds" "$deciseconds"
102+
printf "%s %s%ss" "${COMMAND_DURATION_ICON:-}" "${COMMAND_DURATION_COLOR:-}" "$seconds${microseconds:+.$microseconds}"
75103
fi
76104
fi
77105
}

plugins/available/cmd-returned-notify.plugin.bash

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ about-plugin 'Alert (BEL) when process ends after a threshold of seconds'
44
url "https://github.com/Bash-it/bash-it"
55

66
function precmd_return_notification() {
7-
local command_start="${COMMAND_DURATION_START_SECONDS:=0}"
8-
local current_time
9-
current_time="$(_shell_duration_en)"
7+
local command_start="${COMMAND_DURATION_START_SECONDS:=0}" current_time
8+
current_time="$(_command_duration_current_time)"
9+
1010
local -i command_duration="$((${current_time%.*} - ${command_start%.*}))"
1111
if [[ "${command_duration}" -gt "${NOTIFY_IF_COMMAND_RETURNS_AFTER:-5}" ]]; then
1212
printf '\a'

test/lib/command_duration.bats

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# shellcheck shell=bats
2+
# shellcheck disable=2034,2329
3+
4+
load "${MAIN_BASH_IT_DIR?}/test/test_helper.bash"
5+
6+
function local_setup_file() {
7+
setup_libs "command_duration"
8+
}
9+
10+
@test "command_duration: _command_duration_current_time" {
11+
run _command_duration_current_time
12+
assert_success
13+
assert_output --regexp '^[0-9]+(\.[0-9]+)?$'
14+
}
15+
16+
@test "command_duration: _command_duration_current_time without EPOCHREALTIME" {
17+
_command_duration_current_time_no_epoch() {
18+
local EPOCHREALTIME
19+
unset EPOCHREALTIME
20+
local SECONDS=123
21+
_command_duration_current_time
22+
}
23+
run _command_duration_current_time_no_epoch
24+
assert_success
25+
assert_output "123"
26+
}
27+
28+
@test "command_duration: _command_duration_pre_exec" {
29+
_command_duration_pre_exec
30+
assert [ -n "$COMMAND_DURATION_START_SECONDS" ]
31+
}
32+
33+
@test "command_duration: _command_duration_pre_cmd" {
34+
COMMAND_DURATION_START_SECONDS="1234.567"
35+
_command_duration_pre_cmd
36+
assert [ -z "$COMMAND_DURATION_START_SECONDS" ]
37+
}
38+
39+
@test "command_duration: _dynamic_clock_icon" {
40+
_dynamic_clock_icon 1
41+
assert [ -n "$COMMAND_DURATION_ICON" ]
42+
}
43+
44+
@test "command_duration: _command_duration disabled" {
45+
unset BASH_IT_COMMAND_DURATION
46+
COMMAND_DURATION_START_SECONDS="100"
47+
run _command_duration
48+
assert_output ""
49+
}
50+
51+
@test "command_duration: _command_duration no start time" {
52+
BASH_IT_COMMAND_DURATION=true
53+
unset COMMAND_DURATION_START_SECONDS
54+
run _command_duration
55+
assert_output ""
56+
}
57+
58+
@test "command_duration: _command_duration below threshold" {
59+
BASH_IT_COMMAND_DURATION=true
60+
COMMAND_DURATION_MIN_SECONDS=2
61+
# Mock _command_duration_current_time
62+
_command_duration_current_time() { echo 101; }
63+
COMMAND_DURATION_START_SECONDS=100
64+
run _command_duration
65+
assert_output ""
66+
}
67+
68+
@test "command_duration: _command_duration above threshold (seconds)" {
69+
BASH_IT_COMMAND_DURATION=true
70+
COMMAND_DURATION_MIN_SECONDS=1
71+
COMMAND_DURATION_PRECISION=0
72+
# Mock _command_duration_current_time
73+
_command_duration_current_time() { echo 105; }
74+
COMMAND_DURATION_START_SECONDS=100
75+
run _command_duration
76+
assert_output --regexp ".* 5s$"
77+
}
78+
79+
@test "command_duration: _command_duration precision 0 with microseconds time" {
80+
BASH_IT_COMMAND_DURATION=true
81+
COMMAND_DURATION_MIN_SECONDS=1
82+
COMMAND_DURATION_PRECISION=0
83+
# Mock _command_duration_current_time
84+
_command_duration_current_time() { echo 105.600005; }
85+
COMMAND_DURATION_START_SECONDS=100.200007
86+
run _command_duration
87+
assert_output --regexp ".* 5s$"
88+
}
89+
90+
@test "command_duration: _command_duration with precision" {
91+
BASH_IT_COMMAND_DURATION=true
92+
COMMAND_DURATION_MIN_SECONDS=1
93+
COMMAND_DURATION_PRECISION=1
94+
# Mock _command_duration_current_time
95+
_command_duration_current_time() { echo 105.600000; }
96+
COMMAND_DURATION_START_SECONDS=100.200000
97+
run _command_duration
98+
assert_output --regexp ".* 5.4s$"
99+
}
100+
101+
@test "command_duration: _command_duration with minutes" {
102+
BASH_IT_COMMAND_DURATION=true
103+
COMMAND_DURATION_MIN_SECONDS=1
104+
# Mock _command_duration_current_time
105+
_command_duration_current_time() { echo 200; }
106+
COMMAND_DURATION_START_SECONDS=70
107+
run _command_duration
108+
assert_output --regexp ".* 2m 10s$"
109+
}
110+
111+
@test "command_duration: _command_duration with microsecond rollover" {
112+
BASH_IT_COMMAND_DURATION=true
113+
COMMAND_DURATION_MIN_SECONDS=0
114+
COMMAND_DURATION_PRECISION=1
115+
# Mock _command_duration_current_time
116+
# 105.1 - 100.2 = 4.9
117+
_command_duration_current_time() { echo 105.100000; }
118+
COMMAND_DURATION_START_SECONDS=100.200000
119+
run _command_duration
120+
assert_output --regexp ".* 4.9s$"
121+
}
122+
123+
@test "command_duration: _command_duration with precision and leading zeros" {
124+
BASH_IT_COMMAND_DURATION=true
125+
COMMAND_DURATION_MIN_SECONDS=0
126+
COMMAND_DURATION_PRECISION=3
127+
COMMAND_DURATION_START_SECONDS=100.001000
128+
_command_duration_current_time() { echo 105.002000; }
129+
run _command_duration
130+
assert_output --regexp ".* 5.001s$"
131+
}
132+
133+
@test "command_duration: _command_duration without EPOCHREALTIME (SECONDS only)" {
134+
BASH_IT_COMMAND_DURATION=true
135+
COMMAND_DURATION_MIN_SECONDS=1
136+
COMMAND_DURATION_PRECISION=1
137+
# Mock _command_duration_current_time to return integer (like SECONDS would)
138+
_command_duration_current_time() { echo 105; }
139+
COMMAND_DURATION_START_SECONDS=100
140+
run _command_duration
141+
assert_output --regexp ".* 5s$"
142+
}

test/lib/preexec.bats

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ function local_setup {
6060
assert_success
6161

6262
__bp_install
63-
assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n__bp_interactive_mode'
63+
if ((BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1))); then
64+
assert_equal "${PROMPT_COMMAND[*]}" $'__bp_precmd_invoke_cmd __bp_interactive_mode'
65+
else
66+
assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n__bp_interactive_mode'
67+
fi
6468
}
6569

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

7781
__bp_install
78-
assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n'"$test_prompt_string"$'\n__bp_interactive_mode'
82+
if ((BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1))); then
83+
assert_equal "${PROMPT_COMMAND[*]}" $'__bp_precmd_invoke_cmd\n'"$test_prompt_string"$'\n: __bp_interactive_mode'
84+
else
85+
assert_equal "${PROMPT_COMMAND}" $'__bp_precmd_invoke_cmd\n'"$test_prompt_string"$'\n:\n__bp_interactive_mode'
86+
fi
7987
}
8088

8189
@test "lib preexec: __bp_require_not_readonly()" {

test/plugins/cmd-returned-notify.plugin.bats

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ function local_setup_file() {
1010

1111
@test "plugins cmd-returned-notify: notify after elapsed time" {
1212
NOTIFY_IF_COMMAND_RETURNS_AFTER=0
13-
COMMAND_DURATION_START_SECONDS="$(_shell_duration_en)"
13+
COMMAND_DURATION_START_SECONDS="$(_command_duration_current_time)"
1414
export COMMAND_DURATION_START_SECONDS NOTIFY_IF_COMMAND_RETURNS_AFTER
1515
sleep 1
1616
run precmd_return_notification
@@ -20,7 +20,7 @@ function local_setup_file() {
2020

2121
@test "plugins cmd-returned-notify: do not notify before elapsed time" {
2222
NOTIFY_IF_COMMAND_RETURNS_AFTER=10
23-
COMMAND_DURATION_START_SECONDS="$(_shell_duration_en)"
23+
COMMAND_DURATION_START_SECONDS="$(_command_duration_current_time)"
2424
export COMMAND_DURATION_START_SECONDS NOTIFY_IF_COMMAND_RETURNS_AFTER
2525
sleep 1
2626
run precmd_return_notification
@@ -37,7 +37,7 @@ function local_setup_file() {
3737
@test "lib command_duration: preexec set COMMAND_DURATION_START_SECONDS" {
3838
COMMAND_DURATION_START_SECONDS=
3939
assert_equal "${COMMAND_DURATION_START_SECONDS}" ""
40-
NOW="$(_shell_duration_en)"
40+
NOW="$(_command_duration_current_time)"
4141
_command_duration_pre_exec
4242
# We need to make sure to account for nanoseconds...
4343
assert_equal "${COMMAND_DURATION_START_SECONDS%.*}" "${NOW%.*}"

vendor/github.com/rcaloras/bash-preexec/.github/workflows/bats.yaml

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/rcaloras/bash-preexec/.travis.yml

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)