Repository: xonsh/xonsh

Issue:
Build out zsh function sourcing & aliasing 
Equivalent to what is there now for Bash.

Static Config: Can't use zsh as foreign shell
```
$ cat ~.config/xonsh/config.json
"foreign_shells": [
        {"shell": "/bin/zsh"}
    ]

$ xonsh
Traceback (most recent call last):
  File "/usr/local/bin/xonsh", line 9, in <module>
    load_entry_point('xonsh==0.2.5', 'console_scripts', 'xonsh')()
  File "/usr/local/lib/python3.5/site-packages/xonsh/main.py", line 166, in main
    args = premain(argv)
  File "/usr/local/lib/python3.5/site-packages/xonsh/main.py", line 153, in premain
    shell = builtins.__xonsh_shell__ = Shell(**shell_kwargs)
  File "/usr/local/lib/python3.5/site-packages/xonsh/shell.py", line 61, in __init__
    self._init_environ(ctx, config, rc)
  File "/usr/local/lib/python3.5/site-packages/xonsh/shell.py", line 99, in _init_environ
    self.execer = Execer(config=config)
  File "/usr/local/lib/python3.5/site-packages/xonsh/execer.py", line 40, in __init__
    load_builtins(execer=self, config=config)
  File "/usr/local/lib/python3.5/site-packages/xonsh/built_ins.py", line 673, in load_builtins
    builtins.__xonsh_env__ = ENV = Env(default_env(config=config))
  File "/usr/local/lib/python3.5/site-packages/xonsh/environ.py", line 1099, in default_env
    issue_warning=False))
  File "/usr/local/lib/python3.5/site-packages/xonsh/foreign_shells.py", line 379, in load_foreign_envs
    shenv, _ = foreign_shell_data(**shell)
  File "/usr/local/lib/python3.5/site-packages/xonsh/foreign_shells.py", line 151, in foreign_shell_data
    funcs = parse_funcs(s, shell=shell, sourcer=sourcer)
  File "/usr/local/lib/python3.5/site-packages/xonsh/foreign_shells.py", line 212, in parse_funcs
    namefiles = json.loads(g1.strip())
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/json/__init__.py", line 319, in loads
    return _default_decoder.decode(s)
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/json/decoder.py", line 339, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/json/decoder.py", line 357, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
```



Discussion:

No issues with bash though.

Hi @moigagoo, I think that this is duplicate of #484. We don't yet have native support for xonsh in zsh.  This is largely because I don't have enough experience with zsh to be confident that it would "just work". If you are willing to help test, I'd happily take a stab at implementing support.

Basically, it is just adding zsh script templates that do the work specified in http://xonsh.org/xonshconfig.html#foreign-shells  In my opinion, the only really tricky part is figuring out how to get the names of zsh functions and the files that they live in.

Again, I'd love to see this feature added!


Resolving patch:
diff --git a/xonsh/aliases.py b/xonsh/aliases.py
--- a/xonsh/aliases.py
+++ b/xonsh/aliases.py
@@ -44,9 +44,9 @@ def _ensure_source_foreign_parser():
     parser.add_argument('-l', '--login', type=to_bool, default=False,
                         help='whether the sourced shell should be login',
                         dest='login')
