1# -*- coding: utf-8 -*-
2"""The xonsh shell"""
3import sys
4import random
5import time
6import difflib
7import builtins
8import warnings
9
10from xonsh.platform import (
11    best_shell_type,
12    has_prompt_toolkit,
13    ptk_above_min_supported,
14    ptk_shell_type,
15)
16from xonsh.tools import XonshError, print_exception
17from xonsh.events import events
18import xonsh.history.main as xhm
19
20
21events.doc(
22    "on_transform_command",
23    """
24on_transform_command(cmd: str) -> str
25
26Fired to request xontribs to transform a command line. Return the transformed
27command, or the same command if no transformation occurs. Only done for
28interactive sessions.
29
30This may be fired multiple times per command, with other transformers input or
31output, so design any handlers for this carefully.
32""",
33)
34
35events.doc(
36    "on_precommand",
37    """
38on_precommand(cmd: str) -> None
39
40Fires just before a command is executed.
41""",
42)
43
44events.doc(
45    "on_postcommand",
46    """
47on_postcommand(cmd: str, rtn: int, out: str or None, ts: list) -> None
48
49Fires just after a command is executed. The arguments are the same as history.
50
51Parameters:
52
53* ``cmd``: The command that was executed (after transformation)
54* ``rtn``: The result of the command executed (``0`` for success)
55* ``out``: If xonsh stores command output, this is the output
56* ``ts``: Timestamps, in the order of ``[starting, ending]``
57""",
58)
59
60events.doc(
61    "on_pre_prompt",
62    """
63on_first_prompt() -> None
64
65Fires just before the prompt is shown
66""",
67)
68
69events.doc(
70    "on_post_prompt",
71    """
72on_first_prompt() -> None
73
74Fires just after the prompt returns
75""",
76)
77
78
79def transform_command(src, show_diff=True):
80    """Returns the results of firing the precommand handles."""
81    i = 0
82    limit = sys.getrecursionlimit()
83    lst = ""
84    raw = src
85    while src != lst:
86        lst = src
87        srcs = events.on_transform_command.fire(cmd=src)
88        for s in srcs:
89            if s != lst:
90                src = s
91                break
92        i += 1
93        if i == limit:
94            print_exception(
95                "Modifications to source input took more than "
96                "the recursion limit number of iterations to "
97                "converge."
98            )
99    debug_level = builtins.__xonsh_env__.get("XONSH_DEBUG")
100    if show_diff and debug_level > 1 and src != raw:
101        sys.stderr.writelines(
102            difflib.unified_diff(
103                raw.splitlines(keepends=True),
104                src.splitlines(keepends=True),
105                fromfile="before precommand event",
106                tofile="after precommand event",
107            )
108        )
109    return src
110
111
112class Shell(object):
113    """Main xonsh shell.
114
115    Initializes execution environment and decides if prompt_toolkit or
116    readline version of shell should be used.
117    """
118
119    shell_type_aliases = {
120        "b": "best",
121        "best": "best",
122        "ptk": "prompt_toolkit",
123        "ptk1": "prompt_toolkit1",
124        "ptk2": "prompt_toolkit2",
125        "prompt-toolkit": "prompt_toolkit",
126        "prompt_toolkit": "prompt_toolkit",
127        "prompt-toolkit1": "prompt_toolkit1",
128        "prompt-toolkit2": "prompt_toolkit2",
129        "rand": "random",
130        "random": "random",
131        "rl": "readline",
132        "readline": "readline",
133    }
134
135    def __init__(self, execer, ctx=None, shell_type=None, **kwargs):
136        """
137        Parameters
138        ----------
139        execer : Execer
140            An execer instance capable of running xonsh code.
141        ctx : Mapping, optional
142            The execution context for the shell (e.g. the globals namespace).
143            If none, this is computed by loading the rc files. If not None,
144            this no additional context is computed and this is used
145            directly.
146        shell_type : str, optional
147            The shell type to start, such as 'readline', 'prompt_toolkit1',
148            or 'random'.
149        """
150        self.execer = execer
151        self.ctx = {} if ctx is None else ctx
152        env = builtins.__xonsh_env__
153        # build history backend before creating shell
154        builtins.__xonsh_history__ = hist = xhm.construct_history(
155            env=env.detype(), ts=[time.time(), None], locked=True
156        )
157
158        # pick a valid shell -- if no shell is specified by the user,
159        # shell type is pulled from env
160        if shell_type is None:
161            shell_type = env.get("SHELL_TYPE")
162            if shell_type == "none":
163                # This bricks interactive xonsh
164                # Can happen from the use of .xinitrc, .xsession, etc
165                shell_type = "best"
166        shell_type = self.shell_type_aliases.get(shell_type, shell_type)
167        if shell_type == "best" or shell_type is None:
168            shell_type = best_shell_type()
169        elif shell_type == "random":
170            shell_type = random.choice(("readline", "prompt_toolkit"))
171        if shell_type == "prompt_toolkit":
172            if not has_prompt_toolkit():
173                warnings.warn(
174                    "prompt_toolkit is not available, using " "readline instead."
175                )
176                shell_type = "readline"
177            elif not ptk_above_min_supported():
178                warnings.warn(
179                    "prompt-toolkit version < v1.0.0 is not "
180                    "supported. Please update prompt-toolkit. Using "
181                    "readline instead."
182                )
183                shell_type = "readline"
184            else:
185                shell_type = ptk_shell_type()
186        self.shell_type = env["SHELL_TYPE"] = shell_type
187        # actually make the shell
188        if shell_type == "none":
189            from xonsh.base_shell import BaseShell as shell_class
190        elif shell_type == "prompt_toolkit2":
191            from xonsh.ptk2.shell import PromptToolkit2Shell as shell_class
192        elif shell_type == "prompt_toolkit1":
193            from xonsh.ptk.shell import PromptToolkitShell as shell_class
194        elif shell_type == "readline":
195            from xonsh.readline_shell import ReadlineShell as shell_class
196        elif shell_type == "jupyter":
197            from xonsh.jupyter_shell import JupyterShell as shell_class
198        else:
199            raise XonshError("{} is not recognized as a shell type".format(shell_type))
200        self.shell = shell_class(execer=self.execer, ctx=self.ctx, **kwargs)
201        # allows history garbage collector to start running
202        if hist.gc is not None:
203            hist.gc.wait_for_shell = False
204
205    def __getattr__(self, attr):
206        """Delegates calls to appropriate shell instance."""
207        return getattr(self.shell, attr)
208