171 lines
5.6 KiB
Python
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())
|