From cee011667c03668fcc9f719c2c6ca5f95e414d50 Mon Sep 17 00:00:00 2001 From: Sal Date: Thu, 3 Sep 2026 03:59:08 +0100 Subject: [PATCH] fix(load): strip the deferred atinit marker on both branches `atinit'!...'` defers the hook, and the `!' has to be stripped before the body is evaluated. Two branches strip it and each had its own defect. The nocd branch of the source path stripped `1' instead of `!': eval "${ICE[atinit]#1}" so the body ran as a command named `!typeset', reported by #445. The command branch put the strip inside the subscript: eval "${ICE[atinit#!]}" which looks up an ICE key named `atinit#!', finds nothing, and evaluates the empty string. That one is silent: no error, no hook, and the load reports success. It was not in the report and affects `as"command"' plug-ins without nocd, which is the ordinary case for that branch. Reproduced against the previous source: CASE as-command atinit-bang: MARK_CMD=unset (no diagnostic at all) CASE normal+nocd atinit-bang: MARK_NORM=unset ((eval):1: command not found: !typeset) Both now read ${ICE[atinit]#!}, matching the other two sites. tests/atinit-deferred-marker.zsh loads an `as"command"' plug-in without nocd and a normal plug-in with nocd, each with a deferred atinit that sets a marker, and asserts both markers. Each defect was reintroduced separately and the matching assertion observed failing. Closes #445 --- .github/workflows/zsh-n.yml | 13 ++++++ tests/atinit-deferred-marker.zsh | 73 ++++++++++++++++++++++++++++++++ zi.zsh | 4 +- 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tests/atinit-deferred-marker.zsh diff --git a/.github/workflows/zsh-n.yml b/.github/workflows/zsh-n.yml index 01076b7d..fbf550d0 100644 --- a/.github/workflows/zsh-n.yml +++ b/.github/workflows/zsh-n.yml @@ -10,6 +10,7 @@ on: - "zi.zsh" - "lib/**" - "tests/annex-unregister.zsh" + - "tests/atinit-deferred-marker.zsh" - "tests/archive-extraction.zsh" - "tests/completion-refresh.zsh" - "tests/hook-ownership.zsh" @@ -36,6 +37,7 @@ on: - "zi.zsh" - "lib/**" - "tests/annex-unregister.zsh" + - "tests/atinit-deferred-marker.zsh" - "tests/archive-extraction.zsh" - "tests/completion-refresh.zsh" - "tests/hook-ownership.zsh" @@ -256,6 +258,17 @@ jobs: - name: Test annex unregister run: zsh -f tests/annex-unregister.zsh + atinit-deferred-marker: + name: Atinit Deferred Marker + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Zsh + run: sudo apt update && sudo apt-get install -yq zsh + - name: Test atinit deferred marker + run: zsh -f tests/atinit-deferred-marker.zsh + archive-extraction: name: Archive extraction runs-on: ubuntu-latest diff --git a/tests/atinit-deferred-marker.zsh b/tests/atinit-deferred-marker.zsh new file mode 100644 index 00000000..82a008e3 --- /dev/null +++ b/tests/atinit-deferred-marker.zsh @@ -0,0 +1,73 @@ +#!/usr/bin/env zsh +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +builtin emulate -R zsh +setopt pipe_fail + +fail() { + builtin print -u2 -r -- "not ok - $1" + exit 1 +} + +typeset project_root="${ZI_TEST_CHECKOUT:-${0:A:h:h}}" +typeset temp_root +temp_root="$(command mktemp -d "${TMPDIR:-/tmp}/zi-atinit-marker-test.XXXXXXXX")" || + fail "create temporary directory" +trap 'command rm -rf -- "$temp_root"' EXIT INT TERM + +command mkdir -p \ + "${temp_root}/home" \ + "${temp_root}/cache" \ + "${temp_root}/config" \ + "${temp_root}/data" \ + "${temp_root}/zdotdir" \ + "${temp_root}/cmdplug" \ + "${temp_root}/normplug" || fail "create isolated environment" + +builtin print -rl -- '#!/bin/sh' 'echo tool' \ + > "${temp_root}/cmdplug/mytool" || fail "write the command plug-in payload" +command chmod +x "${temp_root}/cmdplug/mytool" || fail "make the payload executable" +builtin print -r -- ':' \ + > "${temp_root}/normplug/normplug.plugin.zsh" || fail "write the normal plug-in" + +env \ + HOME="${temp_root}/home" \ + XDG_CACHE_HOME="${temp_root}/cache" \ + XDG_CONFIG_HOME="${temp_root}/config" \ + XDG_DATA_HOME="${temp_root}/data" \ + ZDOTDIR="${temp_root}/zdotdir" \ + ZI_TEST_CHECKOUT="$project_root" \ + ZI_TEST_ROOT="$temp_root" \ + zsh -f <<'ZSH' || fail "a deferred atinit hook did not run" +builtin emulate -R zsh +setopt pipe_fail + +builtin source "${ZI_TEST_CHECKOUT}/zi.zsh" || return 1 +.zi-prepare-home || return 1 + +# The `!' prefix defers the hook, and the prefix has to be stripped before the +# body is evaluated. Two branches strip it, and each had its own defect: +# the command branch wrote the strip inside the subscript, `${ICE[atinit#!]}', +# which looks up a key that does not exist and silently evaluated nothing; the +# nocd branch of the source path stripped `1' instead of `!', leaving the body +# to fail as a command called `!typeset'. + +# Command branch, without nocd. +zi ice as"command" pick"mytool" atinit'!typeset -g ZI_TEST_MARK_CMD=ran' +zi load "${ZI_TEST_ROOT}/cmdplug" >/dev/null 2>&1 +[[ ${ZI_TEST_MARK_CMD:-unset} == ran ]] || { + builtin print -u2 -r -- "as\"command\" deferred atinit did not run: ${ZI_TEST_MARK_CMD:-unset}" + return 1 +} + +# Source path, with nocd active. +zi ice nocd atinit'!typeset -g ZI_TEST_MARK_NOCD=ran' +zi load "${ZI_TEST_ROOT}/normplug" >/dev/null 2>&1 +[[ ${ZI_TEST_MARK_NOCD:-unset} == ran ]] || { + builtin print -u2 -r -- "nocd deferred atinit did not run: ${ZI_TEST_MARK_NOCD:-unset}" + return 1 +} +ZSH + +builtin print -r -- "ok - the deferred atinit marker is stripped on the command and nocd branches" diff --git a/zi.zsh b/zi.zsh index 2e8ae220..82d749d9 100644 --- a/zi.zsh +++ b/zi.zsh @@ -1854,7 +1854,7 @@ builtin setopt no_aliases (( ++ ZI[TMP_SUBST_DEPTH] )) } local ZERO - [[ $ICE[atinit] = '!'* ]] && { local ___oldcd="$PWD"; (( ${+ICE[nocd]} == 0 )) && { () { builtin setopt local_options no_auto_pushd; builtin cd -q "${${${(M)___user:#%}:+$___plugin}:-${ZI[PLUGINS_DIR]}/${___id_as//\//---}}"; } && eval "${ICE[atinit#!]}"; ((1)); } || eval "${ICE[atinit]#!}"; () { builtin setopt local_options no_auto_pushd; builtin cd -q "$___oldcd"; }; } + [[ $ICE[atinit] = '!'* ]] && { local ___oldcd="$PWD"; (( ${+ICE[nocd]} == 0 )) && { () { builtin setopt local_options no_auto_pushd; builtin cd -q "${${${(M)___user:#%}:+$___plugin}:-${ZI[PLUGINS_DIR]}/${___id_as//\//---}}"; } && eval "${ICE[atinit]#!}"; ((1)); } || eval "${ICE[atinit]#!}"; () { builtin setopt local_options no_auto_pushd; builtin cd -q "$___oldcd"; }; } [[ -n ${ICE[src]} ]] && { ZERO="${${(M)ICE[src]##/*}:-$___pdir_orig/${ICE[src]}}"; (( ${+ICE[silent]} )) && { { [[ -n $___precm ]] && { builtin ${___precm[@]} 'source "$ZERO"'; ((1)); } || { ((1)); $___builtin source "$ZERO"; }; } 2>/dev/null 1>&2; (( ___retval += $? )); ((1)); } || { ((1)); { [[ -n $___precm ]] && { builtin ${___precm[@]} 'source "$ZERO"'; ((1)); } || { ((1)); $___builtin source "$ZERO"; }; }; (( ___retval += $? )); }; } [[ -n ${ICE[multisrc]} ]] && { local ___oldcd="$PWD"; () { builtin setopt local_options no_auto_pushd; builtin cd -q "$___pdir_orig"; }; eval "reply=(${ICE[multisrc]})"; () { builtin setopt local_options no_auto_pushd; builtin cd -q "$___oldcd"; }; local ___fname; for ___fname in "${reply[@]}"; do ZERO="${${(M)___fname:#/*}:-$___pdir_orig/$___fname}"; (( ${+ICE[silent]} )) && { { [[ -n $___precm ]] && { builtin ${___precm[@]} 'source "$ZERO"'; ((1)); } || { ((1)); $___builtin source "$ZERO"; }; } 2>/dev/null 1>&2; (( ___retval += $? )); ((1)); } || { ((1)); { [[ -n $___precm ]] && { builtin ${___precm[@]} 'source "$ZERO"'; ((1)); } || { ((1)); $___builtin source "$ZERO"; }; }; (( ___retval += $? )); }; done; } # Run the atload hooks right before atload ice. @@ -1895,7 +1895,7 @@ builtin setopt no_aliases (( ${+ICE[blockf]} )) && { local -a fpath_bkp; fpath_bkp=( "${fpath[@]}" ); } local ZERO="$___pdir_path/$___fname" (( ${+ICE[aliases]} )) || builtin setopt no_aliases - [[ $ICE[atinit] = '!'* ]] && { local ___oldcd="$PWD"; (( ${+ICE[nocd]} == 0 )) && { () { builtin setopt local_options no_auto_pushd; builtin cd -q "${${${(M)___user:#%}:+$___plugin}:-${ZI[PLUGINS_DIR]}/${___id_as//\//---}}"; } && eval "${ICE[atinit]#!}"; ((1)); } || eval "${ICE[atinit]#1}"; () { builtin setopt local_options no_auto_pushd; builtin cd -q "$___oldcd"; }; } + [[ $ICE[atinit] = '!'* ]] && { local ___oldcd="$PWD"; (( ${+ICE[nocd]} == 0 )) && { () { builtin setopt local_options no_auto_pushd; builtin cd -q "${${${(M)___user:#%}:+$___plugin}:-${ZI[PLUGINS_DIR]}/${___id_as//\//---}}"; } && eval "${ICE[atinit]#!}"; ((1)); } || eval "${ICE[atinit]#!}"; () { builtin setopt local_options no_auto_pushd; builtin cd -q "$___oldcd"; }; } (( ${+ICE[silent]} )) && { { [[ -n $___precm ]] && { builtin ${___precm[@]} 'source "$ZERO"'; ((1)); } || { ((1)); $___builtin source "$ZERO"; }; } 2>/dev/null 1>&2; (( ___retval += $? )); ((1)); } || { ((1)); { [[ -n $___precm ]] && { builtin ${___precm[@]} 'source "$ZERO"'; ((1)); } || { ((1)); $___builtin source "$ZERO"; }; }; (( ___retval += $? )); } [[ -n ${ICE[src]} ]] && { ZERO="${${(M)ICE[src]##/*}:-$___pdir_orig/${ICE[src]}}"; (( ${+ICE[silent]} )) && { { [[ -n $___precm ]] && { builtin ${___precm[@]} 'source "$ZERO"'; ((1)); } || { ((1)); $___builtin source "$ZERO"; }; } 2>/dev/null 1>&2; (( ___retval += $? )); ((1)); } || { ((1)); { [[ -n $___precm ]] && { builtin ${___precm[@]} 'source "$ZERO"'; ((1)); } || { ((1)); $___builtin source "$ZERO"; }; }; (( ___retval += $? )); }; } [[ -n ${ICE[multisrc]} ]] && { local ___oldcd="$PWD"; () { builtin setopt local_options no_auto_pushd; builtin cd -q "$___pdir_orig"; }; eval "reply=(${ICE[multisrc]})"; () { builtin setopt local_options no_auto_pushd; builtin cd -q "$___oldcd"; }; for ___fname in "${reply[@]}"; do ZERO="${${(M)___fname:#/*}:-$___pdir_orig/$___fname}"; (( ${+ICE[silent]} )) && { { [[ -n $___precm ]] && { builtin ${___precm[@]} 'source "$ZERO"'; ((1)); } || { ((1)); $___builtin source "$ZERO"; }; } 2>/dev/null 1>&2; (( ___retval += $? )); ((1)); } || { { [[ -n $___precm ]] && { builtin ${___precm[@]} 'source "$ZERO"'; ((1)); } || { ((1)); $___builtin source "$ZERO"; }; }; (( ___retval += $? )); } done; }