Команды с аргументами

Сегодня Цыплёнок-помощник научится не просто реагировать на команду, а слушать, что ты к ней дописал — например, добавлять дело в список прямо командой /add погулять с собакой.
Аргументы команды — это всё, что пользователь написал после самой команды и пробела. Команда /add молоко — это команда add с аргументом молоко, и aiogram умеет отдать тебе этот «хвост» отдельно.

Зачем команде аргументы

Представь свой список дел на сегодня. Было бы здорово, если бы можно было просто написать боту /add сделать домашку по физике — и дело само добавилось в список. А /del 2 удалило бы вторую строчку. Не нужно никаких меню, никаких диалогов — одна команда, и сразу результат. Так работают многие реальные боты: /weather Москва, /find мемы про котов, /kick @игрок в боте игрового клана.

Секрет в том, что команда — это не только слово после слэша. Всё, что ты допишешь дальше, бот тоже получает. В прошлом уроке про хэндлеры сообщений Цыплёнок реагировал на текст целиком. Сейчас мы научимся аккуратно отделять саму команду от её «хвоста» — и этот хвост называется аргументами команды.

Вот к чему мы придём за урок — бот, который принимает дело прямо в команде:

from aiogram.filters import Command
from aiogram.types import Message
from aiogram.filters import CommandObject


@dp.message(Command("add"))
async def add_handler(message: Message, command: CommandObject):
    if command.args is None:
        await message.answer("Напиши, что добавить: например /add купить хлеб")
        return
    delo = command.args
    await message.answer(f"Добавил в список: {delo} \ud83d\udc24")

Результат: на сообщение /add купить хлеб бот ответит «Добавил в список: купить хлеб 🐤». А если написать просто /add без текста — вежливо подскажет: «Напиши, что добавить: например /add купить хлеб». Дальше разберём по косточкам, откуда бот берёт этот «хвост».

Как Telegram передаёт аргументы

Тут поможет аналогия с запиской официанту. Когда ты пишешь /add купить хлеб, Telegram не присылает боту что-то волшебное — он присылает обычную строку текста "/add купить хлеб". Всю строку целиком. А дальше aiogram, как внимательный официант, читает записку и делит её на две части:

  • команда — первое слово после слэша, то есть add;
  • аргументы — всё, что идёт после команды и первого пробела, то есть купить хлеб.

Граница между ними — самый первый пробел после команды. Всё, что слева, — это команда. Всё, что справа, — один длинный аргумент-строка. Обрати внимание: aiogram не режет аргументы на отдельные слова сам. Он отдаёт тебе весь хвост одной строкой "купить хлеб", а уже ты решаешь, что с ним делать: оставить как есть или разбить на части.

Почему так, а не сразу списком слов? Потому что библиотека не может угадать, чего именно ты хочешь. Для команды /say привет всем тебе нужен весь хвост целиком — иначе фраза развалится. А для /calc 12 + 8 тебе, наоборот, нужны три отдельных куска. Одна и та же команда у разных ботов разбирается по-разному, поэтому aiogram честно отдаёт сырой текст и не лезет в твою логику. Это удобно: ты сам выбираешь, по чему резать — по пробелам, по запятым или вообще не резать.

Ещё одна деталь, которая часто удивляет новичков: лишние пробелы. Если пользователь напишет /add   купить хлеб с тремя пробелами после команды, aiogram отрежет только одну границу и отдаст тебе аргументы уже без ведущих пробелов — то есть "купить хлеб". А вот пробелы внутри хвоста он трогать не станет: это твой текст, и решать, что с ним делать, тоже тебе. Запомни этот принцип — он избавит от половины вопросов «почему у меня в аргументах какой-то мусор».

Чтобы было совсем наглядно, посмотрим на чистом Python, как примерно происходит это деление. Возьмём метод строки split с ограничением на одно разбиение:

def razobrat(text):
    parts = text.split(maxsplit=1)
    command = parts[0]
    args = parts[1] if len(parts) > 1 else None
    return command, args


for msg in ["/add купить хлеб", "/add", "/weather Москва"]:
    print(repr(msg), "->", razobrat(msg))

Вывод:

'/add купить хлеб' -> ('/add', 'купить хлеб')
'/add' -> ('/add', None)
'/weather Москва' -> ('/weather', 'Москва')

