How to correctly export env variables containing spaces in ZSH

Solution 1:

I think you're (kind of) abusing environment variables here. That space is part of the variable, and when you're performing parameter expansion with $, it will simply pass that space to the command. mvn gets one argument – your variable contents.

As Kamil Maciorowski correctly observes in a comment, this is specific to Zsh. What you are proposing would work in Bash if you left out the quotes:

bash-5.0$ export test="foo bar"
bash-5.0$ ls $test
ls: bar: No such file or directory
ls: foo: No such file or directory
bash-5.0$ ls "$test"
ls: foo bar: No such file or directory

One alternative to do what you want would be a global alias (also a feature of Zsh, not Bash), which can be substituted anywhere in a line. It keeps its spaces:

$ alias -g SKIP="-Dskip1 -Dskip2"
$ alias -g SKIP2="-Dskip3"
$ mvn SKIP SKIP2

This will simply "append" the string wherever added.

Solution 2:

The problem has nothing to do with quoting or escaping, it's all to do with word splitting (or the lack thereof). In most shells, if you use a variable that contains whitespace without putting double-quotes around it, the value will be split into "words" based on that whitespace. So if the variable SKIP is set to -Dskip1 -Dskip2 (note that there are no quotes or escapes in this value), and you use it like mvn $SKIP, it becomes equivalent to mvn "-Dskip1" "-Dskip2".

But zsh isn't most shells, and (by default) it does not do word splitting. If the variable is set to -Dskip1 -Dskip2, then that's what's getting passed to the command, as a single argument. The quotes you see in the set -x output aren't really there, they are just added to make it explicit that the entire string is being passed as a single argument.

There are a couple of ways to tell zsh that you want word splitting done. You can use the = modifier in the expansion:

mvn ${=SKIP}

Or you can turn on sh-like splitting for all expansions with a shell option:

setopt shwordsplit
mvn $SKIP

...however, this can have unpleasant side effects, like unexpected splitting of things you thought were single arguments, and expansion of anything that looks like a filename wildcard into a list of filenames. Shell word splitting tends to cause bugs, which is why it's disabled by default in zsh. The really proper way to store multiple strings in a variable is to use an array instead of a plain variable:

SKIP=(-Dskip1 -Dskip2)
mvn "${SKIP[@]}"

This will work in zsh, bash, and some other shells (but not those without array support). However, you cannot export arrays from zsh (well, you can run export SKIP, but it doesn't do anything). Arrays are a shell feature, while environment variables are an OS thing that only supports plain strings. Do you really need to export these options, or are you just using them within the shell?

Solution 3:

Single quotes appear in the diagnostic line printed because of set -x. They do not get to mvn. The variable content (-Dskip1 -Dskip2) gets to mvn as a single argument because in zsh you don't need to quote variables rigidly (you do in POSIX-compliant shells).

The problem seems to be in the way zsh expands variables. I think you want word splitting to occur when $SKIP is unquoted (like it happens in other shells) but in zsh it does not normally occur.

Use ${=SKIP} to trigger word splitting after substitution.

Example:

% SKIP='-Dskip1 -Dskip2' 
% printf "%s\n" $SKIP   
-Dskip1 -Dskip2
% printf "%s\n" ${=SKIP}
-Dskip1
-Dskip2
%