1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4import click
5import sys
6import os
7import os.path as osp
8import subprocess
9from pathlib import Path
10
11from mathicsscript.termshell import ShellEscapeException, mma_lexer
12
13from mathicsscript.termshell_gnu import TerminalShellGNUReadline
14from mathicsscript.termshell_prompt import TerminalShellPromptToolKit
15
16try:
17    __import__("readline")
18except ImportError:
19    have_readline = False
20    readline_choices = ["Prompt", "None"]
21else:
22    readline_choices = ["GNU", "Prompt", "None"]
23    have_readline = True
24
25from mathicsscript.format import format_output
26
27from mathics_scanner import replace_wl_with_plain_text
28from mathics.core.parser import MathicsFileLineFeeder
29from mathics.core.definitions import autoload_files, Definitions
30from mathics.core.expression import Symbol, SymbolTrue, SymbolFalse
31from mathics.core.evaluation import Evaluation, Output
32from mathics.core.expression import from_python
33from mathics import version_string, license_string
34from mathics import settings
35
36from pygments import highlight
37
38
39def get_srcdir():
40    filename = osp.normcase(osp.dirname(osp.abspath(__file__)))
41    return osp.realpath(filename)
42
43
44from mathicsscript.version import __version__
45
46
47def ensure_settings():
48    home = Path.home()
49    base_config_dir = home / ".config"
50    if not base_config_dir.is_dir():
51        os.mkdir(str(base_config_dir))
52    config_dir = base_config_dir / "mathicsscript"
53    if not config_dir.is_dir():
54        os.mkdir(str(config_dir))
55
56    settings_file = config_dir / "settings.m"
57    if not settings_file.is_file():
58        import mathicsscript
59
60        srcfn = Path(mathicsscript.__file__).parent / "user-settings.m"
61        try:
62            with open(srcfn, "r") as src:
63                buffer = src.readlines()
64        except:
65            print(f"'{srcfn}' was not found.")
66            return ""
67        try:
68            with open(settings_file, "w") as dst:
69                for l in buffer:
70                    dst.write(l)
71        except:
72            print(f" '{settings_file}'  cannot be written.")
73            return ""
74    return settings_file
75
76
77def load_settings(shell):
78    autoload_files(shell.definitions, get_srcdir(), "autoload")
79    settings_file = ensure_settings()
80    if settings_file == "":
81        return
82    with open(settings_file, "r") as src:
83        feeder = MathicsFileLineFeeder(src)
84        try:
85            while not feeder.empty():
86                evaluation = Evaluation(
87                    shell.definitions,
88                    output=TerminalOutput(shell),
89                    catch_interrupt=False,
90                    format="text",
91                )
92                query = evaluation.parse_feeder(feeder)
93                if query is None:
94                    continue
95                evaluation.evaluate(query)
96        except (KeyboardInterrupt):
97            print("\nKeyboardInterrupt")
98    return True
99
100
101Evaluation.format_output = format_output
102
103
104class TerminalOutput(Output):
105    def max_stored_size(self, settings):
106        return None
107
108    def __init__(self, shell):
109        self.shell = shell
110
111    def out(self, out):
112        return self.shell.out_callback(out)
113
114
115@click.command()
116@click.version_option(version=__version__)
117@click.option(
118    "--full-form",
119    "-f",
120    "full_form",
121    flag_value="full_form",
122    default=False,
123    required=False,
124    help="Show how input was parsed to FullForm",
125)
126@click.option(
127    "--persist",
128    default=False,
129    required=False,
130    is_flag=True,
131    help="go to interactive shell after evaluating FILE or -e",
132)
133@click.option(
134    "--quiet",
135    "-q",
136    default=False,
137    is_flag=True,
138    required=False,
139    help="don't print message at startup",
140)
141@click.option(
142    "--readline",
143    type=click.Choice(readline_choices, case_sensitive=False),
144    default="Prompt",
145    show_default=True,
146    help="""Readline method. "Prompt" is usually best. None is generally available and have the fewest features.""",
147)
148@click.option(
149    "--completion/--no-completion",
150    default=True,
151    show_default=True,
152    help=(
153        "GNU Readline line editing. enable tab completion; "
154        "you need a working GNU Readline for this option."
155    ),
156)
157@click.option(
158    "--unicode/--no-unicode",
159    default=sys.getdefaultencoding() == "utf-8",
160    show_default=True,
161    help="Accept Unicode operators in input and show unicode in output.",
162)
163@click.option(
164    "--prompt/--no-prompt",
165    default=True,
166    show_default=True,
167    help="Do not prompt In[] or Out[].",
168)
169@click.option(
170    "--pyextensions",
171    "-l",
172    required=False,
173    multiple=True,
174    help="directory to load extensions in Python",
175)
176@click.option(
177    "-c",
178    "-e",
179    "--execute",
180    help="evaluate EXPR before processing any input files (may be given "
181    "multiple times). Sets --quiet and --no-completion",
182    multiple=True,
183    required=False,
184)
185@click.option(
186    "--run",
187    type=click.Path(readable=True),
188    help=(
189        "go to interactive shell after evaluating PATH but leave "
190        "history empty and set $Line to 1"
191    ),
192)
193@click.option(
194    "-s",
195    "--style",
196    metavar="PYGMENTS-STYLE",
197    type=str,
198    help=("Set pygments style. Use 'None' if you do not want any pygments styling."),
199    required=False,
200)
201@click.option(
202    "--pygments-tokens/--no-pygments-tokens",
203    default=False,
204    help=("Show pygments tokenization of output."),
205    required=False,
206)
207@click.option(
208    "--strict-wl-output/--no-strict-wl-output",
209    default=False,
210    help=("Most WL-output compatible (at the expense of useability)."),
211    required=False,
212)
213@click.argument("file", nargs=1, type=click.Path(readable=True), required=False)
214def main(
215    full_form,
216    persist,
217    quiet,
218    readline,
219    completion,
220    unicode,
221    prompt,
222    pyextensions,
223    execute,
224    run,
225    style,
226    pygments_tokens,
227    strict_wl_output,
228    file,
229) -> int:
230    """A command-line interface to Mathics.
231
232    Mathics is a general-purpose computer algebra system
233    """
234
235    exit_rc = 0
236    quit_command = "CTRL-BREAK" if sys.platform == "win32" else "CONTROL-D"
237
238    extension_modules = []
239    if pyextensions:
240        for ext in pyextensions:
241            extension_modules.append(ext)
242
243    definitions = Definitions(add_builtin=True)
244    definitions.set_line_no(0)
245    # Set a default value for $ShowFullFormInput to False.
246    # Then, it can be changed by the settings file (in WL)
247    # and overwritten by the command line parameter.
248    definitions.set_ownvalue(
249        "Settings`$ShowFullFormInput", from_python(True if full_form else False)
250    )
251    definitions.set_ownvalue(
252        "Settings`$PygmentsShowTokens", from_python(True if pygments_tokens else False)
253    )
254
255    readline = "none" if (execute or file and not persist) else readline.lower()
256    if readline == "prompt":
257        shell = TerminalShellPromptToolKit(
258            definitions, style, completion, unicode, prompt
259        )
260    else:
261        want_readline = readline == "gnu"
262        shell = TerminalShellGNUReadline(
263            definitions, style, want_readline, completion, unicode, prompt
264        )
265
266    load_settings(shell)
267    if run:
268        with open(run, "r") as ifile:
269            feeder = MathicsFileLineFeeder(ifile)
270            try:
271                while not feeder.empty():
272                    evaluation = Evaluation(
273                        shell.definitions,
274                        output=TerminalOutput(shell),
275                        catch_interrupt=False,
276                        format="text",
277                    )
278                    query = evaluation.parse_feeder(feeder)
279                    if query is None:
280                        continue
281                    evaluation.evaluate(query, timeout=settings.TIMEOUT)
282            except (KeyboardInterrupt):
283                print("\nKeyboardInterrupt")
284
285        definitions.set_line_no(0)
286
287    if execute:
288        for expr in execute:
289            evaluation = Evaluation(
290                shell.definitions, output=TerminalOutput(shell), format="text"
291            )
292            shell.terminal_formatter = None
293            result = evaluation.parse_evaluate(expr, timeout=settings.TIMEOUT)
294            shell.print_result(result, prompt, "text", strict_wl_output)
295
296            # After the next release, we can remove the hasattr test.
297            if hasattr(evaluation, "exc_result"):
298                if evaluation.exc_result == Symbol("Null"):
299                    exit_rc = 0
300                elif evaluation.exc_result == Symbol("$Aborted"):
301                    exit_rc = -1
302                elif evaluation.exc_result == Symbol("Overflow"):
303                    exit_rc = -2
304                else:
305                    exit_rc = -3
306
307        if not persist:
308            return exit_rc
309
310    if file is not None:
311        with open(file, "r") as ifile:
312            feeder = MathicsFileLineFeeder(ifile)
313            try:
314                while not feeder.empty():
315                    evaluation = Evaluation(
316                        shell.definitions,
317                        output=TerminalOutput(shell),
318                        catch_interrupt=False,
319                        format="text",
320                    )
321                    query = evaluation.parse_feeder(feeder)
322                    if query is None:
323                        continue
324                    evaluation.evaluate(query, timeout=settings.TIMEOUT)
325            except (KeyboardInterrupt):
326                print("\nKeyboardInterrupt")
327
328        if not persist:
329            return exit_rc
330
331    if not quiet and prompt:
332        print(f"\nMathicscript: {__version__}, {version_string}\n")
333        print(license_string + "\n")
334        print(f"Quit by evaluating Quit[] or by pressing {quit_command}.\n")
335    # If defined, full_form and style overwrite the predefined values.
336    definitions.set_ownvalue(
337        "Settings`$ShowFullFormInput", SymbolTrue if full_form else SymbolFalse
338    )
339
340    definitions.set_ownvalue(
341        "Settings`$PygmentsStyle", from_python(shell.pygments_style)
342    )
343    definitions.set_ownvalue(
344        "Settings`$PygmentsShowTokens", from_python(pygments_tokens)
345    )
346    definitions.set_ownvalue("Settings`MathicsScriptVersion", from_python(__version__))
347    definitions.set_attribute("Settings`MathicsScriptVersion", "System`Protected")
348    definitions.set_attribute("Settings`MathicsScriptVersion", "System`Locked")
349    TeXForm = Symbol("System`TeXForm")
350
351    definitions.set_line_no(0)
352    while True:
353        try:
354            if have_readline and shell.using_readline:
355                import readline as GNU_readline
356
357                last_pos = GNU_readline.get_current_history_length()
358
359            full_form = definitions.get_ownvalue(
360                "Settings`$ShowFullFormInput"
361            ).replace.to_python()
362            style = definitions.get_ownvalue("Settings`$PygmentsStyle")
363            fmt = lambda x: x
364            if style:
365                style = style.replace.get_string_value()
366                if shell.terminal_formatter:
367                    fmt = lambda x: highlight(
368                        str(query), mma_lexer, shell.terminal_formatter
369                    )
370
371            evaluation = Evaluation(shell.definitions, output=TerminalOutput(shell))
372            query, source_code = evaluation.parse_feeder_returning_code(shell)
373
374            if (
375                have_readline
376                and shell.using_readline
377                and hasattr(GNU_readline, "remove_history_item")
378            ):
379                current_pos = GNU_readline.get_current_history_length()
380                for pos in range(last_pos, current_pos - 1):
381                    GNU_readline.remove_history_item(pos)
382                wl_input = source_code.rstrip()
383                if unicode:
384                    wl_input = replace_wl_with_plain_text(wl_input)
385                GNU_readline.add_history(wl_input)
386
387            if query is None:
388                continue
389
390            if hasattr(query, "head") and query.head == TeXForm:
391                output_style = "//TeXForm"
392            else:
393                output_style = ""
394
395            if full_form:
396                print(fmt(query))
397            result = evaluation.evaluate(
398                query, timeout=settings.TIMEOUT, format="unformatted"
399            )
400            if result is not None:
401                shell.print_result(
402                    result, prompt, output_style, strict_wl_output=strict_wl_output
403                )
404
405        except ShellEscapeException as e:
406            source_code = e.line
407            if len(source_code) and source_code[1] == "!":
408                try:
409                    print(open(source_code[2:], "r").read())
410                except:
411                    print(str(sys.exc_info()[1]))
412            else:
413                subprocess.run(source_code[1:], shell=True)
414
415                # Should we test exit code for adding to history?
416                GNU_readline.add_history(source_code.rstrip())
417                ## FIXME add this... when in Mathics core updated
418                ## shell.defintions.increment_line(1)
419
420        except (KeyboardInterrupt):
421            print("\nKeyboardInterrupt")
422        except EOFError:
423            if prompt:
424                print("\n\nGoodbye!\n")
425            break
426        except SystemExit:
427            print("\n\nGoodbye!\n")
428            # raise to pass the error code on, e.g. Quit[1]
429            raise
430        finally:
431            # Reset the input line that would be shown in a parse error.
432            # This is not to be confused with the number of complete
433            # inputs that have been seen, i.e. In[]
434            shell.reset_lineno()
435    return exit_rc
436
437
438if __name__ == "__main__":
439    sys.exit(main())
440