Bash read игнорирует ведущие пробелы

У меня есть файл a.txt со следующим содержимым

    aaa
    bbb

Когда я выполняю следующий script:

while read line
do
    echo $line
done < a.txt > b.txt

сгенерированный b.txt содержит следующие

aaa
bbb

Видно, что ведущие пространства линий удалены. Как я могу сохранить ведущие пробелы?

Ответы

Ответ 1

Это описано в разделе Bash FAQ в чтении данных по очереди.

Команда чтения изменяет каждую прочитанную строку; по умолчанию он удаляет все ведущие и завершающие пробельные символы (пробелы и вкладки или любые символы пробелов, присутствующие в IFS). Если это нежелательно, переменная IFS должна быть очищена:

# Exact lines, no trimming
while IFS= read -r line; do
  printf '%s\n' "$line"
done < "$file"

Как правильно указывает Чарльз Даффи (и я пропустил, сосредоточившись на проблеме IFS); если вы хотите увидеть пробелы в своем выходе, вам также нужно указать переменную, когда вы ее используете, или оболочка снова сбросит пробелы.

Заметки о некоторых других различиях в цитированном фрагменте по сравнению с исходным кодом.

Использование аргумента -r для read рассматривается в одном предложении в верхней части ранее связанной страницы.

Параметр -r для чтения предотвращает интерпретацию обратной косой черты (обычно используется как пара новой строки обратной косой черты, для продолжения работы по нескольким строкам). Без этой опции любые обратные косые черты на входе будут отброшены. Вы должны почти всегда использовать параметр -r с чтением.

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

Ответ 2

Здесь есть несколько проблем:

  • Если IFS не очищается, read стирает ведущие и конечные пробелы.
  • echo $line string-splits и glob - расширяет содержимое $line, разбивая его на отдельные слова и передавая эти слова в виде отдельных аргументов команде echo. Таким образом, даже если IFS, очищенный в read времени, echo $line, все равно будет отбрасывать начальное и конечное пробелы и изменить пробелы между словами в один пробельный символ каждый. Кроме того, строка, содержащая только символ *, будет расширена, чтобы содержать список имен файлов.
  • echo "$line" является значительным улучшением, но все равно не будет корректно обрабатывать такие значения, как -n, который он рассматривает как сам эхо-аргумент. printf '%s\n' "$line" полностью исправит это.
  • read без -r обрабатывает обратные слэши как символы продолжения, а не буквальное содержимое, так что они не будут включены в значения, полученные, если не удвоятся, чтобы сбежать.

Таким образом:

while IFS= read -r line; do
  printf '%s\n' "$line"
done