From 06ab6de19373d7aab2786fe25b35c1b4fdf7c007 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 12 Sep 2026 07:33:40 +0100 Subject: [PATCH] fix(load): consume blank before-load hook replacements `(z)` splits a blank string into one empty word, so a before-load hook that consumed its final request and returned an empty `ZI[annex-before-load:new-@]` reached the loader as a blank object ID and failed with "No plugin or snippet ID given". Drop the word when the replacement holds no non-whitespace character. Apply the same guard to `new-global-ices`, where the stray word made an empty ice-list look odd and malformed, costing a spurious warning and 7 added to the return status. Quoted non-empty replacements, following requests, and odd hook failure statuses are unchanged. Refs #511 --- tests/load-object-status.zsh | 48 ++++++++++++++++++++++++++++++++++++ zi.zsh | 7 ++++++ 2 files changed, 55 insertions(+) diff --git a/tests/load-object-status.zsh b/tests/load-object-status.zsh index 61e0a4e..f064eb2 100644 --- a/tests/load-object-status.zsh +++ b/tests/load-object-status.zsh @@ -81,6 +81,54 @@ check "snippet failure" snippet 5 5 || return 1 return 1 } } || return 1 + +# Before-load hooks can consume a request without loading an object. Exercise +# real dispatch so a blank replacement cannot become a blank object ID. +typeset -a attempted +integer hook_status=2 +typeset replacement='' new_ices='' +.zi-load-object() { attempted+=( "$2" ); return 0; } +probe_replace() { + [[ $2 == fixture ]] || return 0 + ZI[annex-before-load:new-@]="${replacement}${4:+ $4}" + ZI[annex-before-load:new-global-ices]=$new_ices + return $hook_status +} +@zi-register-annex probe-replace hook:before-load-2 probe_replace '' '' +zi light @fixture || { print -u2 'empty replacement failed'; return 1; } +(( $#attempted == 0 )) || return 1 + +hook_status=3 +zi light @fixture +(( $? == 3 && $#attempted == 0 )) || { + print -u2 'empty replacement lost the hook error' + return 1 +} +hook_status=2 +zi for @fixture @example/following || return 1 +[[ $#attempted == 1 && $attempted[1] == example/following ]] || return 1 + +replacement="@'example/with spaces'" +zi light @fixture || return 1 +[[ $#attempted == 2 && $attempted[2] == 'example/with spaces' ]] || return 1 + +# A later empty replacement must not reuse the previous non-empty array. +replacement='' +zi light @fixture || return 1 +(( $#attempted == 2 )) || return 1 + +# `(z)' yields one blank word for whitespace too, so it is the same consumed +# state and must not reach the loader as a blank object ID either. +replacement=' ' +zi light @fixture || { print -u2 'whitespace replacement failed'; return 1; } +(( $#attempted == 2 )) || { print -u2 'whitespace replacement loaded an object'; return 1; } +replacement='' + +# A blank global-ice override means "no global ices", not a malformed odd list. +hook_status=6 +zi light @fixture || { print -u2 'blank global ices reported a bad ice-list'; return 1; } +(( $#attempted == 2 )) || return 1 +hook_status=2 ZSH builtin print -r -- "ok - .zi-load-object reports its own load status and leaves aggregation to the caller" diff --git a/zi.zsh b/zi.zsh index 5318fa1..cc732f3 100644 --- a/zi.zsh +++ b/zi.zsh @@ -2970,12 +2970,19 @@ zi() { if (( ___retval2 & 2 )) { local -a ___args ___args=( "${(@Q)${(@z)ZI[annex-before-load:new-@]}}" ) + # (z) yields one empty word for a blank string, which would be + # read as a blank object ID. A blank replacement means the hook + # consumed the request, so drop the word. + [[ -n ${ZI[annex-before-load:new-@]//[[:space:]]/} ]] || ___args=() builtin set -- "${___args[@]}" } # Override $___ices? if (( ___retval2 & 4 )) { local -a ___new_ices ___new_ices=( "${(Q@)${(@z)ZI[annex-before-load:new-global-ices]}}" ) + # Same blank-word split as above; here it would make an empty + # ice-list look like an odd, malformed one. + [[ -n ${ZI[annex-before-load:new-global-ices]//[[:space:]]/} ]] || ___new_ices=() (( 0 == ${#___new_ices} % 2 )) && \ ___ices=( "${___new_ices[@]}" ) || \ { [[ ${ZI[MUTE_WARNINGS]} != (1|true|on|yes) ]] && \