-    parser.add_argument('--envcmd', default='env', dest='envcmd',
+    parser.add_argument('--envcmd', default=None, dest='envcmd',
                         help='command to print environment')
-    parser.add_argument('--aliascmd', default='alias', dest='aliascmd',
+    parser.add_argument('--aliascmd', default=None, dest='aliascmd',
                         help='command to print aliases')
     parser.add_argument('--extra-args', default=(), dest='extra_args',
                         type=(lambda s: tuple(s.split())),
@@ -78,7 +78,7 @@ def source_foreign(args, stdin=None):
         pass  # don't change prevcmd if given explicitly
     elif os.path.isfile(ns.files_or_code[0]):
         # we have filename to source
-        ns.prevcmd = '{0} {1}'.format(ns.sourcer, ' '.join(ns.files_or_code))
+        ns.prevcmd = '{0} "{1}"'.format(ns.sourcer, '" "'.join(ns.files_or_code))
     elif ns.prevcmd is None:
         ns.prevcmd = ' '.join(ns.files_or_code)  # code to run, no files
     foreign_shell_data.cache_clear()  # make sure that we don't get prev src
@@ -110,6 +110,14 @@ def source_bash(args, stdin=None):
     return source_foreign(args, stdin=stdin)
 
 
+def source_zsh(args, stdin=None):
+    """Simple zsh-specific wrapper around source-foreign."""
+    args = list(args)
+    args.insert(0, 'zsh')
+    args.append('--sourcer=source')
+    return source_foreign(args, stdin=stdin)
+
+
 def source_alias(args, stdin=None):
     """Executes the contents of the provided files in the current context.
     If sourced file isn't found in cwd, search for file along $PATH to source instead"""
@@ -190,6 +198,7 @@ def trace(args, stdin=None):
     'quit': exit,
     'xexec': xexec,
     'source': source_alias,
+    'source-zsh': source_zsh,
     'source-bash': source_bash,
     'source-foreign': source_foreign,
     'history': history_alias,
diff --git a/xonsh/foreign_shells.py b/xonsh/foreign_shells.py
--- a/xonsh/foreign_shells.py
+++ b/xonsh/foreign_shells.py
@@ -59,19 +59,46 @@
 echo $namefile
 """.strip()
 
+DEFAULT_ZSH_FUNCSCMD = """
+namefile="{"
+for name in ${(ok)functions}; do
+  loc=$(whence -v $name)
+  loc=${(z)loc}
+  file=${loc[7,-1]}
+  namefile="${namefile}\\"${name}\\":\\"${(Q)file:A}\\","
+done 
+namefile="${namefile%?}}"
+echo ${namefile}
+""".strip()
+
+DEFAULT_ENVCMDS = {
+    'bash': 'env',
+    '/bin/bash': 'env',
+    'zsh': 'env',
+    '/usr/bin/zsh': 'env',
+}
+DEFAULT_ALIASCMDS = {
+    'bash': 'alias',
+    '/bin/bash': 'alias',
+    'zsh': 'alias -L',
+    '/usr/bin/zsh': 'alias -L',
+}
 DEFAULT_FUNCSCMDS = {
     'bash': DEFAULT_BASH_FUNCSCMD,
     '/bin/bash': DEFAULT_BASH_FUNCSCMD,
+    'zsh': DEFAULT_ZSH_FUNCSCMD,
+    '/usr/bin/zsh': DEFAULT_ZSH_FUNCSCMD,
 }
-
 DEFAULT_SOURCERS = {
     'bash': 'source',
     '/bin/bash': 'source',
+    'zsh': 'source',
+    '/usr/bin/zsh': 'source',
 }
 
 @lru_cache()
-def foreign_shell_data(shell, interactive=True, login=False, envcmd='env',
-                       aliascmd='alias', extra_args=(), currenv=None,
+def foreign_shell_data(shell, interactive=True, login=False, envcmd=None,
+                       aliascmd=None, extra_args=(), currenv=None,
                        safe=True, prevcmd='', postcmd='', funcscmd=None,
                        sourcer=None):
     """Extracts data from a foreign (non-xonsh) shells. Currently this gets
@@ -85,10 +112,10 @@ def foreign_shell_data(shell, interactive=True, login=False, envcmd='env',
         Whether the shell should be run in interactive mode.
     login : bool, optional
         Whether the shell should be a login shell.
-    envcmd : str, optional
+    envcmd : str or None, optional
         The command to generate environment output with.
-    aliascmd : str, optional
-        The command to generate alais output with.
+    aliascmd : str or None, optional
+        The command to generate alias output with.
     extra_args : tuple of str, optional
         Addtional command line options to pass into the shell.
     currenv : tuple of items or None, optional
@@ -129,6 +156,8 @@ def foreign_shell_data(shell, interactive=True, login=False, envcmd='env',
     if login:
         cmd.append('-l')
     cmd.append('-c')
+    envcmd = DEFAULT_ENVCMDS.get(shell, 'env') if envcmd is None else envcmd
+    aliascmd = DEFAULT_ALIASCMDS.get(shell, 'alias') if aliascmd is None else aliascmd
     funcscmd = DEFAULT_FUNCSCMDS.get(shell, '') if funcscmd is None else funcscmd
     command = COMMAND.format(envcmd=envcmd, aliascmd=aliascmd, prevcmd=prevcmd,
                              postcmd=postcmd, funcscmd=funcscmd).strip()
@@ -206,13 +235,25 @@ def parse_funcs(s, shell, sourcer=None):
     if m is None:
         return {}
     g1 = m.group(1)
-    namefiles = json.loads(g1.strip())
+    try:
+        namefiles = json.loads(g1.strip())
+    except json.decoder.JSONDecodeError as exc:
+        msg = ('{0!r}\n\ncould not parse {1} functions:\n'
+               '  s  = {2!r}\n'
+               '  g1 = {3!r}\n\n'
+               'Note: you may be seeing this error if you use zsh with '
+               'prezto. Prezto overwrites GNU coreutils functions (like echo) '
+               'with its own zsh functions. Please try disabling prezto.')
+        warn(msg.format(exc, shell, s, g1), RuntimeWarning)
+        return {}
     sourcer = DEFAULT_SOURCERS.get(shell, 'source') if sourcer is None \
                                                     else sourcer
     funcs = {}
     for funcname, filename in namefiles.items():
         if funcname.startswith('_'):
             continue  # skip private functions
+        if not os.path.isabs(filename):
+            filename = os.path.abspath(filename)
         wrapper = ForeignShellFunctionAlias(name=funcname, shell=shell,
                                             sourcer=sourcer, filename=filename)
         funcs[funcname] = wrapper
@@ -224,7 +265,7 @@ class ForeignShellFunctionAlias(object):
     they were aliases. This does not currently support taking stdin.
     """
 
-    INPUT = ('{sourcer} {filename}\n'
+    INPUT = ('{sourcer} "{filename}"\n'
              '{funcname} {args}\n')
 
     def __init__(self, name, shell, filename, sourcer=None):
@@ -298,9 +339,11 @@ def ensure_shell(shell):
     if 'login' in shell_keys:
         shell['login'] = to_bool(shell['login'])
     if 'envcmd' in shell_keys:
-        shell['envcmd'] = ensure_string(shell['envcmd'])
+        shell['envcmd'] = None if shell['envcmd'] is None \
+                               else ensure_string(shell['envcmd'])
     if 'aliascmd' in shell_keys:
-        shell['aliascmd'] = ensure_string(shell['aliascmd'])
+        shell['aliascmd'] = None if shell['aliascmd'] is None \
+                                 else ensure_string(shell['aliascmd'])
     if 'extra_args' in shell_keys and not isinstance(shell['extra_args'], tuple):
         shell['extra_args'] = tuple(map(ensure_string, shell['extra_args']))
     if 'currenv' in shell_keys and not isinstance(shell['currenv'], tuple):


Test patch: