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