Команда read в bash: синтаксис, флаги -p, -s, -t, -a, -r и чтение файла построчно

Команда read в bash: синтаксис, флаги -p, -s, -t, -a, -r и чтение файла построчно Полезное

read — встроенная команда оболочки bash, которая читает одну строку из стандартного ввода (или из указанного файлового дескриптора), делит ее на слова по символам из переменной IFS и записывает слова в переменные. Это основной способ получить ввод пользователя в bash-скрипте и прочитать файл построчно в цикле while read.

Примеры ниже проверены в GNU bash 5.2.21 (Ubuntu 24.04). Отличия для macOS (там системный bash 3.2), sh/dash и zsh вынесены в отдельный раздел.

Три понятия, без которых read непонятен

  • Строка — все символы до разделителя (по умолчанию до перевода строки \n). Одна команда read читает ровно одну строку.
  • Поле (слово) — кусок строки между символами из IFS (по умолчанию пробел, табуляция и перевод строки).
  • Код возврата — 0, если строка прочитана до разделителя; не 0 при конце файла, таймауте или ошибке. На этом держится цикл while read.

Минимальный рабочий пример

Скрипт спрашивает имя сервера и порт, подставляет порт по умолчанию и проверяет ввод:

#!/usr/bin/env bash
# ask.sh - спрашивает имя сервера и порт, порт по умолчанию 22
read -r -p "Имя сервера: " host
if ! read -r -t 10 -p "Порт [22]: " port; then
  echo; port=""          # таймаут или конец ввода: недописанное не берем
fi
port=${port:-22}

if [ -z "$host" ]; then
  echo "Ошибка: имя сервера не задано" >&2
  exit 1
fi
case $port in
  *[!0-9]*) echo "Ошибка: порт должен быть числом" >&2; exit 1 ;;
esac
if [ "${#port}" -gt 5 ] || [ "$((10#$port))" -lt 1 ] || [ "$((10#$port))" -gt 65535 ]; then
  echo "Ошибка: порт вне диапазона 1-65535" >&2
  exit 1
fi
echo "Подключаюсь к $host:$((10#$port))"

Запуск в терминале: bash ask.sh, ввести db01, затем 2222. На экране появится:

Имя сервера: db01
Порт [22]: 2222
Подключаюсь к db01:2222

Что делает каждая часть:

  • -r — обратный слеш читается как обычный символ (подробнее ниже). Ставьте его почти всегда.
  • -p "..." — текст приглашения без перевода строки. Он выводится, только когда ввод идет с терминала.
  • -t 10 — ждать не больше 10 секунд. Если время вышло, read вернет код больше 128, курсор переводится на новую строку, а переменная очищается. Очистка нужна: при таймауте bash 5.2 оставляет в переменной уже набранную часть ввода (набрали 80 без Enter — после таймаута port=80, код 142), и без port="" скрипт взял бы недописанное значение.
  • ${port:-22} — если переменная пуста (нажали Enter или истек таймаут), берется 22.
  • Проверки после read обязательны: read не валидирует ввод. Злые входы из прогона: 22abc и 22 (полноширинные цифры) дают «порт должен быть числом», 70000, 0 и 99999999999999999999 — «порт вне диапазона», пустое имя — «имя сервера не задано». 10# нужен, чтобы 0022 не читалось как восьмеричное число.

Синтаксис и как read делит строку

read [-ers] [-a массив] [-d разделитель] [-i текст] [-n N] [-N N] [-p приглашение] [-t секунды] [-u fd] [имя ...]

Правила раздачи слов по переменным:

  1. Строка режется по символам IFS, пробелы в начале и в конце отбрасываются.
  2. Первое слово идет в первую переменную, второе — во вторую.
  3. В последнюю переменную попадает весь остаток строки, вместе с внутренними пробелами.
  4. Если слов меньше, чем переменных, лишние переменные становятся пустыми.
  5. Если имен нет вовсе, вся строка попадает в REPLY — причем без обрезки пробелов.
echo "Иван Петров Москва Россия" | { read -r name surname rest; echo "name=$name"; echo "surname=$surname"; echo "rest=$rest"; }
name=Иван
surname=Петров
rest=Москва Россия

Разделитель полей меняется через IFS только для одной команды: IFS=: read -r .... Так удобно разбирать строки вида /etc/passwd:

IFS=: read -r user pass uid gid gecos home shell <<< "root:x:0:0:root:/root:/bin/bash"
echo "user=$user uid=$uid home=$home shell=$shell"
user=root uid=0 home=/root shell=/bin/bash

Это упрощенный разбор: для CSV с кавычками и запятыми внутри значений read не подходит, нужен парсер (например, модуль csv в Python).

Флаги read: сводная таблица

