Загрузка данных


#!/usr/bin/env python3
"""Проверка синтаксической корректности строки вызова утилиты sort (sort_23).
 
Ввод: строки со стандартного ввода. Вывод:
  sort ...          -> True / False
  пустая строка     -> False
  getstatistic      -> "<успешных> <неуспешных> <всего>"
  getsuccesscount   -> число успешных проверок
  getfailedcount    -> число неуспешных проверок
  resetstatistic    -> (нет вывода) сброс статистики
Статистика хранится во временном файле (используется только для неё).
"""
import os
import re
import shlex
import sys
 
# Путь к файлу статистики
STATS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "stats")
 
 
# ----------------------------- статистика ---------------------------------
class Statistics:
    def __init__(self, success_count=0, fail_count=0):
        self.success_count = success_count
        self.fail_count = fail_count
 
    @property
    def total_count(self):
        return self.success_count + self.fail_count
 
 
def read_stats():
    if not os.path.exists(STATS_FILE):
        return Statistics()
    try:
        with open(STATS_FILE, "r", encoding="utf-8") as f:
            line = f.read().strip()
            if line:
                succ, fail, _ = map(int, line.split())
                return Statistics(success_count=succ, fail_count=fail)
    except Exception:
        pass
    return Statistics()
 
 
def write_stats(stats):
    try:
        os.makedirs(os.path.dirname(STATS_FILE), exist_ok=True)
        with open(STATS_FILE, "w", encoding="utf-8") as f:
            f.write(f"{stats.success_count} {stats.fail_count} {stats.total_count}\n")
    except Exception:
        pass
 
 
# ----------------------- описание ключей из ТЗ ----------------------------
# Короткие ключи без аргумента
SHORT_NOARG = set("bdfgiMhnRrVcCmsuz")
# Короткие ключи с обязательным аргументом: -k KEYDEF, -o FILE, -S SIZE,
# -t SEP, -T DIR
SHORT_ARG = set("koSTt")
 
# Длинные ключи без аргумента -> внутренний идентификатор
LONG_NOARG = {
    "ignore-leading-blanks": "b",
    "dictionary-order": "d",
    "ignore-case": "f",
    "general-numeric-sort": "g",
    "ignore-nonprinting": "i",
    "month-sort": "M",
    "human-numeric-sort": "h",
    "numeric-sort": "n",
    "random-sort": "R",
    "reverse": "r",
    "version-sort": "V",
    "merge": "m",
    "stable": "s",
    "unique": "u",
    "zero-terminated": "z",
    "debug": "debug",
    "help": "help",
    "version": "version",
}
 
# Длинные ключи с обязательным аргументом (--name=VALUE или --name VALUE)
LONG_ARG = {
    "random-source": "random-source",
    "sort": "sort",
    "batch-size": "batch-size",
    "compress-program": "compress-program",
    "files0-from": "files0-from",
    "key": "k",
    "output": "o",
    "buffer-size": "S",
    "field-separator": "t",
    "temporary-directory": "T",
    "parallel": "parallel",
}
 
# --check[=diagnose-first|quiet|silent]: -c / -C
CHECK_VALUES = {"diagnose-first": "c", "quiet": "C", "silent": "C"}
 
# --sort=WORD -> эквивалентный короткий ключ
SORT_WORDS = {
    "general-numeric": "g", "human-numeric": "h", "month": "M",
    "numeric": "n", "random": "R", "version": "V",
    "g": "g", "h": "h", "M": "M", "n": "n", "R": "R", "V": "V",
}
 
# Таблица несовместимых ключей из ТЗ (строка -> несовместимые с ней)
CONFLICT_ROWS = {
    "n": ["g", "h", "V", "M", "R"],
    "g": ["n", "h", "V", "M", "R"],
    "h": ["n", "g", "V", "M", "R"],
    "V": ["n", "g", "h", "M", "R"],
    "M": ["n", "g", "h", "V", "R"],
    "R": ["n", "g", "h", "V", "M", "c", "C"],
    "c": ["u", "o", "m", "R", "s", "S", "T", "batch-size",
          "compress-program", "debug", "parallel"],
    "C": ["o", "m", "R", "S", "T", "batch-size", "compress-program", "parallel"],
    "m": ["u", "c", "C"],
    "s": ["u", "c"],
    "u": ["c", "m", "s"],
    "o": ["c", "C"],
    "b": ["d", "i"],
    "d": ["b", "i", "f"],
    "f": ["d", "i"],
    "i": ["b", "d", "f"],
    "z": ["files0-from"],
}
# Несовместимость симметрична: превращаем таблицу в множество пар
CONFLICT_PAIRS = set()
for _a, _row in CONFLICT_ROWS.items():
    for _b in _row:
        CONFLICT_PAIRS.add(frozenset((_a, _b)))
 
# KEYDEF: F[.C][OPTS][,F[.C][OPTS]]; поле >= 1, начальный символ >= 1,
# конечный символ >= 0 (0 = до конца поля); OPTS - модификаторы упорядочения
KEYDEF_RE = re.compile(
    r"^[1-9]\d*(\.[1-9]\d*)?[bdfghiMnRrV]*(,[1-9]\d*(\.\d+)?[bdfghiMnRrV]*)?$"
)
SIZE_RE = re.compile(r"^(\d+)([%bkKMGTPEZYRQ]?)$")
 
 
# --------------------------- проверка значений ----------------------------
def valid_keydef(v):
    return KEYDEF_RE.match(v) is not None
 
 
def valid_separator(v):
    # SEP - одиночный символ (допускается запись \0)
    return len(v) == 1 or v == "\\0"
 
 