Видишь? split(maxsplit=1) делит строку максимум один раз — поэтому команда отрывается от хвоста, а сам хвост остаётся целым, даже если в нём несколько слов. И заметь: когда после команды ничего нет, второго куска просто не существует, и мы получаем None. Это важный случай, к нему мы ещё вернёмся. Aiogram внутри делает почти то же самое, только аккуратнее — а тебе отдаёт готовый результат.

Получаем аргументы через CommandObject

Теперь главное. Чтобы aiogram отдал тебе разобранную команду, нужно попросить у него специальный объект — CommandObject. Делается это волшебно просто: ты дописываешь его в параметры хэндлера, и aiogram сам его подставит.

from aiogram.filters import Command, CommandObject
from aiogram.types import Message


@dp.message(Command("add"))
async def add_handler(message: Message, command: CommandObject):
    await message.answer(f"Аргументы команды: {command.args}")

Результат: на /add купить хлеб бот ответит «Аргументы команды: купить хлеб». На просто /add ответит «Аргументы команды: None». Разберём, что здесь происходит.

Откуда берётся command

Смотри на параметры функции: async def add_handler(message: Message, command: CommandObject). Раньше у нас был только message. Теперь мы добавили второй параметр command с типом CommandObject. Aiogram видит этот тип в подсказке и сам создаёт объект и передаёт его — тебе не надо ничего вызывать руками. Это как сказать официанту: «и принеси мне ещё разобранную записку» — а он уже знает, что это значит.

Что внутри CommandObject

У объекта command есть несколько полезных полей. Главное для нас — command.args: это строка со всеми аргументами (тот самый «хвост») или None, если аргументов не было. Есть и другие поля, они тоже пригодятся:

ПолеЧто хранитПример для /add купить хлеб
command.commandимя команды без слэша"add"
command.argsстрока аргументов или None"купить хлеб"
command.prefixпрефикс команды"/"

В 95% случаев тебе нужен только command.args. Запомни его — это твой главный инструмент в этом уроке.

Разбираем строку аргументов на части

Часто одного куска текста мало. Например, для команды /add 3 купить молоко ты, может, захочешь отдельно число 3 (приоритет дела) и отдельно текст купить молоко. Аргументы приходят одной строкой — значит, разбить их на части придётся тебе. И здесь снова работает обычный Python, который ты уже знаешь: метод строки split.

Пример 1. Один аргумент — берём как есть

Самый простой случай: команда /say, которая повторяет за тобой любой текст. Тут резать ничего не надо — весь хвост и есть то, что нужно повторить.

@dp.message(Command("say"))
async def say_handler(message: Message, command: CommandObject):
    if command.args is None:
        await message.answer("Что повторить? Напиши: /say привет всем")
        return
    await message.answer(command.args)

Результат: на /say привет, клан! бот ответит «привет, клан!». Никакого разбора — берём command.args целиком.

Пример 2. Два аргумента — отделяем первое слово

Теперь команда /remind 18 сделать уроки: первое слово — во сколько напомнить, остальное — текст напоминания. Используем split(maxsplit=1), чтобы оторвать только первый кусок, а хвост оставить целым:

@dp.message(Command("remind"))
async def remind_handler(message: Message, command: CommandObject):
    if command.args is None:
        await message.answer("Формат: /remind 18 текст напоминания")
        return
    parts = command.args.split(maxsplit=1)
    if len(parts) < 2:
        await message.answer("Нужно и время, и текст: /remind 18 сделать уроки")
        return
    chas, tekst = parts[0], parts[1]
    await message.answer(f"В {chas}:00 напомню: {tekst} \ud83d\udc24")

Результат: на /remind 18 сделать уроки бот ответит «В 18:00 напомню: сделать уроки 🐤». Почему именно maxsplit=1? Потому что без него split разрезал бы строку по каждому пробелу, и «сделать уроки» превратилось бы в два отдельных слова. А нам нужно отделить только время, а текст сохранить как есть — даже если в нём пять слов.

Пример 3. Список значений — режем по запятой

А вот команда для опроса друзей: /poll пицца, суши, бургеры. Здесь логично разделять варианты по запятой, а не по пробелу — ведь вариант может состоять из нескольких слов. Берём split(",") и подчищаем пробелы у каждого куска:

@dp.message(Command("poll"))
async def poll_handler(message: Message, command: CommandObject):
    if command.args is None:
        await message.answer("Дай варианты через запятую: /poll пицца, суши, бургеры")
        return
    varianty = [v.strip() for v in command.args.split(",")]
    spisok = "\n".join(f"{i + 1}. {v}" for i, v in enumerate(varianty))
    await message.answer(f"Голосуем за:\n{spisok}")