Флаг Что делает Пример
-r не обрабатывать \ как экранирование read -r line
-p текст приглашение перед вводом (только с терминала) read -r -p "Имя: " name
-s не отображать вводимые символы read -r -s -p "Пароль: " pass
-t сек таймаут; при истечении код больше 128, набранная часть ввода остается в переменной read -r -t 5 answer
-n N вернуть управление после N символов или раньше, по Enter read -r -n 1 key
-N N читать ровно N символов, разделитель игнорируется read -r -N 4 chunk
-a имя записать слова в индексированный массив с 0 read -r -a parts
-d символ читать до этого символа, а не до \n read -r -d ';' first
-u fd читать из файлового дескриптора fd read -r -u 3 line
-e / -i текст редактирование строки через readline / текст по умолчанию (только вместе с -e) read -e -i "/tmp" dir

Проверка нескольких флагов в том же bash 5.2:

read -r -a arr <<< "red green  blue"
echo "count=${#arr[@]} first=${arr[0]} last=${arr[2]}"
read -r -n 3 a <<< "abcdef"; echo "n3=$a"
read -r -d ';' first <<< "one;two"; echo "first=$first"
count=3 first=red last=blue
n3=abc
first=one

Двойной пробел между green и blue не создал пустой элемент: пробельные символы из IFS склеиваются. С не-пробельным разделителем (IFS=:) два двоеточия подряд дают пустое поле.

Зачем почти всегда нужен -r

Без -r обратный слеш считается экранированием и пропадает. Неверно:

printf '%s\n' 'C:\new\table' | { read line; echo "без -r: $line"; }

Фактический результат — путь испорчен:

без -r: C:newtable

Исправление — read -r:

printf '%s\n' 'C:\new\table' | { read -r line; echo "с -r:   $line"; }
с -r:   C:\new\table

Чтение файла построчно: while read

Надежный шаблон для текстового файла:

while IFS= read -r line || [ -n "$line" ]; do
  echo "строка: $line"
done < file.txt

Каждая часть решает свою проблему:

  • IFS= — сохранить пробелы в начале и в конце строки (без него отступ и хвост превратится в отступ и хвост).
  • -r — сохранить обратные слеши.
  • || [ -n "$line" ] — обработать последнюю строку, если в конце файла нет перевода строки. Для файла a\nb\nc без финального \n цикл без этой проверки печатает только a и b: read прочитал c, но вернул код 1 из-за конца файла.
  • < file.txt после done — перенаправление на весь цикл, файл открывается один раз.

Если нужен весь файл в массиве, в bash 4+ проще mapfile -t lines < file.txt (синоним readarray).

Команда внутри цикла «съедает» строки

Частая ловушка в админских скриптах: цикл по списку серверов обрабатывает только первый. Причина — команда в теле цикла (ssh, ffmpeg, mysql) читает тот же stdin, что и read, и забирает остаток файла. Воспроизведение, где cat играет роль такой команды:

printf 'web1\nweb2\nweb3\n' > hosts.txt
while IFS= read -r host; do
  echo "обрабатываю $host"
  cat > /dev/null      # имитация команды, читающей stdin (ssh, ffmpeg, mysql)
done < hosts.txt
обрабатываю web1

Исправление — дать команде внутри цикла свой пустой stdin:

while IFS= read -r host; do
  echo "обрабатываю $host"
  cat > /dev/null < /dev/null   # у команды внутри цикла свой пустой stdin
done < hosts.txt
обрабатываю web1
обрабатываю web2
обрабатываю web3

Для ssh то же дает флаг ssh -n. Другой вариант — читать список из отдельного дескриптора: while IFS= read -r -u 3 host; do ...; done 3< hosts.txt. Тогда stdin остается свободным, но команды в теле будут ждать ввода с клавиатуры, если сами читают stdin.

Пароль, подтверждение и таймаут

#!/usr/bin/env bash
read -r -s -p "Пароль: " pass
echo                      # после -s перевод строки не выводится
if [ -z "$pass" ]; then echo "Пустой пароль" >&2; exit 1; fi
echo "Длина пароля: ${#pass}"
unset pass
read -r -n 1 -p "Продолжить? [y/N] " answer
echo
case $answer in
  y|Y) echo "Продолжаю" ;;
  *)   echo "Отменено" ;;
esac

При вводе s3cret и y символы пароля не отображаются, затем выводятся Длина пароля: 6 и Продолжаю. Для -n 1 Enter нажимать не нужно: read возвращается после первого символа. Любой ответ, кроме y/Y, считается отказом — безопасное умолчание для опасных действий.

Граница флага -s: он только выключает эхо в терминале. Значение лежит в обычной переменной и может утечь:

  • через set -x (трассировка печатает команды с подставленными значениями);
  • через аргументы команд: mysql -p"$pass" виден другим пользователям в ps;
  • через export — переменную получат все дочерние процессы.

