Wielowymiarowy dla pętli

12

Czy istnieje sposób na określenie wielu zmiennych (nie tylko liczb całkowitych) w forpętlach w bash? Mogę mieć 2 pliki zawierające dowolny tekst, z którym musiałbym pracować.

Potrzebuję funkcjonalnie czegoś takiego:

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

Jakieś pomysły?

użytkownik488244
źródło

Odpowiedzi:

11

Po pierwsze, nie czytaj wierszy za pomocą , ponieważ istnieje kilka nieuniknionych problemów z czytaniem wierszy za pomocą podziału słów.

Zakładając pliki o równej długości lub jeśli chcesz zapętlać tylko do momentu odczytania krótszych dwóch plików, możliwe jest proste rozwiązanie.

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

Złożenie bardziej ogólnego rozwiązania jest trudne, ponieważ readzwraca false i kilka innych powodów. W tym przykładzie można odczytać dowolną liczbę strumieni i powrócić po najkrótszym lub najdłuższym wejściu.

#!/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:

Zatem w zależności od tego , czy readNpobiera -l, wynik jest albo

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

lub

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

Konieczność odczytu wielu strumieni w pętli bez zapisywania wszystkiego w wielu tablicach nie jest tak powszechna. Jeśli chcesz tylko czytać tablice, powinieneś rzucić okiem mapfile.

ormaaj
źródło
||nie działa dla mnie Jest w stanie przetworzyć plik1 następnie file2 , ale robi zachować pliki zsynchronizowane z &&, co wychodzi z while pętli podczas pierwszego EOF . - GNU bash 4.1.5
Peter.O
Nie miałem wolnego czasu na przetestowanie - tak, prawdopodobnie chciałbyś mieć oba elementy na iterację. Operator ||„listy” zwiera jak w większości języków. Na pewno odpowiedziano na to tysiąc razy przed ...
ormaaj
Nie chodzi o to, jak często coś wcześniej wspomniano. Przeciwnie, kod, który obecnie wyświetlasz w odpowiedzi , nie działa . Na podstawie twojego „ prawdopodobnie chcesz obu elementów ...”, brzmi to tak, jakbyś go jeszcze nie testował.
Peter.O
@ Peter.O Jestem świadomy. Zostało to naprawione.
ormaaj
2

Gotowe; gotowe - potrzebujesz dwóch forów i dwóch dones:

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

Plik 2 jest oczywiście przetwarzany raz dla każdego słowa w pliku 1.

Użycie <zamiast kota powinno w większości przypadków być niezwykłym przyrostem wydajności. Bez udziału podprocesu. (Niepewny co do ostatniego zdania).

Nieskompresowane:

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

Jeśli nie lubisz czytać słów, ale wierszy i zachować białe znaki, użyj while i czytaj:

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

Jeśli chcesz dołączyć 2 pliki, a nie zagnieżdżać je:

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

Jeśli chcesz skompresować 2 pliki, użyj wklej:

paste file1 file2
nieznany użytkownik
źródło
1
Mówisz, że nie jest zaangażowany żaden podproces. Czy () nie jest podpowłoką? Nie chcę być zawieszona, ale nie jestem pewna, co się teraz dzieje. Na przykład wiem, że kiedy mam skrypt i nie chcę, aby na przykład polutował bieżącą powłokę za pomocą płyt CD, robię to wewnątrz (). Również uruchomienie (sleep 4) i na przykład po ps pokazuje, że proces jest nadal uruchomiony. Czy możesz proszę opracować.
Silverrocker
Cóż - przepraszam, bardzo działam tutaj bez fundamentu. Myślałem, że tak słyszę, ale może to tylko <porównanie cat. Nie jestem pewien, jak go przetestować i kiedy stanie się on semantycznie ważny. Jeśli wywoływane są potoki, zmienne w podprocesach nie pojawiają się w głównym procesie, ale nie mamy tutaj zmiennych. Przekreślam zdanie krytyczne, dopóki ktoś nie może go wyjaśnić.
użytkownik nieznany
0

whilema interesującą składnię. Możesz umieścić wiele poleceń przed do ... whilepętlą, a sprawa może wymagać żonglowania tą funkcją, w zależności od twoich szczególnych wymagań: czytasz do końca najdłuższego pliku, czy tylko do końca najkrótszego.

Na przykład read || readpo prostu nie działa (zgodnie z wymaganiami pytania), ponieważ gdy czytany jest pierwszy plik true, odczyt drugiego pliku jest pomijany, dopóki pierwszy plik nie zostanie odczytany od początku do końca ... Wtedy, ponieważ status jest nadal true, pętla while trwa i odczytuje drugi plik od początku do końca.

read && readodczyta pliki jednocześnie (synchronicznie), jeśli chcesz tylko czytać do najkrótszego pliku. Jeśli jednak chcesz odczytać oba pliki eof, musisz pracować z while'swymaganiami składni, tj. przez polecenie bezpośrednio przeddo while pętli wytwarzania niezerowy kod zwrotny, aby wydostać się z pętli while.

Oto przykład, jak czytać oba pliki do 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

(możesz chcieć przetestować eof3 i eof4 przed odczytem, ​​ale ogólna idea istnieje, szczególnie w końcowym stanie prawda / fałsz.

Peter.O
źródło