#!/usr/bin/env python3 """Точка входа: интерактивный CLI-калькулятор. Читает выражения до ключевого слова «выход» / "exit". Можно запускать: python cli.py # интерактивный режим (TTY) или pipe python cli.py "2 + 2" "3 * (4 - 1)" # аргументы — как входные строки """ from __future__ import annotations import sys from calculator import is_exit_command, parse_and_eval, try_evaluate HELP = """ Калькулятор. Команды и возможности: Выражения: 2 + 3 * (4 - 1) % 100 Операторы: + - * / // % ** () Функции: sqrt abs exp ln log log2 log10 round floor ceil sin cos tan asin acos atan (тригонометрия — радианы) Константы: pi e tau inf Числа: 1e3 2.5E-4 1_000_000 ans (последний результат) Команды: help - показать эту подсказку vars - показать переменные (ans) hist - историю вычислений reset - очистить переменные и историю выход/exit - завершить """ _STATEMENT_HELP = "Помощь:" _HISTORY_FILE = "~/.calc_history" _HEADER = ("Калькулятор. Введите выражение, 'выход'/'exit' — для завершения. " "Подсказка: 'help'.") class Session: """Состояние сессии: переменные (env), история вычислений.""" def __init__(self) -> None: self.env: dict[str, float] = {"ans": 0.0} self.history: list[tuple[str, float]] = [] def process(self, line: str) -> list[str] | None: """Обрабатывает одну строку ввода. Возвращает список строк для вывода, или None — это команда выхода. """ text = line.strip() if not text: return [] cmd = text.lower() if is_exit_command(cmd): return None if cmd == "help": return [HELP] if cmd == "reset": self.env = {"ans": 0.0} self.history.clear() return ["Сессия сброшена (ans = 0, история пуста)."] if cmd == "vars": if not self.env: return ["Переменных нет."] lines = [f"{k} = {try_evaluate(str(v))}" for k, v in self.env.items()] return [f"Переменные: {', '.join(lines)}"] if cmd == "hist": if not self.history: return ["История пуста."] lines = [f"{i + 1}. {expr} = {try_evaluate(str(result))}" for i, (expr, result) in enumerate(self.history)] return [f"История ({len(lines)}):"] + lines try: result = parse_and_eval(text, self.env) except Exception as exc: # CalcError / SyntaxError / … return [f"Ошибка: {exc}"] shown = try_evaluate(text, self.env) self.env["ans"] = result self.history.append((text, result)) return [shown] def _setup_readline() -> None: """Включаем стрелки↑/↓ и сохранение истории в ~/.calc_history (без падений).""" try: import atexit import readline except ImportError: return try: readline.read_history_file(_HISTORY_FILE) except (FileNotFoundError, OSError): pass readline.set_history_length(500) atexit.register(_save_history, readline) def _save_history(readline_module) -> None: try: readline_module.write_history_file(_HISTORY_FILE) except OSError: pass def repl(exprs: list[str] | None) -> int: """Запускает цикл ввода/вычисления.""" if exprs: # режим «пакетного» запуска (аргументы строки выражений) session = Session() for expr in exprs: if is_exit_command(expr): break out = session.process(expr) if out is not None: print(out[-1]) return 0 if sys.stdin.isatty(): # настоящий интерактивный режим: читаем построчно с приглашением _setup_readline() session = Session() print(_HEADER) while True: try: line = input("calc> ") except (EOFError, KeyboardInterrupt): print() break out = session.process(line) if out is None: print("Пока!") break for row in out: print(row) return 0 # pipe / файл: детерминированно печатаем результаты, без заголовка session = Session() data = sys.stdin.read() for line in data.splitlines(): expr = line.strip() if not expr: continue out = session.process(expr) if out is None: break for row in out: print(row) return 0 def _has_readline() -> bool: import importlib.util return importlib.util.find_spec("readline") is not None def main(argv: list[str] | None = None) -> int: argv = list(sys.argv[1:] if argv is None else argv) return repl(argv) if __name__ == "__main__": raise SystemExit(main())