Поэтому пароль передавайте программе через stdin или файл с правами 600, не включайте трассировку в этом участке и делайте unset после использования. Для неинтерактивных сценариев лучше хранилище секретов, а не ввод руками.

Таймаут: при истечении -t bash 5.2 возвращает код 142 (128 + номер сигнала SIGALRM). Проверяйте условие -gt 128, а не конкретное число. Особый случай -t 0: read ничего не читает, а только сообщает кодом возврата, есть ли данные для чтения.

Почему переменная пустая после пайпа

Неверно:

echo "hello" | read -r x; echo "x=[$x]"

Фактический результат в bash:

x=[]

Каждая часть конвейера в bash выполняется в подоболочке, и x исчезает вместе с ней. Исправления — here-string или группировка:

read -r x <<< "hello"; echo "x=[$x]"
echo "hello" | { read -r x; echo "x=[$x]"; }

Обе строки печатают x=[hello]. Еще вариант — shopt -s lastpipe в скрипте (bash 4.2+, работает, когда отключено управление заданиями, то есть в обычном неинтерактивном скрипте). В zsh исходный вариант с пайпом работает сразу: там последняя команда конвейера выполняется в текущей оболочке.

read в bash, sh и zsh

Среда Что отличается
bash 5.x (Linux) все флаги из таблицы выше
bash 3.2 (системный на macOS) нет -i, -N, mapfile, shopt -s lastpipe
dash (/bin/sh в Debian и Ubuntu) -a и -d дают ошибку Illegal option; -r и -p работают
zsh массив — флаг -A, а read -a дает bad option: -a; -p означает чтение из сопроцесса: read -p "Имя: " n дает ошибку -p: no coprocess; приглашение пишется так: read "n?Имя: "

Практический вывод: в первой строке скрипта указывайте #!/usr/bin/env bash и запускайте его через bash script.sh, а не sh script.sh, если используете флаги bash.

Если не получилось

Симптом Причина Что сделать
Пропали обратные слеши нет -r read -r
Срезались пробелы в начале строки IFS по умолчанию IFS= read -r line
Не обработана последняя строка файла нет \n в конце || [ -n "$line" ] в условии цикла
Цикл по файлу отработал один раз команда в теле читает stdin cmd < /dev/null, ssh -n или read -u 3
Переменная пуста после echo ... | read подоболочка конвейера <<<, { ...; } или lastpipe
Не видно приглашения -p ввод не с терминала (пайп, файл) это ожидаемо; для логов выводите текст через echo
Illegal option -a (dash), bad option: -a (zsh), read: -N: invalid option (bash 3.2) скрипт запущен не той оболочкой или старым bash bash script.sh, проверить shebang
Скрипт завис на read в cron или CI нет терминала и ввода задавать значения параметрами/переменными окружения, -t как страховка

Выводы

  • read — встроенная команда bash: читает одну строку, делит по IFS, остаток строки уходит в последнюю переменную, без имен — в REPLY.
  • Базовая форма для ввода — read -r -p "..." var, для файла — while IFS= read -r line || [ -n "$line" ]; do ...; done < file.
  • -s прячет только эхо на экране; пароль в переменной все равно нужно защищать от set -x, ps и export.
  • -t возвращает код больше 128 при таймауте, -n 1 удобен для подтверждений, -a пишет слова в массив.
  • Флаги зависят от оболочки: dash, zsh и старый bash на macOS ведут себя иначе, поэтому скрипт с флагами bash запускают через bash.

Где применяется / связь с практикой

Освойте тему на практике

read встречается в каждом втором скрипте администратора: мастера первичной настройки сервера, подтверждение перед удалением, разбор списков хостов и логов построчно, парсинг конфигов с разделителями. Систематически освоить shell, права, процессы и сервисы Linux можно на курсе «Администратор Linux. Базовый уровень», а разобрать отдельные темы с практикующими инженерами — на открытых уроках Otus.

FAQ

Чем read отличается от аргументов скрипта $1, $2?
Аргументы передаются при запуске и подходят для автоматизации (cron, CI), read спрашивает значение во время работы. Удобная схема — брать значение из аргумента, а read вызывать, только если аргумент не задан.

Можно ли прочитать ввод, не дожидаясь Enter?
Да, read -r -n 1 key возвращается после одного символа. Клавиши-стрелки при этом приходят несколькими символами escape-последовательности, их нужно дочитывать отдельно.

Как прочитать вывод команды построчно, не теряя переменные после цикла?
Используйте подстановку процесса: while IFS= read -r line; do ...; done < <(команда). Цикл выполнится в текущей оболочке, и переменные, измененные внутри, останутся доступны после done.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)