Практика · Тема 24 · Скрипти командного рядка> Лекція: lecture.html · Тест: quiz.html · Домашнє завдання: homework.mdДосі наш код жив у зошиті. Зараз ми напишемо справжній скрипт — файл, якийзапускають із термінала з аргументами, — і перевіримо його так, як його перевіряв битой, хто збирається запускати його автоматично.Що зробимо:1. створимо тимчасову майстерню й покладемо туди CSV із продажами;2. напишемо zvit.py із argparse: позиційний аргумент, необовʼязкові, прапорець;3. запустимо його як окремий процес через subprocess і перевіримо результат;4. подивимось довідку -h, якої ніхто не писав;5. переконаємось assert-ами, що коди виходу чесні: 0 при успіху, 2 на поганих аргументах, 1 на помилці виконання;6. розділимо потоки: перевіримо, що результат пішов у stdout, а повідомлення про помилку — у stderr, і що вони не змішуються;7. викличемо main() напряму, без окремого процесу, — так тестують скрипти насправді;8. приберемо за собою.Мережа не потрібна, сторонніх бібліотек теж: усе робить стандартна бібліотека.¶

1 · Тимчасова майстерняСкрипт має жити у файлі, а файл — у теці. Щоб не смітити в проєкті, зробимо тимчасовутеку: наприкінці ми її видалимо цілком, і від практики не лишиться жодного сліду.¶

In [1]:
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

# окрема тека для всіх файлів цієї практики — її ми видалимо в самому кінці
majsternya = Path(tempfile.mkdtemp(prefix="tema24_"))

print("майстерня:", majsternya)
print("тека існує:", majsternya.is_dir())
print("інтерпретатор, яким запускатимемо скрипт:", sys.executable)
майстерня: /tmp/tema24_0iojo3q3
тека існує: True
інтерпретатор, яким запускатимемо скрипт: /usr/bin/python3

2 · Дані, які скрипт читатимеНаш zvit.py рахуватиме підсумок по колонці CSV-файлу — той самий наскрізний приклад,що й у лекції. Спершу створимо самі дані: невелику таблицю продажів із колонкамиdata, tovar, kilkist, suma.Числа беремо маленькі й «круглі» — щоб потім можна було перевірити результат скриптаусно, а не вірити йому на слово.¶

In [2]:
prodazhi_tekst = """data,tovar,kilkist,suma
2026-07-01,кава,2,180.00
2026-07-01,чай,1,90.00
2026-07-02,кава,3,270.00
2026-07-03,тістечко,4,160.00
2026-07-03,кава,1,90.00
"""

fajl_danykh = majsternya / "prodazhi.csv"
fajl_danykh.write_text(prodazhi_tekst, encoding="utf-8")

# рахуємо очікувані числа тут, у зошиті, щоб було з чим порівнювати скрипт
ochikuvana_suma = 180.00 + 90.00 + 270.00 + 160.00 + 90.00
ochikuvana_kilkist = 2 + 1 + 3 + 4 + 1

print("створено:", fajl_danykh.name)
print("рядків із даними:", len(prodazhi_tekst.strip().split("\n")) - 1)
print("очікувана сума по колонці suma   :", ochikuvana_suma)
print("очікувана сума по колонці kilkist:", ochikuvana_kilkist)
створено: prodazhi.csv
рядків із даними: 5
очікувана сума по колонці suma   : 790.0
очікувана сума по колонці kilkist: 11

3 · Пишемо скриптТепер сам zvit.py. Це той самий код, що й у розділі «Структура зрілого скрипта»в лекції — з трьома важливими рішеннями:- уся робота в функції main(), а на верхньому рівні файлу лише означення;- main() повертає код виходу, а не викликає sys.exit усередині — так її можна викликати з тесту й просто подивитись на число;- sys.exit(main()) під захисною умовою if __name__ == "__main__": — саме тут число нарешті потрапляє в операційну систему.Зверни увагу, куди що друкується: результат — у stdout, усе службове — у stderr.¶

