I want 'cat' to print but only if it has nothing following it except 's'
in which case 'cats' will print irrespective of following characters.
So 'cat' and 'cats' are the only outputs and
'catz' will print nothing but 'catszzzzz' will print 'cats'. At the
moment I'm doing this with two sequential tests and it works fine, but
I'm wondering if they can be combined.
This should do it, if I understood your requirements correctly:
${${(M)string:#cat(|s*)}[1,4]}
The first expansion leaves cat(|s*) intact (cat, cats, catsz, catszz, etc.) and turns everything else into nul. The second expansion extracts the first 4 characters, effectively dropping any potential suffix after cats.
Another option would be to capture the interesting part of the match explicitly (requires extended_glob):
[[ $string == (#b)((cat)|(cats)*) ]] && out=$match[2]$match[3] || out=
Roman