Результат: на /poll пицца, суши, бургеры бот ответит списком «1. пицца / 2. суши / 3. бургеры» (каждый пункт с новой строки). Метод .strip() у каждого куска убирает лишние пробелы — ведь после запятой пользователь почти всегда ставит пробел, и без strip вышло бы « суши» с пробелом в начале.

Маленький разбор приёмов с разбором строки — на чистом Python, чтобы закрепить:

args = "пицца, суши,  бургеры"
varianty = [v.strip() for v in args.split(",")]
print(varianty)
print("Количество вариантов:", len(varianty))

Вывод:

['пицца', 'суши', 'бургеры']
Количество вариантов: 3

Обрабатываем пустой ввод

А теперь — самое важное правило этого урока. Пользователь почти наверняка хоть раз напишет команду без аргументов: просто /add и ничего больше. Может, забыл дописать, может, не знает формата, может, нажал случайно. И если ты к этому не готов — бот сломается.

Помнишь, в начале мы видели, что при отсутствии аргументов command.args равно None? Вот тут это и стреляет. Если ты сразу попробуешь сделать command.args.split() на None, Python выдаст ошибку AttributeError: 'NoneType' object has no attribute 'split' — потому что у «ничего» нет метода split. Поэтому первым делом в каждом хэндлере с аргументами проверяй: а пришли ли они вообще?

@dp.message(Command("add"))
async def add_handler(message: Message, command: CommandObject):
    if command.args is None:
        await message.answer(
            "После /add напиши, что добавить.\n"
            "Например: /add купить корм коту"
        )
        return
    await message.answer(f"Добавил: {command.args} \ud83d\udc24")

Результат: на просто /add бот вежливо объяснит формат и покажет пример, а на /add купить корм коту — добавит дело. Обрати внимание на return после ответа об ошибке: он нужен, чтобы функция тут же завершилась и не пошла дальше выполнять основной код с пустыми аргументами. Без return бот сначала пожалуется, а потом всё равно попробует обработать None — и упадёт.

Заметь ещё одну приятную мелочь: текст подсказки мы собрали из двух строк, склеенных через \n — это перенос строки. В Telegram сообщение придёт в две строчки: сначала «После /add напиши, что добавить.», а под ней — пример. Такие маленькие подсказки прямо в ответе бота — признак заботы о пользователе: человек не гадает, что он сделал не так, а сразу видит правильный формат и пример. Хороший бот не ругается «ошибка», а мягко направляет — и Цыплёнок именно такой.

Хорошее правило для всех команд с аргументами выглядит так:

  1. сначала проверь command.args is None — если пусто, подскажи формат и сделай return;
  2. при необходимости разбей строку через split и проверь, что кусков хватает;
  3. только потом выполняй основную работу команды.

Частые ошибки и подводные камни

На командах с аргументами новички спотыкаются предсказуемо. Покажу грабли заранее.

1. Забыл проверить на None

Самая частая беда. Пользователь шлёт /add без текста, ты сразу делаешь command.args.split() — и ловишь AttributeError, бот в этот момент «глохнет». Лечится одной строчкой проверки if command.args is None в самом начале.

2. Перепутал command.args и message.text

Можно соблазниться взять весь текст сообщения через message.text и резать его самому. Но тогда в аргументы попадёт и сама команда: message.text для /add хлеб — это "/add хлеб" целиком, со слэшем и словом add. А command.args — уже чистый "хлеб" без команды. Бери command.args, не изобретай велосипед.

3. Забыл добавить CommandObject в параметры

Если объявить хэндлер как async def add_handler(message: Message) без второго параметра, то command внутри взять будет неоткуда. Чтобы aiogram передал объект, его нужно дописать в параметры именно с типом CommandObject — по этому типу библиотека и понимает, что подставить.

4. Используешь split() без maxsplit, когда хвост — это фраза

Если для /remind 18 сделать уроки написать просто command.args.split(), ты получишь список ['18', 'сделать', 'уроки'] — три куска вместо двух. Текст напоминания развалится. Когда первое слово — параметр, а остальное — цельная фраза, бери split(maxsplit=1).

5. Не подчистил пробелы после разбора

После split(",") по строке "пицца, суши" ты получишь ['пицца', ' суши'] — у второго варианта в начале прилип пробел. В сравнениях и выводе это аукнется. Привычка вызывать .strip() у каждого куска экономит часы отладки.

Мини-практика: команда /calc своими руками

Теперь твоя очередь. Задание: научи Цыплёнка считать простые выражения вида /calc 12 + 8. Бот должен брать два числа и знак между ними, посчитать и ответить результатом.

Подсказки, чтобы не застрять:

  1. Зарегистрируй хэндлер на Command("calc") и добавь параметр command: CommandObject.
  2. Первым делом проверь command.args is None — если пусто, подскажи формат /calc 12 + 8 и сделай return.
  3. Разбей строку: parts = command.args.split(). Здесь обычный split подойдёт — нам нужны ровно три куска: число, знак, число.
  4. Проверь, что кусков именно три (len(parts) == 3), иначе снова подскажи формат.
  5. Преврати первый и третий кусок в числа через int(...), посмотри на знак parts[1] и посчитай: для "+" сложи, для "-" вычти.
  6. Ответь результатом через await message.answer(...).

Вот примерный каркас (сначала попробуй сам, потом сверься):

@dp.message(Command("calc"))
async def calc_handler(message: Message, command: CommandObject):
    if command.args is None:
        await message.answer("Формат: /calc 12 + 8")
        return
    parts = command.args.split()
    if len(parts) != 3:
        await message.answer("Нужно: число знак число, например /calc 12 + 8")
        return
    a, znak, b = int(parts[0]), parts[1], int(parts[2])
    if znak == "+":
        await message.answer(f"{a} + {b} = {a + b}")
    elif znak == "-":
        await message.answer(f"{a} - {b} = {a - b}")
    else:
        await message.answer("Знаю только + и -")

Результат: на /calc 12 + 8 бот ответит «12 + 8 = 20», а на /calc 30 - 7 — «30 - 7 = 23». Если справился — ты уже умеешь принимать структурированные данные прямо из команды. Подумай на будущее: что если пользователь напишет /calc два плюс три? int("два") вызовет ошибку. Защититься от такого мы научимся в уроках про обработку ошибок.

Итоги

Сегодня Цыплёнок-помощник стал заметно умнее: теперь он слушает не только саму команду, но и то, что ты к ней дописал. Закрепим главное:

  • Аргументы команды — это весь текст после команды и первого пробела; Telegram присылает команду целой строкой, а aiogram отделяет хвост за тебя.
  • Чтобы получить аргументы, добавь в хэндлер параметр command: CommandObject — aiogram подставит объект сам. Нужное поле — command.args.
  • Разбивать строку на части — твоя задача: обычный split() для слов, split(maxsplit=1) для «первое слово + фраза», split(",") для списка через запятую (не забудь .strip()).
  • Всегда первым делом проверяй command.args is None и делай return — иначе на пустой команде бот упадёт с AttributeError.

В следующем уроке мы перестанем заставлять пользователя помнить форматы команд и дадим ему кнопки — те самые удобные клавиатуры под полем ввода. Цыплёнок начнёт предлагать варианты сам, а пользоваться им станет приятно даже тому, кто впервые открыл бота. Не забудь сохранить свой bot.py — до встречи!

Проверьте себя
1. Что попадёт в command.args для сообщения /add купить молоко?
AВся строка "/add купить молоко" целиком
BТолько слово "add" без слэша
CСтрока "купить молоко" — всё после команды и пробела
DСписок ['купить', 'молоко']
2. Как заставить aiogram передать в хэндлер разобранную команду?
AВызвать CommandObject() внутри функции вручную
BДобавить в параметры хэндлера аргумент command: CommandObject
CИмпортировать CommandObject и больше ничего не делать
DПрописать args=True в декораторе @dp.message
3. Чему равно command.args, если пользователь отправил просто /add без текста?
AПустой строке ""
BЗначению None
CПустому списку []
DСтроке "/add"
4. Почему для /remind 18 сделать уроки лучше использовать split(maxsplit=1)?
AЧтобы строка разбилась по каждому пробелу на отдельные слова
BЧтобы отделить только первое слово (время), а текст напоминания оставить целым
Cmaxsplit=1 ускоряет работу бота
DБез maxsplit метод split вообще не работает
5. Что произойдёт, если вызвать command.args.split() при command.args, равном None?
AВернётся пустой список []
BВернётся None
CPython выдаст ошибку AttributeError: 'NoneType' object has no attribute 'split'
DБот просто промолчит без ошибки
6. Зачем после ответа об ошибке ("напиши формат") ставят return?
AЧтобы отправить сообщение дважды
BЧтобы функция завершилась и не выполняла дальше код с пустыми аргументами
Creturn здесь не нужен, это лишняя строка
DЧтобы перезапустить бота