In [3]:
kod_skrypta = '''\
#!/usr/bin/env python3
"""Підсумок по числовій колонці CSV-файлу з продажами."""
import argparse
import csv
import sys

DIYI = {"suma": "сума", "serednye": "середнє", "maksymum": "максимум"}


def zrobyty_rozbyrach():
    """Опис аргументів окремо від роботи — так розбирач можна перевірити в тесті."""
    parser = argparse.ArgumentParser(
        prog="zvit.py",
        description="Рахує підсумок по числовій колонці CSV-файлу з продажами.")
    parser.add_argument("fajl", help="шлях до CSV-файлу")
    parser.add_argument("-k", "--kolonka", default="suma", metavar="НАЗВА",
                        help="назва числової колонки (типово: %(default)s)")
    parser.add_argument("-d", "--dia", default="suma",
                        choices=["suma", "serednye", "maksymum"],
                        help="що порахувати (типово: %(default)s)")
    parser.add_argument("-t", "--tochnist", type=int, default=2, metavar="N",
                        help="знаків після коми (типово: %(default)s)")
    parser.add_argument("--tyho", action="store_true",
                        help="друкувати лише число, без заголовка")
    return parser


def main(argv=None):
    """Повертає код виходу: 0 — успіх, 1 — не змогли порахувати."""
    args = zrobyty_rozbyrach().parse_args(argv)

    try:
        with open(args.fajl, encoding="utf-8", newline="") as f:
            ryadky = list(csv.DictReader(f))
    except OSError as pomylka:
        # повідомлення про помилку — у stderr, щоб не змішатися з результатом
        print(f"zvit.py: {pomylka}", file=sys.stderr)
        return 1

    if not ryadky:
        print("zvit.py: у файлі немає жодного рядка з даними", file=sys.stderr)
        return 1
    if args.kolonka not in ryadky[0]:
        nayavni = ", ".join(ryadky[0].keys())
        print(f"zvit.py: немає колонки {args.kolonka!r}; є: {nayavni}", file=sys.stderr)
        return 1

    try:
        chysla = [float(r[args.kolonka]) for r in ryadky]
    except ValueError:
        print(f"zvit.py: колонка {args.kolonka!r} не числова", file=sys.stderr)
        return 1

    if args.dia == "suma":
        rezultat = sum(chysla)
    elif args.dia == "serednye":
        rezultat = sum(chysla) / len(chysla)
    else:
        rezultat = max(chysla)

    # службовий рядок — теж у stderr: це звіт про перебіг, а не результат
    print(f"zvit.py: оброблено {len(chysla)} рядків", file=sys.stderr)

    # і тільки це — результат, тільки він іде в stdout
    tekst = f"{rezultat:.{args.tochnist}f}"
    if args.tyho:
        print(tekst)
    else:
        print(f"{DIYI[args.dia]} по колонці {args.kolonka}: {tekst}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
'''

fajl_skrypta = majsternya / "zvit.py"
fajl_skrypta.write_text(kod_skrypta, encoding="utf-8")

print("створено:", fajl_skrypta.name)
print("рядків у скрипті:", len(kod_skrypta.strip().split("\n")))
створено: zvit.py
рядків у скрипті: 74

4 · Запускаємо скрипт як окремий процесsubprocess.run запускає програму так само, як це зробив би термінал, і повертаєобʼєкт із трьома цікавими полями: returncode (код виходу), stdout і stderr.Щоб не залежати від того, який Python стоїть у системі, запускаємо той самийінтерпретатор, у якому працює зошит, — sys.executable. Це та сама ідея, що йpython3 -m pip замість просто pip із теми 02.Зробимо маленьку функцію-помічницю: далі ми запускатимемо скрипт багато разів.¶

In [4]:
def zapustyty(*argumenty):
    """Запускає zvit.py з переданими аргументами й повертає результат subprocess."""
    # text=True — щоб stdout і stderr прийшли рядками, а не байтами
    return subprocess.run(
        [sys.executable, str(fajl_skrypta), *argumenty],
        capture_output=True, text=True, encoding="utf-8")


