The prefix that went missing
- #expansion
- #arrays
- #bugfix
Shell has a compact idiom for prepending to every element of an array:
files=(report.txt notes.txt)
printf '%s\n' "${files[@]/#/backup-}"In bash that prints backup-report.txt and backup-notes.txt. huck printed
the filenames back at you, unchanged:
# before
$ huck -c 'files=(report.txt notes.txt); printf "%s\n" "${files[@]/#/backup-}"'
report.txt
notes.txt
# after
backup-report.txt
backup-notes.txtThe scalar form was equally quiet — ${s/#/pre} on abc gave back abc
rather than preabc.
Why it did nothing
${x/pattern/replacement} substitutes; the # says anchor the match at the
start (and % at the end). What makes the prepend idiom work is that the
pattern here is empty, so the match is empty too, and an empty match at
the start is exactly the position you want the replacement inserted.
huck's substitution routine opened with a reasonable-looking shortcut: an empty pattern matches nothing, so return the value unchanged. That is true everywhere except at an anchor — which is the one place the empty pattern is ever deliberately used. The shortcut ran before the anchor was consulted, so the interesting case never got a chance.
The fix moves the shortcut after the anchor check. Anchored, an empty pattern
now inserts at that end; unanchored (${x//}, ${x///-}) it stays the no-op
it should be, as does the replace-all spelling ${x//#/pre}, which bash also
leaves alone.
Where it came from
This one is a footnote to the blog itself. The posts backfilled this week quote real output, which meant rebuilding huck at nine historical commits and running fragments through them. One of those runs printed a v71-era message:
huck: ${a[…]}: modifier Substitute { … anchor: Prefix … } not supported on array in v71
The array path was implemented later — and the anchored-empty-pattern case inside it quietly became a no-op instead of an error. It had gone from loud to silent, which is the worst direction for a bug to travel, and no test noticed because nothing exercised the idiom.
Twenty-seven byte-identical fragments now do, covering the insert cases, the
six no-op spellings, and arrays — indexed, associative, @, *, and elements
containing spaces.
Issue #448. Also filed and
still open from the same archaeology: #449,
an EXIT trap set inside a subshell never firing — which turns out to need
the same plumbing as #442,
where exit inside any trap action is swallowed.