Есть три основных идиоматических способка чтения файлов в bash. Некоторые из них загружают файл в память целиков, другие читают его построчно.
read
Возможные траблы
Пробую получить данные из /etc/password
.
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 работает), ибо данные попадут в подоболочку, созданную пайпом |
, и не выйдут оттуда. Поэтому есть другой рабочий вариант для такого случая:
grep '^nobody' /etc/passwd | { \
read -d':' user shadow uid gid gecos home shell; \
echo "$user | $shadow | $uid | $gid | $gecos | $home | $shell" \
}
# nobody | | | | | |
Но все еще нет остальных данных. Проблема в том, что
-d
— это разделительпо концу строки, а не по полям ($IFS
). Поэтому вот конечное решение:
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
, поэтому НЕ рекомендую ее юзать.
Простое построчное чтение файла
while read line; do
echo "$line"
done < somefile.txt
mapfile
Команда
mapfile
илиreadarray
читает файл в массив (список).
Появилась в bash 4.0.
Самые часто используемые параметры:
Key | Explain |
---|---|
-n count | ограничивает кол-во читаемых строк |
-s count | пропускает указанное кол-во строк |
-c/-C | отображение индикатора хода выполнения операции |
Чтение файла, загружая его в память
В самом простом случае mapfile
грузит весь файл в память:
mapfile -t nodes < /path/to/list/of/hosts # -t удаляет \n
# Цикл, обходящий узлы
for node in ${nodes[@]}; do
ssh "$node" 'echo -e "$HOSTNAME:\t$(uptime)"'
done
Чтение файла порциями
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
будет выполняется вечно.
Чтение файла методом “грубой силы”
for line in "$(< file)"; do
echo "$line"
done
Изменение $IFS
при чтении файлы
IFS - Internal Field Separator (внутренний разделитель полей).
Переменная$IFS
применима, когда требуется разбить строку на слова.
По умолчанию, используетсяIFS=$' \t\n'
(<пробел><табуляция><перевод строки>) с механизмом экранирования$''
по стандарту ANSI C.
Если уверен, что нужно изменить $IFS
, сделай это в функции (как локальное переменное) или локально по отношению к команде (IFS=':' read ...
) (Локальные переменные).
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) или подстановка процессов
Если нужно обработать только часть файлы, можно юзать временные файлы (хоть это и гемор):
Пример без использования имитации файлов:
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
С использованием имитации файлов:
diff <(cut -f1 /path/to/previous-report.log | sort -u) \
<(cut -f1 /path/to/current-report.log | sort -u)
Так-то этот прием называется подстановка процессов.
Соус: Книга Идиомы Bash → Глава 9. Файлы и не только