sproba = zapustyty(str(fajl_danykh))

print("код виходу:", sproba.returncode)
print("--- stdout ---")
print(sproba.stdout, end="")
print("--- stderr ---")
print(sproba.stderr, end="")
код виходу: 0
--- stdout ---
сума по колонці suma: 790.00
--- stderr ---
zvit.py: оброблено 5 рядків

Перше й найважливіше: результат скрипта збігається з тим, що ми порахували вручну.Перевіримо це assert-ом — і заразом переконаємось, що код виходу нульовий.

In [5]:
udacha = zapustyty(str(fajl_danykh))

assert udacha.returncode == 0, f"успішний запуск має давати 0, а дав {udacha.returncode}"

# у stdout має бути рівно один рядок — результат
ryadky_stdout = udacha.stdout.strip().split("\n")
assert len(ryadky_stdout) == 1, f"у stdout мав бути один рядок, а їх {len(ryadky_stdout)}"

# дістаємо число з кінця рядка й порівнюємо з порахованим у зошиті
chyslo_zi_skrypta = float(ryadky_stdout[0].split(":")[-1])
assert chyslo_zi_skrypta == ochikuvana_suma, \
    f"скрипт дав {chyslo_zi_skrypta}, а ми чекали {ochikuvana_suma}"

print("скрипт надрукував:", ryadky_stdout[0])
print("ми порахували руками:", ochikuvana_suma)
print("✅ скрипт рахує те саме, що й ми")
скрипт надрукував: сума по колонці suma: 790.00
ми порахували руками: 790.0
✅ скрипт рахує те саме, що й ми

5 · Довідка, якої ніхто не писавТепер найприємніше. Ми не написали жодного рядка довідки — ми лише описали аргументи.Запустимо скрипт із -h і подивимось, що argparse зібрав із цього опису.¶

In [6]:
dovidka = zapustyty("-h")

print("код виходу:", dovidka.returncode)
print("--- stdout ---")
print(dovidka.stdout)
код виходу: 0
--- stdout ---
usage: zvit.py [-h] [-k НАЗВА] [-d {suma,serednye,maksymum}] [-t N] [--tyho]
               fajl

Рахує підсумок по числовій колонці CSV-файлу з продажами.

positional arguments:
  fajl                  шлях до CSV-файлу

options:
  -h, --help            show this help message and exit
  -k НАЗВА, --kolonka НАЗВА
                        назва числової колонки (типово: suma)
  -d {suma,serednye,maksymum}, --dia {suma,serednye,maksymum}
                        що порахувати (типово: suma)
  -t N, --tochnist N    знаків після коми (типово: 2)
  --tyho                друкувати лише число, без заголовка

In [7]:
# довідка — це результат роботи (нас про неї попросили), тому вона в stdout і код 0
assert dovidka.returncode == 0, "запит довідки — це успіх, код має бути 0"
assert dovidka.stderr == "", "довідка не має нічого писати в stderr"

# усі описані аргументи мають бути в тексті довідки — і ми їх туди не вписували
for chastyna in ["usage:", "fajl", "--kolonka", "--dia", "--tochnist", "--tyho", "-h"]:
    assert chastyna in dovidka.stdout, f"у довідці немає {chastyna!r}"

# типові значення підставились через %(default)s — довідка не може застаріти
assert "типово: suma" in dovidka.stdout, "у довідці немає підставленого default"

print("✅ довідка згенерувалась сама й містить усі аргументи")
✅ довідка згенерувалась сама й містить усі аргументи

6 · Аргументи, які змінюють поведінкуОдна програма — багато різних запусків. Проженемо кілька комбінацій і перевіримокожну: --kolonka міняє колонку, --dia — дію, --tochnist — кількість знаків,--tyho прибирає заголовок.Порядок необовʼязкових аргументів не має значення — навмисно поставимо позиційнийfajl в кінець, щоб це побачити.¶

