Я прочитал страницу help read
, но все еще не совсем понятен. Не знаю, какой вариант использовать.
Как я могу читать N строк за раз с помощью Bash?
Я прочитал страницу help read
, но все еще не совсем понятен. Не знаю, какой вариант использовать.
Как я могу читать N строк за раз с помощью Bash?
Это сложнее, чем кажется. Проблема заключается в том, как сохранить дескриптор файла.
Решение состоит в создании нового дескриптора нового файла, который работает как stdin
(дескриптор файла 0), но является независимым, а затем считывается из него по мере необходимости.
#!/bin/bash
# Create dummy input
for i in $(seq 1 10) ; do echo $i >> input-file.txt ; done
# Create new file handle 5
exec 5< input-file.txt
# Now you can use "<&5" to read from this file
while read line1 <&5 ; do
read line2 <&5
read line3 <&5
read line4 <&5
echo "Four lines: $line1 $line2 $line3 $line4"
done
# Close file handle 5
exec 5<&-
Хотя выбранный ответ работает, на самом деле нет необходимости в отдельном дескрипторе файла. Просто использование команды чтения на оригинальном дескрипторе будет работать нормально.
Вот два примера, один со строкой, другой с файлом:
# Create a dummy file
echo -e "1\n2\n3\n4" > testfile.txt
# Loop through and read two lines at a time
while read -r ONE; do
read -r TWO
echo "ONE: $ONE TWO: $TWO"
done < testfile.txt
# Create a dummy variable
STR=$(echo -e "1\n2\n3\n4")
# Loop through and read two lines at a time
while read -r ONE; do
read -r TWO
echo "ONE: $ONE TWO: $TWO"
done <<< "$STR"
Запуск вышеприведенного сценария приведет к выводу (одинаковый вывод для обоих циклов):
ONE: 1 TWO: 2
ONE: 3 TWO: 4
ONE: 1 TWO: 2
ONE: 3 TWO: 4
С Bash ≥4 вы можете использовать mapfile
следующим образом:
while mapfile -t -n 10 ary && ((${#ary[@]})); do
printf '%s\n' "${ary[@]}"
printf -- '--- SNIP ---\n'
done < file
Чтобы читать по 10 строк за раз.
Простейший метод - довольно понятный. Это похоже на метод, предоставляемый @Fmstrat, за исключением того, что второй оператор read
находится перед do
.
while read first_line; read second_line
do
echo "$first_line" "$second_line"
done
Вы можете использовать это, используя для этого многострочный вход:
seq 1 10 | while read first_line; read second_line
do
echo "$first_line" "$second_line"
done
выход:
1 2
3 4
5 6
7 8
9 10
Я не думаю, что есть способ сделать это изначально в bash, но для этого можно создать удобную функцию:
#
# Reads N lines from input, keeping further lines in the input.
#
# Arguments:
# $1: number N of lines to read.
#
# Return code:
# 0 if at least one line was read.
# 1 if input is empty.
#
function readlines () {
local N="$1"
local line
local rc="1"
# Read at most N lines
for i in $(seq 1 $N)
do
# Try reading a single line
read line
if [ $? -eq 0 ]
then
# Output line
echo $line
rc="0"
else
break
fi
done
# Return 1 if no lines where read
return $rc
}
С помощью этого можно легко выполнить цикл над N-строчными фрагментами данных, выполнив что-то вроде
while chunk=$(readlines 10)
do
echo "$chunk" | ... # Whatever processing
done
В этом цикле $chunk будет содержать 10 строк ввода на каждой итерации, кроме последней, которая будет содержать последние строки ввода, которые могут быть меньше 10, но всегда больше 0.
Это намного проще! :)
cat input-file.txt | xargs -L 10 ./do_something.sh
или
cat input-file.txt | xargs -L 10 echo
Я придумал что-то очень похожее на ответ @albarji, но более краткий.
read_n() { for i in $(seq $1); do read || return; echo $REPLY; done; }
while lines="$(read_n 5)"; do
echo "========= 5 lines below ============"
echo "$lines"
done < input-file.txt
Функция read_n
будет читать строки $1
из stdin
(используйте перенаправление, чтобы прочитать ее из файла, как и встроенная команда read
). Поскольку код выхода из read
поддерживается, вы можете использовать read_n
в цикле, как показано вышеприведенным примером.
Просто используйте цикл for
:
for i in $(seq 1 $N) ; do read line ; lines+=$line$'\n' ; done
В bash версии 4 вы также можете использовать команду mapfile
.
В зависимости от того, что вы пытаетесь сделать, вы можете просто сохранить предыдущие строки.
LINE_COUNT=0
PREVLINE1=""
PREVLINE2=""
while read LINE
do LINE_COUNT=$(($LINE_COUNT+1));
if [[ $LINE_COUNT == 3 ]]; then
LINE_COUNT=0
# do whatever you want to do with the 3 lines
done
PREVLINE2="$PREVLINE1"
PREVLINE1="$LINE"
done
done < $FILE_IN
Я знаю, что вы спросили о bash, но я поражен тем, что это работает с zsh
#!/usr/bin/env zsh
cat 3-lines.txt | read -d\4 my_var my_other_var my_third_var
К сожалению, это не работает с bash
, по крайней мере, теми версиями, которые я пробовал.
"magic" здесь - это -d\4
(это не работает в bash), который устанавливает разделитель строк как символ EOT
, который будет найден в конце вашего cat
, или любую команду, которая производит вывод.
Если вы хотите прочитать массив элементов N
, bash имеет readarray
и mapfile
, который может читать файлы с линиями N
и сохранять каждую строку в одной позиции массива.
ИЗМЕНИТЬ
После некоторых попыток я узнал, что это работает с bash:
$ read -d# a b
Hello
World
#
$ echo $a $b
Hello World
$
Однако я не мог заставить { cat /tmp/file ; echo '#'; } | read -d# a b
работать: (
echo
имитирует файл с двумя строками ввода, используйте head -2
до paste
при необходимости:
IFS=\; read A B < <(echo -en "X1 X2\nY1 Y2\n" | paste -s -d\;)
Если вы хотите читать строки в цикле и создавать пары и строки, используйте только одно слово:
while read NAME VALUE; do
echo "$NAME=$VALUE";
done < <(echo -en "M\n1\nN\n2\nO\n3\n" | xargs -L2 echo)
Вот альтернативный способ сделать это:
//This will open the file and users can start adding variables.
cat > file
//After finished ctrl + D will close it
cat file|while read line;
do
//do some stuff here
done
Awk - забавный способ для этого случая:
~$ cat test.txt
tom
[email protected]
jack
[email protected]
marry
[email protected]
gogo
[email protected]
~$ cat test.txt | awk 'BEGIN{c=1}{a=c;if(a==2){print b" "$0;c=1} if(a==1){b=$0;c=2}}'
tom [email protected]
jack [email protected]
marry [email protected]
gogo [email protected]
mapfile -n читает дополнительную строку за последней строкой, назначенной массиву, через Bash. Исправлено в 4.2.35
Посмотрев на все ответы, я думаю, что следующее является самым простым, то есть больше сценаристов поймут это лучше, чем любое другое решение, но только для небольшого числа элементов:
while read -r var1 && read -r var2; do
echo "$var1" "$var2"
done < yourfile.txt
Подход с несколькими командами также превосходен, но это менее известный синтаксис, хотя все еще интуитивно понятный:
while read -r var1; read -r var2; do
echo "$var1" "$var2"
done < yourfile.txt
Преимущество заключается в том, что вам не нужны продолжения строки для большего количества элементов:
while
read -r var1
read -r var2
...
read -r varN
do
echo "$var1" "$var2"
done < yourfile.txt
Опубликованный ответ xargs также хорош в теории, но на практике обработка комбинированных строк не так очевидна. Например, одно решение, которое я придумал, используя эту технику:
while read -r var1 var2; do
echo "$var1" "$var2"
done <<< $(cat yourfile.txt | xargs -L 2 )
но опять же это использует менее известный оператор <<<
. Однако этот подход имеет то преимущество, что если ваш сценарий изначально
while read -r var1; do
echo "$var1"
done <<< yourfile.txt
тогда расширение его на несколько строк несколько естественно:
while read -r var1 var2; do
echo "$var1" "$var2"
done <<< $(cat endpoints.txt | xargs -L 2 )
while read -r var1; do
read -r var2
echo "$var1" "$var2"
done < yourfile.txt
это единственный другой, который я бы рассмотрел среди множества приведенных, за его простоту, но синтаксически он не так выразителен; по сравнению с версией &&
или мультикомандной версией это не так.