Files

171 lines
5.6 KiB
Python

#!/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())