Skip to content

Commit 89cf657

Browse files
committed
fix(spec): rework zsh completion mock around vared+zpty crash
`vared -c tmp` inside `zpty {,}myfn` aborts with `free(): invalid pointer` on glibc-based zsh 5.9 builds (Ubuntu 24.04, recent Homebrew), so every zsh case in `mock_spec.cr` returned the abort string instead of completions. Spawning a fresh `zsh -i -c 'source <file>'` inside the zpty gives ZLE its own interactive session and avoids the heap corruption. The completion setup is materialized to a tempfile so the inner shell can pick it up. Also drop the outer EXIT/signal trap: `zpty` propagates inherited traps to its child, and after the first iteration that child runs the trap on exit and deletes the source file the next iteration still needs. Cleanup now runs once after the loop instead. Tighten up the delimiter parser too — using `==` substring matches and keeping the post-D_OPN remainder in `PTY_LINES` makes the unambiguous single-match path work without a leading blank line.
1 parent c39e93f commit 89cf657

1 file changed

Lines changed: 64 additions & 50 deletions

File tree

src/tabular/shell/zsh.ecr

Lines changed: 64 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,73 @@
11
# SEE: https://unix.stackexchange.com/questions/668618/how-to-write-automated-tests-for-zsh-completion/668827#668827
2+
#
3+
# Background:
4+
# The Stack Exchange recipe runs `vared -c tmp` inside `zpty {,}fn`, but on
5+
# glibc-based zsh 5.9 builds (Ubuntu 24.04, Debian sid, recent Homebrew)
6+
# that combination corrupts the heap and the pty emits `free(): invalid
7+
# pointer` instead of any completions. Spawning a fresh interactive
8+
# `zsh -i -c 'source <file>'` inside the zpty gives ZLE its own session and
9+
# sidesteps the crash. We also avoid an EXIT trap on the outer shell — zpty
10+
# propagates inherited traps to the child, so an outer cleanup trap would
11+
# delete the source file out from under the next iteration.
212
autoload -U compinit
3-
compinit
13+
compinit -u
414

515
<% completers.each do |comp| -%>
616
eval "$(<%= comp %> completion <%= bin %>)"
17+
<% end %>
18+
19+
INNER=$(mktemp -p /var/tmp tabular_<%= pty %>.XXXXXX) || exit 1
20+
{
21+
print -r -- 'autoload -U compinit'
22+
print -r -- 'compinit -u'
23+
<% completers.each do |comp| -%>
24+
print -r -- 'eval "$(<%= comp %> completion <%= bin %>)"'
725
<% end -%>
26+
print -r -- 'D_OPN=$'\''\C-B'\'
27+
print -r -- 'D_CLS=$'\''\C-C'\'
28+
print -r -- 'zstyle ":completion:*" max-matches-width 10'
29+
print -r -- 'zstyle ":completion:*" list-colors ""'
30+
print -r -- 'zle -C {,,}complete-word'
31+
print -r -- 'complete-word () {'
32+
print -r -- ' unset "compstate[vared]"'
33+
print -r -- ' compstate[insert]=menu'
34+
print -r -- ' compadd -x "${D_OPN}"'
35+
print -r -- ' _main_complete "$@"'
36+
print -r -- ' if [ -n "$compstate[unambiguous]" ] && [ "$compstate[nmatches]" = "1" ]; then'
37+
print -r -- ' result=${compstate[unambiguous]/#$BASH_REMATCH}'
38+
print -r -- ' echo -e "${D_OPN}\n${result}\n${D_CLS}"'
39+
print -r -- ' fi'
40+
print -r -- ' compadd -J -last- -x "${D_CLS}"'
41+
print -r -- ' exit'
42+
print -r -- '}'
43+
print -r -- 'bindkey "^I" complete-word'
44+
print -r -- 'vared -c tmp'
45+
} > "${INNER}"
46+
47+
D_OPN=$'\C-B'
48+
D_CLS=$'\C-C'
849

9-
compmock () {
10-
export D_OPN=$'\C-B'
11-
export D_CLS=$'\C-C'
12-
13-
<%= pty %> () {
14-
zstyle ':completion:*' max-matches-width 10 # Ensure alt-names on separate rows
15-
zstyle ':completion:*' list-colors '' # Disable colouring
16-
17-
# Bind a custom widget to TAB.
18-
bindkey '^I' complete-word
19-
zle -C {,,}complete-word
20-
complete-word () {
21-
unset 'compstate[vared]' # Disguise a "normal" command-line
22-
compstate[insert]=menu # Ensure menu is always displayed
23-
compadd -x "${D_OPN}" # Open delimiter
24-
_main_complete "$@" # Run completion
25-
# Intercept unambiguous match
26-
if [ -n "$compstate[unambiguous]" ] && [ "$compstate[nmatches]" = '1' ]; then
27-
result=${compstate[unambiguous]/#$BASH_REMATCH}
28-
echo -e "${D_OPN}\n${result}\n${D_CLS}"
29-
fi
30-
compadd -J -last- -x "${D_CLS}" # close delimiter
31-
exit
32-
}
33-
34-
vared -c tmp # Start line editor
35-
}
36-
37-
zmodload zsh/zpty # Load the pseudo terminal module.
38-
trap 'zpty -d' ABRT EXIT HUP INT QUIT TERM # Delete the pty.
39-
40-
while read -r -t10 -A ARGS; do
41-
zpty {,}<%= pty %> # Create a new pty and run our function in it.
42-
zpty -w -n <%= pty %> "$1 $ARGS[*]"$'\t' # Simulate a command being typed and tabbed.
43-
44-
# Capture terminal output
45-
PTY_LINES=()
46-
while zpty -r <%= pty %> REPLY; do
47-
[[ "${REPLY}" =~ "${D_CLS}" ]] && break
48-
[[ "${REPLY}" =~ "${D_OPN}" ]] \
49-
&& PTY_LINES=() \
50-
|| PTY_LINES+=("${REPLY%%$'\n'}")
51-
done
52-
53-
print -nrC1 -- "${PTY_LINES[@]}" | sed -r -e 's/\x1b\[[0-9;]*m?//g' -e 's/\r//g' -e 's/ *$//g'
54-
print -n -- $'\C-@'
55-
zpty -d <%= pty %>
50+
zmodload zsh/zpty
51+
52+
while read -r -t10 -A ARGS; do
53+
zpty <%= pty %> "zsh -i -c 'source ${INNER}'"
54+
zpty -w -n <%= pty %> "<%= prog %> ${ARGS[*]}"$'\t'
55+
56+
PTY_LINES=()
57+
while zpty -r <%= pty %> REPLY; do
58+
[[ "${REPLY}" == *"${D_CLS}"* ]] && break
59+
if [[ "${REPLY}" == *"${D_OPN}"* ]]; then
60+
REPLY="${REPLY##*${D_OPN}}"
61+
PTY_LINES=()
62+
fi
63+
PTY_LINES+=("${REPLY%%$'\n'}")
5664
done
57-
}
5865

59-
compmock <%= prog %>
66+
print -nrC1 -- "${PTY_LINES[@]}" \
67+
| sed -r -e 's/\x1b\[[0-9;]*m?//g' -e 's/\r//g' -e 's/ *$//g' \
68+
| sed '/^$/d'
69+
print -n -- $'\C-@'
70+
zpty -d <%= pty %>
71+
done
72+
73+
rm -f "${INNER}"

0 commit comments

Comments
 (0)