In [8]:
# (аргументи, що очікуємо побачити в stdout)
perevirky = [
    ([str(fajl_danykh)], "сума по колонці suma: 790.00"),
    ([str(fajl_danykh), "--kolonka", "kilkist"], "сума по колонці kilkist: 11.00"),
    ([str(fajl_danykh), "--dia", "serednye"], "середнє по колонці suma: 158.00"),
    ([str(fajl_danykh), "--dia", "maksymum", "-t", "1"], "максимум по колонці suma: 270.0"),
    (["-k", "kilkist", "-d", "maksymum", "--tyho", str(fajl_danykh)], "4.00"),
]

for argumenty, ochikuvano in perevirky:
    vyhid = zapustyty(*argumenty)
    otrymano = vyhid.stdout.strip()
    assert vyhid.returncode == 0, f"запуск {argumenty} мав завершитись успішно"
    assert otrymano == ochikuvano, f"чекали {ochikuvano!r}, отримали {otrymano!r}"
    # у показі замінюємо довгий тимчасовий шлях на просту назву файлу
    pokaz = " ".join(a.replace(str(fajl_danykh), "prodazhi.csv") for a in argumenty)
    print(f"✔ zvit.py {pokaz:<52} → {otrymano}")

print("✅ усі комбінації аргументів дали очікуваний результат")
✔ zvit.py prodazhi.csv                                         → сума по колонці suma: 790.00
✔ zvit.py prodazhi.csv --kolonka kilkist                       → сума по колонці kilkist: 11.00
✔ zvit.py prodazhi.csv --dia serednye                          → середнє по колонці suma: 158.00
✔ zvit.py prodazhi.csv --dia maksymum -t 1                     → максимум по колонці suma: 270.0
✔ zvit.py -k kilkist -d maksymum --tyho prodazhi.csv           → 4.00
✅ усі комбінації аргументів дали очікуваний результат

Останній рядок таблиці — той, де позиційний аргумент стоїть у кінці, після трьохнеобовʼязкових. argparse спершу вибирає з рядка все, що починається з дефіса, а те,що лишилось, розкладає по позиційних. Тому порядок і не має значення.Зверни увагу й на --tyho: у stdout лишилось голе число 4.00 без жодного тексту.Саме такий вивід зручно передавати наступній програмі в конвеєрі.

7 · Коди виходу: чесна відповідь операційній системіТепер найважливіше для того, хто запускатиме скрипт автоматично. Домовленість:0 — успіх, ненульове — щось пішло не так. argparse при поганому командному рядкузавжди повертає 2, а наші власні помилки виконання ми позначили кодом 1.Проженемо чотири різні поламані запуски й подивимось на код кожного.¶

In [9]:
polamani = [
    ("без обовʼязкового аргументу", []),
    ("значення поза choices", [str(fajl_danykh), "--dia", "medi"]),
    ("не число там, де чекали int", [str(fajl_danykh), "--tochnist", "bagato"]),
    ("невідомий аргумент", [str(fajl_danykh), "--kolir", "chervonyj"]),
]

for opys, argumenty in polamani:
    vyhid = zapustyty(*argumenty)
    ostannij_ryadok = vyhid.stderr.strip().split("\n")[-1]
    print(f"{opys:<32} код {vyhid.returncode}")
    print(f"{'':<32} {ostannij_ryadok}")
    # argparse завжди відповідає двійкою на зіпсований командний рядок
    assert vyhid.returncode == 2, f"{opys}: чекали код 2, отримали {vyhid.returncode}"
    assert vyhid.stdout == "", f"{opys}: у stdout не має бути нічого"

print("✅ на кожен зіпсований командний рядок argparse відповів кодом 2")
без обовʼязкового аргументу      код 2
                                 zvit.py: error: the following arguments are required: fajl
значення поза choices            код 2
                                 zvit.py: error: argument -d/--dia: invalid choice: 'medi' (choose from 'suma', 'serednye', 'maksymum')
