Skip to content

feat(env): $EPOCHREALTIME and $SRANDOM - #157

Merged
LESdylan merged 1 commit into
developfrom
feat/epochrealtime-srandom
Sep 26, 2026
Merged

LESdylan merged 1 commit into
developfrom
feat/epochrealtime-srandom

Conversation

@LESdylan

Copy link
Copy Markdown
Member

What & why

Section 6 of #134:

$ echo "[${EPOCHREALTIME:-unset}] [${SRANDOM:-unset}]"
[unset] [unset]

$EPOCHSECONDS was already there, but it only counts whole seconds, which is too coarse to time a command from a prompt hook. The usual workaround, date +%s%N, forks once per prompt. bash 5.1 has both variables.

Change. expand_special_dyn_time (new file, src/environment/expand_dyn_time.c) is the tier after expand_special_dyn. That function had no room left under the norm's 25-line limit, so it now ends by calling the new one.

  • $EPOCHREALTIME is clock_gettime(CLOCK_REALTIME), formatted the way bash does: the seconds, a ., and six digits of microseconds.
  • $SRANDOM is 32 bits from getentropy(). bash promises that $SRANDOM cannot be seeded, so it never comes from the $RANDOM generator. getentropy is available on glibc 2.25+ (the oldest CI image, Ubuntu 22.04, has 2.35), on musl and on macOS. If the call fails, the fallback is the session PRNG's full 32 bits.

Both borrow state->linebuf, as $SECONDS does. Like the other dynamic variables, assigning to them does not freeze them.

Refs #134

How I verified it

  • Reproduced first. tests/scripts/60_epochrealtime_srandom.sh is graded against bash --posix 5.3.9. The values change on every read, so it prints properties, never values:

    • EPOCHREALTIME:
      • it has the sec.usec format;
      • its seconds agree with EPOCHSECONDS read just before and just after;
      • it moves about 50 ms across sleep 0.05;
      • assigning to it is ignored.
    • SRANDOM:
      • it is a decimal in 0..2^32−1 and wider than 15 bits;
      • it does not repeat across 16 reads;
      • RANDOM=42 does not make it repeat;
      • a command substitution gets a fresh value;
      • assigning to it is ignored.
    • Both work in arithmetic and under set -u.

    develop fails it at the first line. This branch matches bash, three runs in a row under ASan with leak detection. A false failure would need two independent 32-bit draws to be equal (a chance of about 2⁻³²), or sleep 0.05 taking more than 5 s.

  • Local gates on this commit (ASan debug build):

    • golden tests/tester: 5363/5363;
    • tests/run_scripts.sh against bash --posix: 125/125;
    • verify_alloc.sh: identical output on both heaps;
    • alloc_stress.sh: all clean;
    • tests/pty_suite.sh: 109 ok, 6 skipped, 4 failed.
      • Three of the failures are prompt_compat_matrix, prompt_drift_matrix and prompt_jobs_badge. They expect the non-root %/$ prompt and get #, because the container runs as root; they fail the same way on develop, and CI runs them as a normal user.
      • The fourth is zsh_glob_test's eqsub/reads-back: cat =(echo hi) printed nothing. That is an older race, about one run in 150 on develop too, and unrelated to this change. Its root cause and fix are in their own PR (fix/eqsub-unique-temp).
  • norminette is OK on every touched file.

  • src/environment/README.md lists the new file.

Notes / trade-offs

  • RANDOM=N still does not reseed $RANDOM, which is a divergence already documented in expand2.c. The test does not assert it either way; it only checks that $SRANDOM is unaffected.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RAmeHfJNm7XjYbMNrQqvkG


Generated by Claude Code

    $ echo "[${EPOCHREALTIME:-unset}] [${SRANDOM:-unset}]"
    [unset] [unset]

Section 6 of issue #134. $EPOCHSECONDS was there, but its one-second
resolution is no use for timing a command from a prompt hook, and the
way around it, `date +%s%N`, is a fork per prompt. bash 5.1 has both
variables.

expand_special_dyn_time (expand_dyn_time.c) is the next tier after
expand_special_dyn, which had no room left:
  - $EPOCHREALTIME is clock_gettime(CLOCK_REALTIME) formatted as bash
    formats it: seconds, '.', six digits.
  - $SRANDOM is 32 bits from getentropy(). bash promises it cannot be
    seeded, so it never comes from the $RANDOM generator. getentropy is
    in glibc 2.25+, musl and macOS, which covers every platform CI builds.
    If the kernel lacks the call, the session PRNG's full 32 bits are
    the fallback.
Both borrow state->linebuf like $SECONDS. Like the other dynamic
variables, an assignment does not make them constant.

tests/scripts/60_epochrealtime_srandom.sh, graded against bash --posix,
checks properties, not values:
  - EPOCHREALTIME:
    - sec.usec format;
    - its seconds agree with EPOCHSECONDS read just before and after;
    - it moves about 50ms across `sleep 0.05`;
    - an assignment is ignored.
  - SRANDOM:
    - a decimal in 0..2^32-1, and wider than 15 bits;
    - never repeats across reads;
    - RANDOM=42 does not repeat it;
    - fresh in a command substitution;
    - an assignment is ignored.
  - Both work in arithmetic and under set -u.
develop fails it at the first line; this branch matches bash, three runs
in a row under ASan with leak detection.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RAmeHfJNm7XjYbMNrQqvkG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants