Интерактивный скрипт, который застывает в ожидании нажатия клавиши, - частая беда автоматизации. Пока кто-то не подтвердит действие, всё стоит. А если скрипт запускается по расписанию ночью, когда у клавиатуры никого нет, ожидание превращается в вечность: задача не движется, ресурс занят, а наутро выясняется, что ничего не выполнилось. Команда read в bash умеет решать эту проблему изящно - она способна ждать ввод лишь отведённое время, а затем продолжить по запасному сценарию.

Механизм опирается на флаг -t и на внимательную проверку кода завершения. Сам по себе таймаут бесполезен, если скрипт не различает, что именно случилось: человек ответил, промолчал или нажал конец файла. Разберём, как устроен этот флаг, какой код возврата он отдаёт в каждом случае, и как построить логику, которая не зависнет ни при каком исходе.

Флаг -t задаёт предел ожидания вплоть до долей секунды

Базовая идея проста. Флаг -t с числом секунд говорит команде read: жди ввод не дольше указанного времени. Если за этот срок полная строка не введена, команда сдаётся и возвращает управление скрипту.

read -t 5 -p "Введите имя за 5 секунд: " имя

Флаг -p тут задаёт приглашение, которое печатается перед ожиданием. Число после -t может быть и дробным, что полезно для коротких пауз - например, полсекунды на реакцию в быстром меню.

read -t 0.5 -n 1 быстрая_клавиша

Отдельный любопытный случай - таймаут, равный нулю. При нём команда не пытается ничего читать вовсе, а немедленно возвращает результат, сообщающий лишь о том, доступны ли уже данные на входе. Это используют для мгновенной проверки, не висит ли что-то непрочитанное во входном потоке, без всякого ожидания.

Код завершения больше 128 выдаёт истечение времени

Здесь кроется самое важное, и именно на этом спотыкаются те, кто добавил таймаут, но забыл проверить результат. Команда read сообщает об исходе через код завершения, и у каждого исхода свой код.

Когда человек успел ввести строку до истечения времени, код возврата равен нулю - успех. Когда время вышло, а ввода не было, код возврата оказывается больше 128. Это и есть сигнал таймаута, который скрипт обязан распознать.

read -t 5 -p "Ваш выбор: " выбор
статус=$?

if (( статус == 0 )); then
    echo "Получен ввод: $выбор"
elif (( статус > 128 )); then
    echo "Время вышло, продолжаем по умолчанию"
else
    echo "Достигнут конец ввода"
fi

Захватывать $? нужно сразу первой строкой после read. Любая команда между ними перезапишет код завершения своим собственным, и сигнал таймаута потеряется. Это общее свойство $? в оболочке: переменная хранит статус лишь последней выполненной команды.

Граница в 128 не случайна. По соглашению оболочки коды завершения выше 128 означают, что процесс был прерван чем-то внешним, а не завершился сам. Таймаут как раз и есть такое прерывание ожидания, поэтому он попадает в этот диапазон. Различать стоит именно по условию больше 128, а не по конкретному числу, потому что точное значение может слегка плавать.

Есть и третий исход, который легко упустить. Если входной поток закончился - например, ввод шёл из файла или канала, и данные иссякли - команда вернёт ненулевой код, но не превышающий 128. Это конец файла, а не таймаут, и его полезно отделять, если логика скрипта зависит от источника ввода.

Меню с автоматическим выбором по истечении времени

Связка таймаута и проверки кода раскрывается в живом примере - интерактивном меню, которое не зависнет, даже если у экрана никого нет. После отведённого времени скрипт сам делает безопасный выбор и движется дальше.

ТАЙМАУТ=10
echo "1. Установить"
echo "2. Обновить"
echo "3. Выйти"
echo "Без выбора за $ТАЙМАУТ секунд окно закроется автоматически"

read -t "$ТАЙМАУТ" -p "Выберите пункт: " пункт
статус=$?

if (( статус > 128 )); then
    echo "Время ожидания истекло, выход"
    exit 0
fi

case $пункт in
    1) echo "Запуск установки" ;;
    2) echo "Запуск обновления" ;;
    3) exit 0 ;;
    *) echo "Неверный пункт" ;;
