Skip to content

Чтение файлов в bash

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

read

Возможные траблы

Пробую получить данные из /etc/password.

Bash
grep '^nobody' /etc/passwd | read -d':' user shadow uid gid gecos home shell
echo "$user | $shadow | $uid | $gid | $gecos | $home | $shell"
# |  |  |  |  |  |          # в bash
# nobody |  |  |  |  |  |   # в zsh

Команда выше не выведет данные в bash (в zsh работает), ибо данные попадут в подоболочку, созданную пайпом (|), и не выйдут оттуда. Поэтому есть другой рабочий вариант для такого случая:

Bash
grep '^nobody' /etc/passwd | { \
    read -d':' user shadow uid gid gecos home shell; \
    echo "$user | $shadow | $uid | $gid | $gecos | $home | $shell" \
}
# nobody |  |  |  |  |  | 

Но все еще нет остальных данных. Проблема в том, что -d - это разделительпо концу строки, а не по полям ($IFS). Поэтому вот конечное решение:

Bash
grep '^nobody' /etc/passwd | { \
    IFS=':' read user shadow uid gid gecos home shell; \
    echo "$user | $shadow | $uid | $gid | $gecos | $home | $shell"; \
}
# nobody | x | 65534 | 65534 | nobody | /nonexistent | /usr/sbin/nologin

lastpipe

В bash 4.0+ можно установить параметр shopt -s lastpipe, чтобы последняя команда конвейера выполнялась в текущей оболочке и скрипт мог видеть окружение.
Этот прием работает, только если отключено управление заданиями (в скриптах оно отключено по умолчанию, но может быть включено в интерактивном сеансе).
Отключить управление заданиями можно с помощью команды set +m, но при этом выключится реакция на комбинации клавиш CTRL-C и CTRL-Z, а также команды fg и bg, поэтому НЕ рекомендую ее юзать.

Простое построчное чтение файла

Bash
while read line; do
    echo "$line"
done < somefile.txt

mapfile

Команда mapfile или readarray читает файл в массив (список).
Появилась в bash 4.0.

Самые часто используемые параметры:
+ -n count -- ограничивает кол-во читаемых строк
+ -s count -- пропускает указанное кол-во строк
+ -c/-C -- отображение индикатора хода выполнения операции

Чтение файла, загружая его в память

В самом простом случае mapfile грузит весь файл в память:

Bash
mapfile -t nodes < /path/to/list/of/hosts  # -t удаляет \n
# Цикл, обходящий узлы
for node in ${nodes[@]}; do
    ssh "$node" 'echo -e "$HOSTNAME:\t$(uptime)"'
done

Чтение файла порциями

Bash
BATCH=10
# Прочитать данные....           && если данные доступны!
while mapfile -t -n $BATCH nodes && ((${#nodes[@]}); do
    for node in ${nodes[@]}; do
        ssh "$node" 'echo -e "$HOSTNAME:\t$(uptime)"'
    done
done < /path/to/list/of/hosts

Ключ -n усложняет работу тем, что нужно будет проверять, были ли прочитаны какие-то данные (значение ${nodes[@]} отлично от нуля), иначе while mapfile будет выполняется вечно.


Чтение файла методом "грубой силы"

Bash
for line in "$(< file)"; do
    echo "$line"
done

Изменение $IFS при чтении файлы

IFS - Internal Field Separator (внутренний разделитель полей).
Переменная $IFS применима, когда требуется разбить строку на слова.
По умолчанию, используется IFS=$' \t\n' (<пробел><табуляция><перевод строки>) с механизмом экранирования $'' по стандарту ANSI C.

Если уверен, что нужно изменить $IFS, сделай это в функции (как локальное переменное) или локально по отношению к команде (IFS=':' read ...) ([[Функции в bash#Локальные переменные]]).

Bash
while IFS='' read line word1 word2 word3; do
    :
done < $some_file
# или
function Read_File {
    local IFS=''
    while read line word1 word2 word3; do
        :
    done < $some_file
}

Имитации файлов (pretend files) или подстановка процессов

Если нужно обработать только часть файлы, можно юзать временные файлы (хоть это и гемор):

Пример без использования имитации файлов:

Bash
cut -f1 /path/to/previous-report.log | sort -u > /tmp/previous-report.log
cut -f1 /path/to/current-report.log | sort -u > /tmp/current-report.log
diff /tmp/previous-report.log /tmp/current-report.log
rm /tmp/previous-report.log /tmp/current-report.log

С использованием имитации файлов:

Bash
diff <(cut -f1 /path/to/previous-report.log | sort -u) \
     <(cut -f1 /path/to/current-report.log | sort -u)

Так-то этот прием называется подстановка процессов.


Соус: Книга "Идиомы Bash" --> Глава 9. "Файлы и не только"

bash