With GNU awk:
$ printf '%s\n' {foo,bar}{bar,foo} neither | gawk 'xor(/foo/,/bar/)'
foofoo
barbar
Or portably:
awk '((/foo/) + (/bar/)) % 2'
With a grep with support for -P (PCRE):
grep -P '^((?=.*foo)(?!.*bar)|(?=.*bar)(?!.*foo))'
With sed:
sed '
/foo/{
/bar/d
b
}
/bar/!d'
If you want to consider whole words only (that there is neither foo nor bar in foobar or barbar for instance), you'd need to decide how those words are delimited. If it's by any character other than letters, digits and underscore like the -w option of many grep implementation does, then you'd change those to:
gawk 'xor(/\<foo\>/,/\<bar\>/)'
awk '((/(^|[^[:alnum:]_)foo([^[:alnum:]_]|$)/) + \
(/(^|[^[:alnum:]_)bar([^[:alnum:]_]|$)/)) % 2'
grep -P '^((?=.*\bfoo\b)(?!.*\bbar\b)|(?=.*\bbar\b)(?!.*\bfoo\b))'
For sed that becomes a bit complicated unless you have a sed implementation like GNU sed that supports \</\> as word boundaries like GNU awk does.