read — встроенная команда оболочки bash, которая читает одну строку из стандартного ввода (или из указанного файлового дескриптора), делит ее на слова по символам из переменной IFS и записывает слова в переменные. Это основной способ получить ввод пользователя в bash-скрипте и прочитать файл построчно в цикле while read.
Содержание
- Три понятия, без которых read непонятен
- Минимальный рабочий пример
- Синтаксис и как read делит строку
- Флаги read: сводная таблица
- Зачем почти всегда нужен -r
- Чтение файла построчно: while read
- Пароль, подтверждение и таймаут
- Почему переменная пустая после пайпа
- read в bash, sh и zsh
- Если не получилось
- Выводы
- Где применяется / связь с практикой
- FAQ
Примеры ниже проверены в 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] [имя ...]
Правила раздачи слов по переменным:
- Строка режется по символам
IFS, пробелы в начале и в конце отбрасываются. - Первое слово идет в первую переменную, второе — во вторую.
- В последнюю переменную попадает весь остаток строки, вместе с внутренними пробелами.
- Если слов меньше, чем переменных, лишние переменные становятся пустыми.
- Если имен нет вовсе, вся строка попадает в
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.



