The (.*?) in your second attempt is a non-greedy match, which means it will match the smallest possible substring that satisfies the pattern. In this case, it matches an empty string because the optional (?:b=(\w))? can also match the empty string. Use a positive lookahead assertion to ensure that the (.*?) matches up to the optional b=(\w) part, but doesn't include it: