Задача: спрашивать данные во время работы
Наш report.sh умеет принимать заголовок через аргумент. Мы запускаем bash report.sh 'Study report' — и скрипт печатает отчёт. Это удобно, но у такого подхода есть особенность: всё нужное нужно знать заранее, до нажатия Enter. А что если мы хотим, чтобы скрипт сам спросил заголовок, когда уже работает? Запустили, увидели приглашение, ввели текст, получили отчёт. Такой сценарий тоже бывает полезен.
Сегодня мы научимся читать данные, которые пользователь вводит во время выполнения скрипта. Для этого в Bash есть встроенная команда read. Она ждёт строку со стандартного ввода, сохраняет её в переменную, и скрипт продолжает работу. Заодно разберёмся, как в скриптах устроен ввод и вывод и почему приглашение не должно попадать в сохраняемый результат.
Наш прежний report.sh останется без изменений. Мы не будем его трогать: он работает и пусть работает. Для новой задачи создадим отдельный скрипт, чтобы не смешивать два способа в одном файле. Так и нагляднее: один скрипт берёт заголовок из аргумента, другой спрашивает.
Как read получает строку
Команда read — встроенная в Bash. Она читает одну строку из стандартного ввода. Стандартный ввод мы уже знаем: обычно это клавиатура, если мы запускаем скрипт в терминале и ничего не перенаправляем. Пользователь набирает текст, нажимает Enter — и read забирает всю строку целиком.
Самый простой вызов выглядит так:
read report_title
Bash ждёт, пока пользователь введёт строку и нажмёт Enter. Всё, что было набрано до Enter, сохраняется в переменную report_title. Точнее, сохраняется почти всё: Bash разбивает ввод по пробелам и подставляет его в переменные. Но нам важно, чтобы заголовок попал в переменную целиком, со всеми пробелами, поэтому мы сразу перейдём к более точной форме.
В полной версии мы будем использовать такую команду:
IFS= read -r report_title
Здесь две вещи: -r и IFS=. Разберём обе.
Опция -r отключает особую обработку обратного слеша. Без неё read трактует \ как символ экранирования: например, \ означает пробел, а \\ — одиночный обратный слеш. Это не всегда очевидно и может испортить данные. С -r обратный слеш остаётся обычным символом: что набрали, то и получили. Поэтому в скриптах, где важна точность, read -r используют почти всегда.
А что делает IFS= перед командой? IFS — это переменная, которая управляет тем, как Bash разбивает ввод на части и какие символы считает разделителями. Впервые мы с ней сталкиваемся именно здесь. Когда мы пишем IFS= read -r report_title, мы на время только этого read задаём IFS пустое значение. Пустой IFS отключает разбиение ввода по разделителям и не позволяет удалять пробелы по краям строки. В результате в report_title попадает ровно то, что набрал пользователь, включая начальные и конечные пробелы, если они были.
Почему такая странная форма: IFS= перед командой, а не отдельное присваивание? В Bash присваивание перед командой действует только для этой команды. Мы не хотим менять IFS для всей оболочки — это повлияло бы и на другие команды. А так: на время выполнения read переменная пуста, ввод сохраняется целиком, а после команды IFS в оболочке остаётся прежним. Аккуратно и точечно.
Возможно, такое объяснение IFS звучит сложно. Но для нашего урока достаточно понимать: IFS= перед read нужен, чтобы пробелы не потерялись. Подробные правила IFS нам сейчас не нужны. Это специальный приём для точного чтения строки.
Приглашение пишем в stderr
Теперь соберём новый скрипт. Проверим, что имя свободно:
ls -l ~/linux-scripts/interactive-report.sh
Если файла нет — используем это имя. Если есть — выберем другое, например interactive-report-2.sh. В нашем случае файла нет.
Создадим файл:
cd ~/linux-scripts
nano interactive-report.sh
Вот полный текст:
#!/bin/bash
printf 'Report title: ' >&2
IFS= read -r report_title
printf '%s\n' "$report_title"
printf 'Date: %s\n' "$(date +%Y-%m-%d)"
Разберём, что делает каждая строка.
Первая строка — знакомый shebang. Вторая — printf 'Report title: ' >&2. Это приглашение. Мы выводим текст «Report title: », чтобы пользователь понял, что нужно ввести заголовок. Но обратите внимание на >&2 в конце. Это перенаправление вывода в поток с номером 2 — стандартный поток ошибок.
Мы уже встречали запись 2>&1. Она перенаправляет поток ошибок туда же, куда идёт обычный вывод. А >&2 работает в обратную сторону: берёт обычный вывод и направляет его в поток ошибок. Зачем это нужно? Потому что приглашение — это служебная часть общения со скриптом, а не часть результата. Результат — это отчёт: заголовок и дата. Если пользователь перенаправит обычный вывод в файл, он ожидает увидеть там именно отчёт, а не подсказки. Приглашение же останется на экране, чтобы пользователь видел, что от него требуется.
Потоки мы изучали раньше: стандартный вывод — поток 1, стандартный поток ошибок — поток 2. Здесь мы просто пользуемся этим знанием в скрипте. Вывод printf уходит в поток 2, то есть на экран, даже если поток 1 перенаправлен в файл.
Третья строка — IFS= read -r report_title. Ровно то, что мы только что разобрали. Скрипт останавливается, ждёт строку от пользователя, сохраняет её в report_title целиком.
Четвёртая строка — printf '%s\n' "$report_title". Выводим заголовок из переменной. Это уже часть результата, поэтому обычный вывод, поток 1, без всяких перенаправлений.
Пятая строка — printf 'Date: %s\n' "$(date +%Y-%m-%d)". Здесь дата получается через подстановку команды прямо внутри printf. Мы не сохраняем её в отдельную переменную, как в report.sh. Это компактно и наглядно: подстановка выполнится, строка с датой подставится, printf напечатает подпись и дату. Оба подхода рабочие — в report.sh переменная пригодилась для последовательного рассказа, а здесь мы видим, что подстановку можно встраивать сразу.
Сохраним файл и выйдем из редактора.
Обычный запуск
Запустим скрипт без перенаправлений:
bash interactive-report.sh
На экране появится приглашение без перевода строки:
Report title:
Курсор останется после двоеточия и пробела. Bash ждёт. Введём заголовок и нажмём Enter:
Report title: Linux project
После нажатия Enter скрипт получит строку Linux project и выведет:
Linux project
Date: 2026-09-09
Вся сессия выглядит так:
Report title: Linux project
Linux project
Date: 2026-09-09
В первой строке рядом находятся приглашение и введённый текст. Вторая и третья строки — результат скрипта. Приглашение выведено в stderr, поэтому на экране оно видно. Введённый текст пользователь тоже видит на экране, потому что терминал отражает нажатия клавиш. Но отражение терминала — это не стандартный вывод скрипта. Терминал рисует ввод сам, независимо от наших потоков.
Сохраняем результат в файл
Теперь посмотрим, ради чего мы отправляли приглашение в stderr. Сначала проверьте имя через ls -l interactive-result.txt. Если файл уже существует, выберите другое имя для результата. Затем запустим скрипт с перенаправлением обычного вывода:
bash interactive-report.sh > interactive-result.txt
Обратите внимание: на экране снова появится приглашение Report title: . Это stderr, он не перенаправлен и остаётся на терминале. Введём заголовок, например Saved report, нажмём Enter. Скрипт завершится, но после приглашения на экране ничего не появится: обычный вывод ушёл в файл, а не на экран.
Проверим файл:
cat interactive-result.txt
Внутри:
Saved report
Date: 2026-09-09
Приглашения в файле нет. Именно этого мы добивались: в файле только результат, а служебный вопрос остался на экране. Если бы мы не использовали >&2, приглашение попало бы в файл и испортило его — первой строкой там было бы Report title: Saved report. А так отчёт чистый.
Дальше используем этот уже созданный учебный файл результата и осознанно обновляем его.
Передаём ввод через pipe
До сих пор read получал данные с клавиатуры. Но стандартный ввод можно перенаправить. Мы знакомы с конвейером: вывод одной команды передаётся на вход другой. Воспользуемся этим и передадим заголовок скрипту без ручного ввода:
printf '%s\n' 'Batch report' | bash interactive-report.sh
Что здесь происходит? printf печатает строку Batch report с переводом строки. Конвейер | направляет этот вывод на стандартный ввод команды bash interactive-report.sh. Скрипт запускается, его read смотрит на стандартный ввод и видит строку Batch report. Перевода строки в конце как раз хватает, чтобы read завершился, как будто пользователь нажал Enter.
Вывод команды:
Report title: Batch report
Date: 2026-09-09
Внимательно: где приглашение? Оно здесь, в первой строке, вместе с введённым текстом. Дело в том, что приглашение ушло в stderr, а stderr не перенаправлен — он прямо на экране. Ввод же пришёл из pipe, и терминал его не отражал, потому что никто не набирал текст с клавиатуры. Поэтому на экране мы видим приглашение, а сразу после него — уже результат скрипта. Приглашение и введённая строка оказались рядом, но текст Batch report после приглашения — это не отражение ввода, а сам вывод скрипта: он печатает заголовок из переменной.
Если бы мы перенаправили обычный вывод в файл, а ввод подали через pipe, получилось бы совсем чисто:
printf '%s\n' 'Batch report' | bash interactive-report.sh > interactive-result.txt
На экране останется только приглашение Report title: , а в файле — заголовок и дата. Убедимся:
cat interactive-result.txt
Вывод:
Batch report
Date: 2026-09-09
Мы не трогали interactive-result.txt между запусками — он перезаписан, потому что использовали >. Старое содержимое заменено новым. Это нормально для нашего учебного файла.
Что если ввода нет
Команда read читает ровно одну строку. Если достигнут конец ввода до получения завершающего перевода строки, read завершается с неуспешным статусом. Пока ввод открыт, она может просто ждать данные. Статус — это код, которым команда сообщает, удалась ли её работа. Успех — это ноль, любое ненулевое значение — неудача. Мы ещё подробно поговорим об этом в отдельном уроке, а пока достаточно знать: когда read не может прочитать строку, её статус не равен нулю.
Почему read может не прочитать строку? Например, если стандартный ввод достиг конца. При чтении из терминала Ctrl+D на пустой строке позволяет сообщить о конце ввода. Или входной файл окажется пустым. Если до конца ввода не пришло ни одного символа, переменная будет пустой. Если символы пришли без завершающего перевода строки, read может сохранить их и всё равно вернуть неуспешный статус.
Наш скрипт пока не проверяет статус read. Он просто идёт дальше и печатает всё, что есть в переменной. Если ввода не было, переменная пуста, и в отчёте окажется пустая строка вместо заголовка. Это не ошибка с точки зрения Bash, но и полноценным отчётом такое назвать нельзя. Проверку добавим в следующем уроке, когда познакомимся с условиями. До тех пор будем считать, что скрипт ожидает какой-то ввод, и либо вводим строку, либо не удивляемся пустому заголовку.
Старые файлы не тронуты
Мы добавили два новых файла: скрипт interactive-report.sh и результат interactive-result.txt. Наш report.sh остался в том виде, в котором был после прошлого урока: семь строк, заголовок из $1, дата, каталог. Мы его не открывали и не меняли. arguments.sh тоже на месте. Это хорошая практика: для нового способа работы создаём новый файл, а не переделываем уже работающий.
В нашем каталоге ~/linux-scripts теперь три скрипта и один файл результата. Все созданы нами, системные файлы мы не трогали.
Команда read читает одну строку из стандартного ввода и сохраняет её в переменную. Мы использовали форму IFS= read -r report_title: опция -r оставляет обратные слеши обычными символами, а временный пустой IFS отключает разбиение ввода и удаление краевых пробелов, так что заголовок попадает в переменную целиком. Приглашение «Report title: » скрипт выводит через >&2, то есть в стандартный поток ошибок. Благодаря этому при запуске bash interactive-report.sh > interactive-result.txt в файл попадает только результат — заголовок и дата, а приглашение остаётся на экране.
Скрипт получает ввод и от пользователя, и через конвейер. В команде printf '%s\n' 'Batch report' | bash interactive-report.sh вывод printf становится стандартным вводом скрипта, и read забирает строку оттуда. Приглашение и в этом случае уходит в stderr, поэтому на экране оно видно, но в файл при перенаправлении не попадает. Если ввода нет — например, нажат Ctrl+D — read завершается с неуспешным статусом, а скрипт пока идёт дальше и печатает пустой заголовок. Проверять успех чтения мы научимся в уроке про условия.
Создайте интерактивный скрипт, который спрашивает заголовок у пользователя во время работы. Заодно разберитесь, как отделить служебное приглашение от результата скрипта.
Создание интерактивного скрипта
Новый скрипт будет спрашивать данные, а не получать их через аргумент.
Напишите полный текст interactive-report.sh для Bash. Сценарий должен вывести приглашение Report title: в stderr, прочитать одну строку в report_title с сохранением краевых пробелов и обратных слешей, затем вывести в stdout заголовок и строку Date: с текущей датой YYYY-MM-DD. Объясните роль -r, IFS= и >&2. Файл достаточно представить текстом, не меняя проект теории.
Перенаправление вывода интерактивного скрипта
Проверьте, что останется на экране, а что попадёт в файл при перенаправлении.
Что будет на экране и в файле interactive-result.txt?
Ввод через pipe и поведение read
Скрипт может получать ввод не только с клавиатуры, но и из конвейера.
Запишите команду, которая передаст строку 'Batch report' на стандартный ввод скрипта interactive-report.sh через конвейер, а обычный вывод скрипта сохранит в файл interactive-result.txt. Что останется на экране? Объясните, почему.