не число там, де чекали int      код 2
                                 zvit.py: error: argument -t/--tochnist: invalid int value: 'bagato'
невідомий аргумент               код 2
                                 zvit.py: error: unrecognized arguments: --kolir chervonyj
✅ на кожен зіпсований командний рядок argparse відповів кодом 2

Помилка виконання — це інша історія, і код у неї інший. Файл, якого немає, іколонка, якої немає у файлі, — це не помилки командного рядка: рядок був правильний,просто зробити роботу не вийшло. Наш скрипт повертає на них 1.

In [10]:
nemaye_fajlu = zapustyty(str(majsternya / "nemaye.csv"))
nemaye_kolonky = zapustyty(str(fajl_danykh), "--kolonka", "cina")

for opys, vyhid in [("файлу немає", nemaye_fajlu), ("колонки немає", nemaye_kolonky)]:
    print(f"{opys:<16} код {vyhid.returncode} · stderr: {vyhid.stderr.strip()}")
    assert vyhid.returncode == 1, f"{opys}: чекали код 1, отримали {vyhid.returncode}"

# і найголовніше: у stderr має бути зрозуміле пояснення, а не порожнеча
assert "No such file" in nemaye_fajlu.stderr or "немає" in nemaye_fajlu.stderr, \
    "повідомлення про відсутній файл має пояснювати, що сталось"
assert "cina" in nemaye_kolonky.stderr, "у повідомленні має бути названа шукана колонка"
assert "data, tovar, kilkist, suma" in nemaye_kolonky.stderr, \
    "повідомлення має підказати, які колонки насправді є"

print("✅ помилки виконання дають код 1 і зрозуміле повідомлення")
файлу немає      код 1 · stderr: zvit.py: [Errno 2] No such file or directory: '/tmp/tema24_0iojo3q3/nemaye.csv'
колонки немає    код 1 · stderr: zvit.py: немає колонки 'cina'; є: data, tovar, kilkist, suma
✅ помилки виконання дають код 1 і зрозуміле повідомлення

8 · Два потоки, які не змішуютьсяА тепер перевіримо правило, заради якого все це затівалось: у stdout іде тількирезультат, усе інше — у stderr. Наш скрипт при кожному успішному запуску друкуєслужбовий рядок «оброблено N рядків» — і він не має потрапити в результат.¶

In [11]:
vyhid = zapustyty(str(fajl_danykh))

print("--- stdout (результат) ---")
print(vyhid.stdout, end="")
print("--- stderr (службове) ---")
print(vyhid.stderr, end="")

# службовий рядок є — але не в stdout
assert "оброблено" in vyhid.stderr, "звіт про перебіг мав піти в stderr"
assert "оброблено" not in vyhid.stdout, "звіт про перебіг НЕ мав потрапити в stdout"

# а результат є рівно в stdout і ніде більше
assert "сума по колонці" in vyhid.stdout, "результат мав піти в stdout"
assert "сума по колонці" not in vyhid.stderr, "результат НЕ мав потрапити в stderr"

print("✅ потоки не змішались: результат окремо, повідомлення окремо")
--- stdout (результат) ---
сума по колонці suma: 790.00
--- stderr (службове) ---
zvit.py: оброблено 5 рядків
✅ потоки не змішались: результат окремо, повідомлення окремо

Чому це важливо, видно одразу, щойно ми поставимо скрипт у конвеєр. Уявімо, щонаступна програма приймає тільки число — саме для цього ми й зробили --tyho.Перевіримо, що вивід у режимі --tyho можна перетворити на число без жодногоочищення. Якби службовий рядок пішов у stdout, float() тут би впав.

In [12]:
tyho = zapustyty(str(fajl_danykh), "--tyho")

# увесь stdout цілком має бути числом — жодного зайвого слова
chyslo = float(tyho.stdout.strip())