esac

Логика здесь честно разводит три ветки. Превышение таймаута уводит скрипт в безопасный выход. Корректный ввод направляется в разбор выбора. А неузнанный пункт мягко сообщает об ошибке, не роняя скрипт. Такая структура переживает любой исход без зависания.

Тот же приём ценен в диалогах подтверждения, где у опасного действия должен быть безопасный ответ по умолчанию. Если человек промолчал, скрипт трактует молчание как отказ от рискованного шага.

read -t 15 -p "Удалить старые резервные копии? [y/N] " ответ
if (( $? > 128 )); then
    ответ="N"     # молчание означает безопасный отказ
fi

if [[ "$ответ" =~ ^[Yy]$ ]]; then
    echo "Копии удаляются"
else
    echo "Действие отменено"
fi

Переменная TMOUT задаёт таймаут по умолчанию для всего сеанса

Помимо флага -t, у механизма есть глобальная настройка - переменная окружения TMOUT. Команды read и select уважают её и используют как таймаут по умолчанию там, где явный флаг не задан. Это позволяет один раз установить предел ожидания для всех интерактивных запросов.

TMOUT=30
read -p "Ответ (таймаут 30 секунд из TMOUT): " ввод

У переменной TMOUT есть и второе, более известное применение. Если выставить её в интерактивной командной оболочке, она задаёт автоматическое отключение сеанса при простое. Когда пользователь не вводит команд дольше указанного времени, оболочка сама завершает сеанс. Это распространённая мера на серверах, где забытый открытый сеанс представляет риск. Флаг -t при этом всегда перекрывает значение TMOUT для конкретного вызова read, давая точечный контроль поверх общей настройки.

Тонкости, о которых спотыкаются на практике

Несколько деталей способны испортить вроде бы правильный код, и знать их стоит заранее.

Первая касается источника ввода. Таймаут осмыслен, когда read читает с терминала, ожидая живого человека. Если же ввод перенаправлен из файла или приходит по каналу, поведение меняется: данные обычно либо уже есть целиком, либо поток сразу закрывается, и таймаут просто не успевает сыграть свою роль. Поэтому проектировать логику с расчётом на таймаут имеет смысл для интерактивных сценариев, а не для пакетной обработки.

Вторая тонкость связана с редактированием строки. В сочетании флага таймаута с режимом полнофункционального редактирования ввода в некоторых версиях оболочки встречалась ошибка: таймаут мог остаться включённым и ошибочно сработать при следующем вызове read. Подобные дефекты со временем чинятся обновлениями bash, но это напоминание, что взаимодействие флагов бывает неочевидным, и обновлённая версия оболочки порой избавляет от загадочного поведения сама собой.

Третий момент - неполный ввод при таймауте. Если человек начал печатать, но не успел завершить строку нажатием ввода до истечения времени, частично набранное может оказаться доступным через переменную, переданную в read, хотя код завершения сообщит о таймауте. Полагаться на содержимое переменной при превышении таймаута не стоит - надёжнее ориентироваться именно на код возврата и считать ввод несостоявшимся.

Что унести с собой

Картина складывается в простую и надёжную практику. Флаг -t с числом секунд - основной инструмент, чтобы команда read не ждала вечно. Дробное число задаёт короткие паузы, ноль превращает чтение в мгновенную проверку доступности данных.

Ключ ко всему - проверка кода завершения немедленно после read. Ноль означает полученный ввод, значение больше 128 - истёкший таймаут, промежуточное ненулевое - конец файла. Логика, разводящая эти три исхода, переживает любой сценарий без зависания. Переменная TMOUT задаёт таймаут по умолчанию для всего сеанса, а флаг -t перекрывает её точечно.

Главная мысль проста: таймаут полезен ровно настолько, насколько внимательно скрипт читает его сигнал. Добавить -t и не проверить $? - значит оставить полдела, потому что без проверки скрипт не отличит ответ от молчания. Стоит связать флаг с разбором кода возврата, и интерактивный скрипт перестаёт быть заложником человека у клавиатуры - он движется дальше сам, по разумному запасному пути.