Многовариантный для циклов

12

Есть ли способ указать несколько переменных (не только целые числа) в forциклах в bash? У меня может быть 2 файла, содержащие произвольный текст, с которым мне нужно было бы работать.

Что мне функционально нужно, так это:

for i in $(cat file1) and j in $(cat file2); do command $i $j; done

Есть идеи?

user488244
источник

Ответы:

11

Во-первых, не читайте строки с помощью for , так как есть несколько неизбежных проблем с чтением строк с помощью разделения слов.

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

while read -r x && read -r y <&3; do
    ...
done <file1 3<file2

Составить более общее решение сложно из-за того, что readвозвращает false, и по нескольким другим причинам. В этом примере можно прочитать произвольное количество потоков и вернуться после самого короткого или самого длинного ввода.

#!/usr/bin/env bash

# Open the given files and assign the resulting FDs to arrName.
# openFDs arrname file1 [file2 ...]
openFDs() {
    local x y i arr=$1

    [[ -v $arr ]] || return 1
    shift

    for x; do
        { exec {y}<"$x"; } 2>/dev/null || return 1
        printf -v "${arr}[i++]" %d "$y"
    done
}

# closeFDs FD1 [FD2 ...]
closeFDs() {
    local x
    for x; do
        exec {x}<&-
    done
}

# Read one line from each of the given FDs and assign the output to arrName.
# If the first argument is -l, returns false only when all FDs reach EOF.
# readN [ -l ] arrName FD1 [FD2 ...]
readN() {
    if [[ $1 == -l ]]; then
        local longest
        shift
    else
        local longest=
    fi

    local i x y status arr=$1
    [[ -v $arr ]] || return 1
    shift

    for x; do
        if IFS= read -ru "$x" "${arr}[i]" || { unset -v "${arr}[i]"; [[ ${longest+_} ]] && return 1; }; then
            status=0
        fi
        ((i++))
    done
    return ${status:-1}
}

# readLines file1 [file2 ...]
readLines() {
    local -a fds lines
    trap 'closeFDs "${fds[@]}"' RETURN
    openFDs fds "$@" || return 1

    while readN -l lines "${fds[@]}"; do
        printf '%-1s ' "${lines[@]}"
        echo
    done
}

{
    readLines /dev/fd/{3..6} || { echo 'error occured' >&2; exit 1; }
} <<<$'a\nb\nc\nd' 3<&0 <<<$'1\n2\n3\n4\n5' 4<&0 <<<$'x\ny\nz' 5<&0 <<<$'7\n8\n9\n10\n11\n12' 6<&0

# vim: set fenc=utf-8 ff=unix ts=4 sts=4 sw=4 ft=sh nowrap et:

Таким образом, в зависимости от того, readNполучает ли -l, либо

a 1 x 7
b 2 y 8
c 3 z 9
d 4 10
5 11
12

или

a 1 x 7
b 2 y 8
c 3 z 9

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

ormaaj
источник
||не работает для меня Он обрабатывает file1 затем file2 , но это сохранить файлы синхронизируются с &&, который выходит из то время как цикл по первому EOF . - GNU bash 4.1.5
Peter.O
У меня не было свободного времени для тестирования - да, вы, вероятно, захотите оба элемента на одну итерацию. Оператор ||«list» имеет короткое замыкание, как и в большинстве языков. Конечно, на это уже тысячи раз ответили ...
ormaaj
Дело не в том, как часто что-то упоминалось ранее. Скорее всего, код, который вы сейчас показываете в своем ответе , не работает . Исходя из вашего « вероятно, хотите оба элемента ..», похоже, что вы все еще не проверяли это.
Peter.O
@ Peter.O я в курсе. Это было исправлено.
ormaaj
2

Есть ли сделано; готово - вам нужны два форса и два квеста:

for i in $(< file1); do for j in $(< file2); do echo $i $j;done ; done

Файл2, конечно, обрабатывается для каждого слова в файле1 один раз.

Использование <вместо cat в большинстве случаев должно быть незначительным приростом производительности. Подпроцесс не задействован. (Не уверен насчет последнего предложения).

несжатый:

for i in $(< file1)
do
  for j in $(< file2)
  do
    echo $i $j
  done
done

Если вам не нравится читать слова, кроме строк и сохранять пробелы, используйте while и прочитайте:

while read line; do while read second; do echo "${line}${second}"; done <file2 ; done <file1

Если вы хотите добавить 2 файла, а не вкладывать их:

cat file1 file2 | while read line; do echo "${line}"; done

Если вы хотите сжать 2 файла, используйте paste:

paste file1 file2
Пользователь неизвестен
источник
1
Вы говорите, что подпроцесс не задействован. Не является ли () подоболочкой? Я не хочу быть подвесным, но я не уверен, что происходит сейчас. Например, я знаю, что когда у меня есть скрипт, и я не хочу, чтобы он, например, добавлял в текущую оболочку cd, я делаю это внутри (). Также работает (сон 4) и, например, после ps показывает, что процесс все еще работает. Можете ли вы уточнить, пожалуйста.
Silverrocker
Ну, извини, я очень много играю здесь без оснований. Я думал, что слышал так, но, возможно, это только по <сравнению с cat. И я не уверен, как это проверить, и когда это станет семантически важным. Если каналы вызваны, переменные в подпроцессах не попадают в основной процесс, но у нас здесь нет переменных. Я вычеркиваю критическое предложение, пока кто-нибудь не сможет его прояснить.
пользователь неизвестен
0

whileимеет интересный синтаксис. Вы можете поместить несколько команд перед do ... whileциклом, и рассматриваемый случай может потребовать манипулирования этой функцией, в зависимости от ваших конкретных требований: читаете ли вы до конца самого длинного файла или только до конца самого короткого.

Например, read || readпросто не работает (в соответствии с требованиями вопроса), поскольку при чтении первого файла trueчтение второго файла пропускается до тех пор, пока первый файл не будет прочитан от начала до конца ... Тогда, потому что статус все еще true, цикл while продолжается и читает второй файл от начала до конца.

read && readбудет читать файлы одновременно (синхронно), если вы хотите прочитать только самый короткий файл. Однако, если вы хотите прочитать оба файла eof, вам нужно работать с while'sтребованиями синтаксиса, т.е. по команде непосредственно передdo while тем цикл производства ненулевого кода возврата вырваться из цикла While.

Вот пример того, как читать оба файла на eof

while IFS= read -r line3 <&3 || ((eof3=1))
      IFS= read -r line4 <&4 || ((eof4=1))
      !((eof3 & eof4))
do
    echo "$line3, $line4"
done 3<file3 4<file4

(вы можете захотеть проверить eof3 и eof4 перед чтением, но общая идея есть, особенно в конечном состоянии true / false.

Peter.O
источник