def valid_size(v):
    # SIZE - число (допускается суффикс) или проценты
    m = SIZE_RE.match(v)
    if not m:
        return False
    if m.group(2) == "%":
        return 0 <= int(m.group(1)) <= 100
    return True
 
 
def valid_nonempty(v):
    return v != ""
 
 
def valid_number(v):
    return re.match(r"^\d+$", v) is not None
 
 
def valid_positive(v):
    return re.match(r"^[1-9]\d*$", v) is not None
 
 
def valid_sort_word(v):
    return v in SORT_WORDS
 
 
VALIDATORS = {
    "k": valid_keydef,
    "t": valid_separator,
    "S": valid_size,
    "T": valid_nonempty,
    "o": valid_nonempty,
    "random-source": valid_nonempty,
    "compress-program": valid_nonempty,
    "files0-from": valid_nonempty,
    "batch-size": valid_number,
    "parallel": valid_positive,
    "sort": valid_sort_word,
}
 
 
# ------------------------------ разбор строки -----------------------------
def parse_sort_command(command):
    """Возвращает (имя команды, ключи, файлы, успех разбора).
 
    ключи - список пар (идентификатор, значение или None).
    """
    try:
        tokens = shlex.split(command)  # учитывает кавычки: -t ':' , -t ''
    except ValueError:                 # незакрытая кавычка
        return "", [], [], False
    if not tokens:
        return "", [], [], False
 
    cmd_name = tokens[0]
    keys = []
    file_args = []
    end_of_options = False
 
    i = 1
    while i < len(tokens):
        token = tokens[i]
 
        if end_of_options or token == "-" or not token.startswith("-"):
            file_args.append(token)
 
        elif token == "--":
            end_of_options = True
 
        elif token.startswith("--") or token.startswith("-compress-program="):
            name, has_eq, value = token.lstrip("-").partition("=")
            if name == "check":
                if not has_eq:
                    keys.append(("c", None))
                elif value in CHECK_VALUES:
                    keys.append((CHECK_VALUES[value], None))
                else:
                    return cmd_name, keys, file_args, False
            elif name in LONG_NOARG:
                if has_eq:
                    return cmd_name, keys, file_args, False
                keys.append((LONG_NOARG[name], None))
            elif name in LONG_ARG:
                if not has_eq:
                    i += 1
                    if i >= len(tokens):
                        return cmd_name, keys, file_args, False
                    value = tokens[i]
                keys.append((LONG_ARG[name], value))
            else:
                return cmd_name, keys, file_args, False
 
        else:  # короткие ключи, возможно слитно: -nr, -k2,2, -t:
            j = 1
            while j < len(token):
                ch = token[j]
                if ch in SHORT_NOARG:
                    keys.append((ch, None))
                    j += 1
                elif ch in SHORT_ARG:
                    value = token[j + 1:]
                    if value == "":
                        i += 1
                        if i >= len(tokens):
                            return cmd_name, keys, file_args, False
                        value = tokens[i]
                    keys.append((ch, value))
                    break
                else:
                    return cmd_name, keys, file_args, False
        i += 1
 
    return cmd_name, keys, file_args, True
 
 
# ------------------------------ проверки ----------------------------------
def check_command_name(cmd_name):
    return cmd_name == "sort"
 
 
def check_keys(keys):
    # формат аргументов ключей
    for key, value in keys:
        validator = VALIDATORS.get(key)
        if validator is not None and not validator(value):
            return False
 
    # конфликты ключей
    active = set()
    for key, value in keys:
        active.add(SORT_WORDS[value] if key == "sort" else key)
    for pair in CONFLICT_PAIRS:
        if pair <= active:
            return False
    return True
 
 
def check_file_arguments(file_args):
    # отсутствие файлов допускается (чтение из stdin)
    return True
 
 
def check_command_logic(command):
    cmd_name, keys, file_args, ok = parse_sort_command(command)
    if not ok:
        return False
    return (check_command_name(cmd_name)
            and check_keys(keys)
            and check_file_arguments(file_args))
 
 
# ----------------------------- ввод / вывод -------------------------------
def command_input(line):
    stripped = line.strip()
 
    if stripped == "resetstatistic":
        write_stats(Statistics())
        return ""
    elif stripped == "getsuccesscount":
        return str(read_stats().success_count)
    elif stripped == "getfailedcount":
        return str(read_stats().fail_count)
    elif stripped == "getstatistic":
        st = read_stats()
        return f"{st.success_count} {st.fail_count} {st.total_count}"
 
    # пустая строка - ошибочная проверка (False)
    is_valid = bool(stripped) and check_command_logic(stripped)
 
    stats = read_stats()
    if is_valid:
        stats.success_count += 1
    else:
        stats.fail_count += 1
    write_stats(stats)
    return "True" if is_valid else "False"
 
 
def main():
    if sys.stdin.isatty():
        while True:
            try:
                user_input = sys.stdin.readline()
                if not user_input:
                    break
                cleaned_input = user_input.strip()
                if cleaned_input.lower() == "exit":
                    break
                output = command_input(cleaned_input)
                if output:
                    print(output)
            except (KeyboardInterrupt, EOFError):
                break
    else:
        # Читаем все строки сразу, чтобы корректно обработать полностью пустой stdin
        lines = sys.stdin.readlines()
        if not lines:
            # пустой ввод - ошибочная проверка
            output = command_input("")
            if output:
                print(output)
        else:
            for raw_line in lines:
                output = command_input(raw_line.strip())
                if output:
                    print(output)
 
 
if __name__ == "__main__":
    main()