The difference between trim_start and trim_end is that the first pass of the loop in trim_start never matches while it always matches in trim_end. Change your input to ' abc ,def' and you'll see the same problem in both trim_start and trim_end.
On a successful match, $1 and $2 are cleared and so are $_[0] and $_[1] (since they are aliased to $1 and $2). On an unsuccessful match, $1 and $2 are left untouched.
Passing a global as an argument is bad, especially when that global is changed by the function to which it is being passed. The best solution is to pass a copy of the global to the function. This can be done by simply changing the call to
$sub->("$1", "$2");
Update: Added explanation.
In reply to Re: Regex capture consumed by non-capturing match
by ikegami
in thread Regex capture consumed by non-capturing match
by ribasushi
| For: | Use: | ||
| & | & | ||
| < | < | ||
| > | > | ||
| [ | [ | ||
| ] | ] |