assert chyslo == ochikuvana_suma, f"чекали {ochikuvana_suma}, отримали {chyslo}"
assert tyho.stderr.strip() != "", "службове повідомлення нікуди не зникло — воно в stderr"

print("stdout цілком:", repr(tyho.stdout))
print("перетворили на число:", chyslo)
print("а службове повідомлення тим часом у stderr:", tyho.stderr.strip())
print("✅ вивід готовий до конвеєра: наступна програма отримає чисті дані")
stdout цілком: '790.00\n'
перетворили на число: 790.0
а службове повідомлення тим часом у stderr: zvit.py: оброблено 5 рядків
✅ вивід готовий до конвеєра: наступна програма отримає чисті дані

9 · Виклик main() напряму — без окремого процесуsubprocess — це чесно, але повільно: кожен запуск створює новий процес Python.Коли скрипт треба перевірити багато разів (наприклад, утестах із теми 19), його імпортують як звичайний модульі викликають main() зі своїм списком аргументів.Саме заради цього main приймає argv=None і повертає код, а не викликаєsys.exit. Зробимо так — і заразом побачимо на власні очі, що if __name__ =="__main__" справді захищає: імпорт нічого не запустить.¶

In [13]:
# додаємо майстерню в sys.path, щоб import побачив наш файл (як у темі 16)
sys.path.insert(0, str(majsternya))

import zvit  # noqa: E402 — імпорт посеред файлу тут навмисний, це частина демонстрації

# сам факт імпорту нічого не надрукував і нічого не порахував:
# уся робота захована під if __name__ == "__main__"
print("модуль імпортовано, __name__ усередині нього:", zvit.__name__)
print("а функція main на місці:", zvit.main)
модуль імпортовано, __name__ усередині нього: zvit
а функція main на місці: <function main at 0x7f6fd4f66ac0>
In [14]:
import io
from contextlib import redirect_stdout, redirect_stderr

# перехоплюємо обидва потоки окремо — так само, як це робив subprocess
buferu_stdout = io.StringIO()
buferu_stderr = io.StringIO()

with redirect_stdout(buferu_stdout), redirect_stderr(buferu_stderr):
    kod = zvit.main([str(fajl_danykh), "--dia", "serednye"])

print("код, який повернула main():", kod)
print("stdout:", buferu_stdout.getvalue().strip())
print("stderr:", buferu_stderr.getvalue().strip())

assert kod == 0, "успішний виклик main() має повернути 0"
assert "середнє по колонці suma: 158.00" in buferu_stdout.getvalue(), \
    "main() мала надрукувати те саме, що й запуск через subprocess"
print("✅ main() працює так само, як окремий процес — але вдесятеро швидше")
код, який повернула main(): 0
stdout: середнє по колонці suma: 158.00
stderr: zvit.py: оброблено 5 рядків
✅ main() працює так само, як окремий процес — але вдесятеро швидше

І остання перевірка, найцікавіша. Коли аргументи погані, argparse не «повертаєпомилку» — він завершує програму, кинувши SystemExit. Це не звичайний виняток:він успадковується від BaseException, а не від Exception, саме щоб випадковийexcept Exception його не проковтнув.Спіймаємо його руками й подивимось на код усередині.

In [15]:
buferu_stderr = io.StringIO()

try:
    with redirect_stderr(buferu_stderr):
        zvit.main([str(fajl_danykh), "--dia", "medi"])
except SystemExit as vyhid_z_prohramy:
    kod_vyhodu = vyhid_z_prohramy.code
else:
    kod_vyhodu = None

print("SystemExit спіймано, код усередині:", kod_vyhodu)
print("а в stderr тим часом:", buferu_stderr.getvalue().strip().split("\n")[-1])

assert kod_vyhodu == 2, f"argparse мав завершити програму з кодом 2, а дав {kod_vyhodu}"
assert "invalid choice" in buferu_stderr.getvalue(), \
    "argparse мав пояснити, що значення не входить у choices"

