Repository: spack/spack

Issue:
spack prints spec that is not reusable ( CMakePackage + ~variant )
<!--
*Explain, in a clear and concise way, the command you ran and the result you were trying to achieve.
Example: "I ran Spack find to list all the installed packages and..."*
-->

Running `spack spec` for a **CMake package** where a **variant is deactivated**, Spack prints a spec that is not reusable:

### Spack version
Tested on develop and 13.2

### Steps to reproduce the issue

```console
$ spack spec h5cpp~mpi
h5cpp@1.10.4-5%clang@10.0.0-apple build_type=RelWithDebInfo ~mpi arch=darwin-mojave-x86_64
...
$ spack spec h5cpp@1.10.4-5%clang@10.0.0-apple build_type=RelWithDebInfo ~mpi arch=darwin-mojave-x86_64
zsh: no such user or named directory: mpi
...
```

### Error Message

This doesn’t generate an error in Spack itself. 

### Information on your system

Default Spack configuration on MacOS, with zsh.

### Comments

The problem is mainly that Spack generates a spec using `~variant` after a whitespace. This seems to happen only with CMake packages, mainly because those have a `build_type` variant that comes first.

Solutions could be to reorder the variants attache `~variant` to the spec, or use `-` instead of `~`.


Discussion:
@scheibelp @becker33 I would appreciate if you could have a look at this one.
I think this is particular to using `zsh` as opposed to the more common `bash`, which passes through nonexistent user directories `~nonexistent` rather than erroring out. Of course, if a system has a user named `mpi` or `cuda`, then even `bash` users will be out of luck :)
@sethrj good to know!
I’ll stick with my beloved zsh :), which is be the default on MacOS Catalina, by the way.
I thought the default on MacOS Catalina was dash, not zsh.

I believe that you can eliminate this error by defining an appropriate function named `zsh_directory_name` that returns whatever its input is. See the zsh manual chapter 14.7.1 here: http://zsh.sourceforge.net/Doc/Release/Expansion.html#Dynamic-named-directories
[Here](https://support.apple.com/en-us/HT208050) is the reference for the default shell on catalina (for new users).

Per [spack documentation](https://spack.readthedocs.io/en/latest/basic_usage.html#variants) I was expecting this issue to be considered a bug in Spack, don’t you think?

Resolving patch:
diff --git a/lib/spack/spack/variant.py b/lib/spack/spack/variant.py
--- a/lib/spack/spack/variant.py
+++ b/lib/spack/spack/variant.py
@@ -567,25 +567,24 @@ def __str__(self):
         # print keys in order
         sorted_keys = sorted(self.keys())
 
+        # Separate boolean variants from key-value pairs as they print
+        # differently. All booleans go first to avoid ' ~foo' strings that
+        # break spec reuse in zsh.
+        bool_keys = []
+        kv_keys = []
+        for key in sorted_keys:
+            bool_keys.append(key) if isinstance(self[key].value, bool) \
+                else kv_keys.append(key)
+
         # add spaces before and after key/value variants.
         string = StringIO()
 
-        kv = False
-        for key in sorted_keys:
-            vspec = self[key]
-
-            if not isinstance(vspec.value, bool):
-                # add space before all kv pairs.
-                string.write(' ')
-                kv = True
-            else:
-                # not a kv pair this time
-                if kv:
-                    # if it was LAST time, then pad after.
-                    string.write(' ')
-                kv = False
+        for key in bool_keys:
+            string.write(str(self[key]))
 
-            string.write(str(vspec))
+        for key in kv_keys:
+            string.write(' ')
+            string.write(str(self[key]))
 
         return string.getvalue()
 


Test patch:
diff --git a/lib/spack/spack/test/spec_syntax.py b/lib/spack/spack/test/spec_syntax.py
--- a/lib/spack/spack/test/spec_syntax.py
+++ b/lib/spack/spack/test/spec_syntax.py
@@ -177,7 +177,7 @@ def test_full_specs(self):
             " ^stackwalker@8.1_1e")
         self.check_parse(
             "mvapich_foo"
-            " ^_openmpi@1.2:1.4,1.6%intel@12.1 debug=2 ~qt_4"
+            " ^_openmpi@1.2:1.4,1.6%intel@12.1~qt_4 debug=2"
             " ^stackwalker@8.1_1e")
         self.check_parse(
             'mvapich_foo'
@@ -185,7 +185,7 @@ def test_full_specs(self):
             ' ^stackwalker@8.1_1e')
         self.check_parse(
             "mvapich_foo"
-            " ^_openmpi@1.2:1.4,1.6%intel@12.1 debug=2 ~qt_4"
+            " ^_openmpi@1.2:1.4,1.6%intel@12.1~qt_4 debug=2"
             " ^stackwalker@8.1_1e arch=test-redhat6-x86")
 
     def test_canonicalize(self):
diff --git a/lib/spack/spack/test/variant.py b/lib/spack/spack/test/variant.py
--- a/lib/spack/spack/test/variant.py
+++ b/lib/spack/spack/test/variant.py
@@ -694,7 +694,7 @@ def test_str(self):
         c['foobar'] = SingleValuedVariant('foobar', 'fee')
         c['feebar'] = SingleValuedVariant('feebar', 'foo')
         c['shared'] = BoolValuedVariant('shared', True)
-        assert str(c) == ' feebar=foo foo=bar,baz foobar=fee +shared'
+        assert str(c) == '+shared feebar=foo foo=bar,baz foobar=fee'
 
 
 def test_disjoint_set_initialization_errors():