Repository: xonsh/xonsh

Issue:
xonsh wizard writes .xonshrc causing error: "xonsh: subprocess mode: command not found: $"
When configuring xonsh's history retention through the Xonsh wizard, if the value `1 GB` is specified, an invalid `.xonshrc` configuration file is written.

Environment:

* xonsh/0.6.4
* macOS 10.13.4 (17E199)
* Python 3.6.5

Steps to reproduce:

1) From a clean environment (no `.xonshrc`, etc), run `xonsh`
2) Run `xonfig wizard`.
3) When prompted: `Would you like to set env vars now, yes or no [default: no]?`, answer `yes`.
4) Accept all defaults, up to, but not including, that for `$XONSH_HISTORY_SIZE`.
5) When prompted for a value for `$XONSH_HISTORY_SIZE`, answer `1 GB`.
A message is printed: `Converted input '1 GB' to (1073741824, 'b').`
6) Accept all remaining defaults.
7) When prompted: `Would you like to write out the current state, yes or no [default: no]?`, answer `yes`.
8) Accept the default location for `.xonshrc`
The following is then written to `.xonshrc`:
```
# XONSH WIZARD START
$ = "(1073741824, 'b')"
$0 = '1073741824'
$1 = 'b'
# XONSH WIZARD END
```
9) Type `exit`.
10) Run `xonsh`.

Expected result:
The xonsh shell starts without error.

Actual result:
The xonsh shell starts, showing the following error:
```
xonsh: For full traceback set: $XONSH_SHOW_TRACEBACK = True
xonsh: subprocess mode: command not found: $
Did you mean one of the following?
    w:   Command (/usr/bin/w)
    [:   Command (/bin/[)
    X:   Command (/opt/X11/bin/X)
    cd:  Alias
    fg:  Alias
xonsh: For full traceback set: $XONSH_SHOW_TRACEBACK = True
xonsh: subprocess mode: command not found: Unexpected token: TokenInfo(type=53 (OP), string='$0', start=(3, 2), end=(3, 4), line="![$0 = '1073741824']\n")
xonsh: For full traceback set: $XONSH_SHOW_TRACEBACK = True
xonsh: subprocess mode: command not found: Unexpected token: TokenInfo(type=53 (OP), string='$1', start=(4, 2), end=(4, 4), line="![$1 = 'b']\n")
```




Discussion:
Thanks for reporting @rhencke!

Resolving patch:
diff --git a/xonsh/wizard.py b/xonsh/wizard.py
--- a/xonsh/wizard.py
+++ b/xonsh/wizard.py
@@ -334,6 +334,11 @@ def dump_rules(self, value):
             dr[r] = func
         self._dr = dr
 
+    @staticmethod
+    def _find_rule_key(x):
+        """Key function for sorting regular expression rules"""
+        return (x[0], len(x[1].pattern))
+
     def find_rule(self, path):
         """For a path, find the key and conversion function that should be used to
         dump a value.
@@ -350,10 +355,25 @@ def find_rule(self, path):
         if len(len_funcs) == 0:
             # No dump rule function for path
             return path, None
-        len_funcs.sort(reverse=True)
+        len_funcs.sort(reverse=True, key=self._find_rule_key)
         _, rule, func = len_funcs[0]
         return rule, func
 
+    def dumps(self, flat):
+        """Dumps a flat mapping of (string path keys, values) pairs and returns
+        a formatted string.
+        """
+        lines = [self.prefix]
+        for path, value in sorted(flat.items()):
+            rule, func = self.find_rule(path)
+            if func is None:
+                continue
+            line = func(path, value)
+            lines.append(line)
+        lines.append(self.suffix)
+        new = '\n'.join(lines) + '\n'
+        return new
+
 
 def create_truefalse_cond(prompt='yes or no [default: no]? ', path=None):
     """This creates a basic condition function for use with nodes like While
@@ -771,16 +791,7 @@ def visit_loadjson(self, node):
 
     def visit_fileinserter(self, node):
         # perform the dumping operation.
-        lines = [node.prefix]
-        flat = self.flatten()
-        for path, value in sorted(flat.items()):
-            rule, func = node.find_rule(path)
-            if func is None:
-                continue
-            line = func(path, value)
-            lines.append(line)
-        lines.append(node.suffix)
-        new = '\n'.join(lines) + '\n'
+        new = node.dumps(self.flatten())
         # check if we should write this out
         if node.check:
             msg = 'The current state to insert is:\n\n{0}\n'
diff --git a/xonsh/xonfig.py b/xonsh/xonfig.py
--- a/xonsh/xonfig.py
+++ b/xonsh/xonfig.py
@@ -161,7 +161,7 @@ def _dump_xonfig_foreign_shell(path, value):
 
 
 def _dump_xonfig_env(path, value):
-    name = os.path.basename(path)
+    name = os.path.basename(path.rstrip('/'))
     ensurer = builtins.__xonsh_env__.get_ensurer(name)
     dval = ensurer.detype(value)
     return '${name} = {val!r}'.format(name=name, val=dval)
@@ -177,6 +177,7 @@ def XONFIG_DUMP_RULES():
             '/env/': None,
             '/foreign_shells/*/': _dump_xonfig_foreign_shell,
             '/env/*': _dump_xonfig_env,
+            '/env/*/[0-9]*': None,
             '/xontribs/': _dump_xonfig_xontribs,
             }
 


Test patch:
diff --git a/tests/test_wizard.py b/tests/test_wizard.py
--- a/tests/test_wizard.py
+++ b/tests/test_wizard.py
@@ -6,7 +6,7 @@
 import pytest
 
 from xonsh.wizard import (Node, Wizard, Pass, PrettyFormatter,
-    Message, Question, StateVisitor)
+    Message, Question, StateVisitor, FileInserter)
 
 
 TREE0 = Wizard(children=[Pass(), Message(message='yo')])
@@ -57,3 +57,26 @@ def test_state_visitor_store():
                 }
     flat_obs = sv.flatten()
     assert flat_exp == flat_obs
+
+
+def dump_xonfig_env_mock(path, value):
+    name = os.path.basename(path.rstrip('/'))
+    return '${name} = {val!r}'.format(name=name, val=value)
+
+
+def test_tuple_store_and_write():
+    # setup
+    sv = StateVisitor()
+    sv.store('/env/XONSH_HISTORY_SIZE', (1073741824, 'b'))
+    dump_rules = {'/': None, '/env/': None,
+                  '/env/*': dump_xonfig_env_mock,
+                  '/env/*/[0-9]*': None}
+    fi = FileInserter(prefix='# XONSH WIZARD START', suffix='# XONSH WIZARD END',
+                      dump_rules=dump_rules, default_file=None,
+                      check=False, ask_filename=False)
+    # run test
+    exp = ("# XONSH WIZARD START\n"
+           "$XONSH_HISTORY_SIZE = (1073741824, 'b')\n"
+           "# XONSH WIZARD END\n")
+    obs = fi.dumps(sv.flatten())
+    assert exp == obs