# і ще одна перевірка, яка пояснює, чому ловити треба саме SystemExit
assert not isinstance(SystemExit(2), Exception), \
    "SystemExit навмисно не Exception — щоб except Exception його не з'їв"
print("✅ SystemExit несе код виходу й навмисно живе поза ієрархією Exception")
SystemExit спіймано, код усередині: 2
а в stderr тим часом: zvit.py: error: argument -d/--dia: invalid choice: 'medi' (choose from 'suma', 'serednye', 'maksymum')
✅ SystemExit несе код виходу й навмисно живе поза ієрархією Exception

10 · Прибираємо за собоюТимчасова тека більше не потрібна. Видаляємо її цілком — і перевіряємо, що відпрактики не лишилось нічого.¶

In [16]:
# крім наших двох файлів там з'явився ще й __pycache__ — слід від import zvit
bulo = sorted(p.name for p in majsternya.iterdir())

sys.path.remove(str(majsternya))
shutil.rmtree(majsternya)

print("у майстерні лежало:", ", ".join(bulo))
print("тека існує:", majsternya.exists())

assert not majsternya.exists(), "тимчасова тека мала зникнути"
print("✅ проєкт лишився чистим")
у майстерні лежало: __pycache__, prodazhi.csv, zvit.py
тека існує: False
✅ проєкт лишився чистим

---## Що ми щойно перевірили- скрипт із argparse рахує те саме, що ми порахували руками;- довідка -h згенерувалася сама й містить усі аргументи разом із типовими значеннями;- порядок необовʼязкових аргументів не має значення;- код виходу 2 — на кожен зіпсований командний рядок, 1 — на помилку виконання, 0 — на успіх;- результат живе в stdout, службові повідомлення — у stderr, і вони не змішуються;- main() можна викликати напряму зі списком аргументів — і саме так скрипти тестують;- SystemExit несе код виходу й навмисно не є Exception.---## Завдання### 🟢 Рівень 1 — БазаДодай до zvit.py необовʼязковий аргумент --rozdiljuvach (типове значення ","),який передається в csv.DictReader параметром delimiter. Створи другий файл данихіз крапкою з комою замість коми й запусти скрипт на ньому через subprocess.Зроблено, якщо: запуск із --rozdiljuvach ";" дає код 0 і той самий підсумок, щой на файлі з комами, а запуск без цього аргументу на тому самому файлі завершуєтьсяненульовим кодом.### 🟡 Рівень 2 — ПлюсНавчи скрипт приймати кілька файлів через nargs="+" і друкувати спільнийпідсумок по всіх. Додай прапорець --po-fajlah, який замість спільного числа друкуєпо рядку на файл.Зроблено, якщо: assert підтверджує, що підсумок по двох файлах дорівнює суміпідсумків по кожному окремо, а з --po-fajlah у stdout рівно стільки рядків,скільки передали файлів.### 🔴 Рівень 3 — ВикликЗроби скрипт придатним для конвеєра. По-перше, навчи його читати зі stdin, колизамість імені файлу передали одинокий дефіс -. По-друге, додай власну функціюперевірки для type=: dodatne_cile(tekst), яка кидаєargparse.ArgumentTypeError з людським поясненням, якщо число відʼємне — і підставїї в --tochnist.Зроблено, якщо: запуск subprocess.run(..., input=вміст_файлу) з аргументом -дає той самий результат, що й запуск із іменем файлу; а --tochnist -1 завершуєтьсякодом 2, і в stderr видно саме твоє повідомлення, а не invalid int value.## Підказки- subprocess.run(..., input="текст", text=True) подає рядок на stdin запущеної програми — це і є конвеєр, тільки без термінала.- Функція для type= приймає один рядок і повертає готове значення; усе, що вона кине, argparse перетворить на повідомлення й код 2 — але лише якщо кинути саме argparse.ArgumentTypeError, ValueError або TypeError.- Щоб перевірити «стільки рядків, скільки файлів», не рахуй символи: розбий stdout.strip() методом split("\n") і візьми len.