for starters, it helps to have a good test case:
apple
banana
orange
apple and banana
apple and orange
banana and apple
banana and orange
orange and apple
orange and banana
apple and banana and orange
orange and banana and orange
none of the above
apple and microsoft
https://regex101.com/r/ebpyjX/1
first, ^(?P<XOR>(?P<apple_not_banana>(?=.*apple)(?!.*banana))|(?P<banana_not_apple>(?!.*apple)(?=.*banana))).*$
is reads as"apple NOT banana, OR, not apple, banana" works directly as XOR but would potentially become long with many subterms:
one and a halfth, ^(?P<XOR>(?=.*apple)(?!.*banana)(?!.*orange)|(?!.*apple)(?=.*banana)(?!.*orange)|(?!.*apple)(?!.*banana)(?=.*orange)).*$
reads as "apple not banana not orange; not apple, banana, not orange; not apple, not banana, orange" and is an example of the three-valued xor, via enumeration of the product:
one and three quarters ^(?P<XOR>(?!(?P<exclusive>(.*apple|.*banana|.*orange){2,}))(?=.*(?P<fruit>apple|banana|orange)).*$)
reads as, "NOT (apple, banana, or orange) 2 or more times, and apple or banana or orange" could scale better in terms of length to larger numbers of terms because it groups logic more effectively, EDIT: however, in practice searching NLM UMLS, I discovered this approach may cause catastrophic backtracking.
second, (?P<xor_is_or_and_nand>^(?P<nand_apple_banana>(?!(?:(?=.*apple)(?=.*banana))))(?P<apple_or_banana>(?=.*apple|.*banana)).*$)
reads as "NOT apple AND banana, AND apple OR banana" shows how XOR is OR + NAND i.e. in lisp this would look like (and (or apple banana) (nand apple banana))
:
finally, (?P<not_with_xor>^(?P<not_microsoft>(?!.*microsoft))(?P<nand_apple_banana>(?!(?:(?=.*apple)(?=.*banana))))(?P<apple_or_banana>(?=.*apple|.*banana)).*$)
reads "NOT microsoft, AND NOT apple AND banana, AND apple OR banana" shows how to ensure coexistence of other negations with the xor query: