It looks like those are meant to be list of strings which you encode by storing them space separated in a scalar variable (assumes the strings don't contain that space character).
It would make more sense to use variables of type list/array, with shells that support them. For instance, with zsh and its ${varX:|varY} array subtraction operator:
VAR1=(1 2 3 4 5) VAR2=(1 3 5) VAR3=(${VAR1:|VAR2})
(VAR3=("${(@)VAR1:|VAR2}") to preserve empty elements)
Now, if you're limited to POSIX sh that is without array support except for $@, you'll have to be more creative.
The standard command for list intersection and substraction is comm. But the lists have to be provided as a sorted list, newline separated and inside files whose name is passed as arguments (though - can be used for one them to mean stdin).
So here, it becomes awkward to use. If your system supports /dev/fd/<n> special files:
VAR3=$(printf '%s\n' "$VAR1" | tr ' ' '\n' | sort | { printf '%s\n' "$VAR2" | tr ' ' '\n' | sort | comm -23 /dev/fd/3 - } 3<&0 | paste -sd ' ' -)
Or:
to_comm() { printf '%s\n' "$@" | tr ' ' '\n' | sort; } from_comm() { paste -sd ' ' -; } VAR3=$(to_comm "$VAR1" | { to_comm "$VAR2" | comm -23 /dev/fd/3 -;} 3<&0 |from_comm)
(that also assumes $VAR1 contains at least one element (how would you express a list with one empty elements differently from an empty list, BTW) and that elements don't contain newline characters).
So you might as well implement it by hand. Loop over each element of the first list and look them up in the second list.
In POSIX shells, you could use the split+glob operator:
IFS=' ' # split on space set -o noglob # we don't want the glob part VAR3= sep= for i in $VAR1; do case " $VAR2 " in (*" $i "*) ;; (*) VAR3=$VAR3$sep$i; sep=$IFS;; esac done
That can't be used if there may be empty elements (like in VAR1=' 2 3' or VAR1='1 3'). For that it would be better to use a non-whitespace separator (like | below) for which the splitting rules are different:
VAR1='*|foo bar||blah' VAR2='|blah' IFS='|' # split on | set -o noglob # we don't want the glob part VAR3= sep= for i in $VAR1''; do # that $VAR1 split+glob invocation will split the content of $VAR1 # into "*", "foo bar", "", "blah" while with IFS=" ", the empty # element wouldn't have been there as sequences of spaces would # have been seen as a single separator. case "|$VAR2|" in (*"|$i|"*) ;; (*) VAR3=$VAR3$sep$i; sep=$IFS;; esac done
The '' in $VAR1'' is to make sure foo| is split into "foo" and "" instead of just "foo" for instance in POSIX shells (most, as that's a POSIX requirement) that treat $IFS as a field terminator instead of separator.
Or you could use awk instead:
export VAR1 VAR2 VAR3=$(awk 'BEGIN{ n = split(ENVIRON["VAR1"], a1, /[ ]/) split(ENVIRON["VAR2"], a2, /[ ]/) for (i in a2) in_a2[a2[i]] for (i = 1; i <= n; i++) if (! (a1[i] in in_a2)) $(++NF) = a1[i] print}')