"$outer" is a quoted scalar variable so it always expands to one argument. If empty or unset, that still expands to one empty argument to join (and when you call your script with -o2, that's one -a 2 argument instead of the two arguments -a and 2).
Your join is probably GNU join in that it accepts options after non-option arguments. That "$outer" is a non-option argument when empty as it doesn't start with - so is treated as a file name and join complains about the third file name provided which it doesn't expect.
If you want a variable with a variable number of arguments, use an array:
outer=() ... (o) outer=(-a "$OPTARG");; ... join "${outer[@]}"
Though here you could also do:
outer= ... (o) outer="-a$OPTARG";; ... join ${outer:+"$outer"} -t "$delim" -1 "$col1" -2 "$col2" -- \ <(sort -t "$delim" -k"$col1,$col1" < "$f1") \ <(sort -t "$delim" -k"$col2,$col2" < "$f2")
Or:
unset -v outer ... (o) outer="$OPTARG";; ... join ${outer+-a "$outer"} ...
(that one doesn't work in zsh except in sh/ksh emulation).
Some other notes:
join -t '\t' doesn't work. You'd need delim=$'\t' to store a literal TAB in $delim - Remember to use
-- when passing arbitrary arguments to commands (or use redirections where possible). So sort -- "$f1" or better sort < "$f1" instead of sort "$f1". - Also bear in mind that for
join, the inputs must be sorted on the join key, not the whole line. - arithmetic expansions are also subject to split+glob so should also be quoted (
shift "$((OPTIND - 1))") (here not a problem though as you're using bash which doesn't inherit $IFS from the environment and you're not modifying IFS earlier in the script, but still good practice).