Pętla przez zawartość pliku w Bash

1387

Jak iterować każdą linię pliku tekstowego za pomocą Bash ?

Za pomocą tego skryptu:

echo "Start!"
for p in (peptides.txt)
do
    echo "${p}"
done

Otrzymuję ten wynik na ekranie:

Start!
./runPep.sh: line 3: syntax error near unexpected token `('
./runPep.sh: line 3: `for p in (peptides.txt)'

(Później chcę zrobić coś bardziej skomplikowanego $pniż tylko wyświetlanie na ekranie.)


Zmienna środowiskowa SHELL to (z env):

SHELL=/bin/bash

/bin/bash --version wynik:

GNU bash, version 3.1.17(1)-release (x86_64-suse-linux-gnu)
Copyright (C) 2005 Free Software Foundation, Inc.

cat /proc/version wynik:

Linux version 2.6.18.2-34-default (geeko@buildhost) (gcc version 4.1.2 20061115 (prerelease) (SUSE Linux)) #1 SMP Mon Nov 27 11:46:27 UTC 2006

Plik peptides.txt zawiera:

RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL
Peter Mortensen
źródło
19
Och, widzę, że wiele się tu wydarzyło: wszystkie komentarze zostały usunięte, a pytanie ponownie otwarte. Tylko dla odniesienia, zaakceptowana odpowiedź w Odczytaj plik po linii, przypisując wartość zmiennej do zmiennej, rozwiązuje problem w sposób kanoniczny i powinna być preferowana w stosunku do tutaj przyjętej.
fedorqui „SO przestań szkodzić”

Odpowiedzi:

2089

Jednym ze sposobów na to jest:

while read p; do
  echo "$p"
done <peptides.txt

Jak wskazano w komentarzach, ma to skutki uboczne przycinania wiodących białych znaków, interpretowania sekwencji odwrotnego ukośnika i pomijania ostatniego wiersza, jeśli brakuje końca linii. Jeśli są to obawy, możesz:

while IFS="" read -r p || [ -n "$p" ]
do
  printf '%s\n' "$p"
done < peptides.txt

Wyjątkowo, jeśli ciało pętli może czytać ze standardowego wejścia , możesz otworzyć plik przy użyciu innego deskryptora pliku:

while read -u 10 p; do
  ...
done 10<peptides.txt

Tutaj 10 jest tylko dowolną liczbą (różną od 0, 1, 2).

Bruno De Fraine
źródło
7
Jak mam interpretować ostatni wiersz? Plik peptides.txt jest przekierowywany do standardowego wejścia i jakoś do całego bloku while?
Peter Mortensen
11
„Slurp peptides.txt w tej pętli while, więc polecenie„ czytaj ”ma coś do zużywania.” Moja metoda „cat” jest podobna, wysyłając dane wyjściowe polecenia do bloku while w celu konsumpcji przez „read”, ale tylko uruchamia inny program w celu wykonania pracy.
Warren Young,
8
Ta metoda wydaje się pomijać ostatnią linię pliku.
xastor,
5
Podwójnie zacytuj linie !! echo „$ p” i plik .. zaufaj mi, to cię ugryzie, jeśli nie !!! WIEM! lol
Mike Q,
5
Obie wersje nie czytają ostatniego wiersza, jeśli nie jest zakończony nowym wierszem. Zawsze używajwhile read p || [[ -n $p ]]; do ...
dawg
447
cat peptides.txt | while read line 
do
   # do something with $line here
done

oraz wariant jednowarstwowy:

cat peptides.txt | while read line; do something_with_$line_here; done

Te opcje pomijają ostatni wiersz pliku, jeśli nie ma końca wiersza końcowego.

Możesz tego uniknąć, wykonując następujące czynności:

cat peptides.txt | while read line || [[ -n $line ]];
do
   # do something with $line here
done
Warren Young
źródło
68
Ogólnie, jeśli używasz „cat” z tylko jednym argumentem, robisz coś złego (lub nieoptymalnego).
JesperE
27
Tak, po prostu nie jest tak wydajny jak Bruno, ponieważ niepotrzebnie uruchamia inny program. Jeśli wydajność ma znaczenie, zrób to tak, jak Bruno. Pamiętam swoją drogę, ponieważ można jej używać z innymi poleceniami, w których składnia „przekierowanie z” nie działa.
Warren Young,
74
Jest z tym jeszcze jeden poważniejszy problem: ponieważ pętla while jest częścią potoku, działa w podpowłoce, a zatem wszelkie zmienne ustawione w pętli są tracone po jej wyjściu (patrz bash-hackers.org/wiki/doku. php / mirroring / bashfaq / 024 ). Może to być bardzo denerwujące (w zależności od tego, co próbujesz zrobić w pętli).
Gordon Davisson
25
Używam „cat file |” jako początku wielu moich poleceń wyłącznie dlatego, że często prototypuję za pomocą „head file |”
mat kelcey
62
To może nie być tak wydajne, ale jest znacznie bardziej czytelne niż inne odpowiedzi.
Savage Reader
144

Opcja 1a: Pętla while: Pojedyncza linia na raz: Przekierowanie wejścia

#!/bin/bash
filename='peptides.txt'
echo Start
while read p; do 
    echo $p
done < $filename

Opcja 1b: Pętla while: Pojedyncza linia na raz:
Otwórz plik, czytaj z deskryptora pliku (w tym przypadku deskryptor pliku # 4).

#!/bin/bash
filename='peptides.txt'
exec 4<$filename
echo Start
while read -u4 p ; do
    echo $p
done
Stan Graves
źródło
W przypadku opcji 1b: czy deskryptor pliku musi zostać ponownie zamknięty? Np. Pętla może być wewnętrzną pętlą.
Peter Mortensen
3
Deskryptor pliku zostanie wyczyszczony po wyjściu z procesu. Można wykonać jawne zamknięcie, aby ponownie użyć numeru fd. Aby zamknąć fd, użyj innego exec ze składnią & -, jak poniżej: exec 4 <& -
Stan Graves
1
Dziękuję za opcję 2. Miałem duże problemy z opcją 1, ponieważ musiałem czytać ze standardowego wejścia w pętli; w takim przypadku Opcja 1 nie będzie działać.
masgo
4
Należy wyraźniej zaznaczyć, że opcja 2 jest zdecydowanie odradzana . @masgo Opcja 1b powinna w takim przypadku działać i można ją połączyć ze składnią przekierowania wejściowego z Opcji 1a, zastępując done < $filenamedone 4<$filename(co jest przydatne, jeśli chcesz odczytać nazwę pliku z parametru polecenia, w którym to przypadku możesz po prostu zastąpić $filenameprzez $1).
Egor Hans,
Potrzebuję zapętlić zawartość pliku, na przykład tail -n +2 myfile.txt | grep 'somepattern' | cut -f3podczas uruchamiania poleceń ssh wewnątrz pętli (zużywa standardowe wejście); opcja 2 wydaje się tutaj jedynym sposobem?
user5359531,
85

Nie jest to lepsze niż inne odpowiedzi, ale jest jeszcze jednym sposobem na wykonanie pracy w pliku bez spacji (patrz komentarze). Uważam, że często potrzebuję jednowierszowych, aby przeglądać listy w plikach tekstowych bez dodatkowego etapu korzystania z oddzielnych plików skryptów.

for word in $(cat peptides.txt); do echo $word; done

Ten format pozwala mi umieścić wszystko w jednym wierszu poleceń. Zmień część „echo $ słowo” na dowolną, a możesz wydawać wiele poleceń oddzielonych średnikami. W poniższym przykładzie użyto zawartości pliku jako argumentów dwóch innych skryptów, które mogłeś napisać.

for word in $(cat peptides.txt); do cmd_a.sh $word; cmd_b.py $word; done

Lub jeśli zamierzasz używać tego jak edytora strumieniowego (naucz się sed), możesz zrzucić dane wyjściowe do innego pliku w następujący sposób.

for word in $(cat peptides.txt); do cmd_a.sh $word; cmd_b.py $word; done > outfile.txt

Użyłem ich tak, jak napisano powyżej, ponieważ użyłem plików tekstowych, w których utworzyłem je z jednym słowem w wierszu. (Patrz komentarze) Jeśli masz spacje, których nie chcesz dzielić słów / linii, robi się to trochę brzydsze, ale to samo polecenie działa w następujący sposób:

OLDIFS=$IFS; IFS=$'\n'; for line in $(cat peptides.txt); do cmd_a.sh $line; cmd_b.py $line; done > outfile.txt; IFS=$OLDIFS

To po prostu mówi powłoce, by dzieliła się tylko na znakach nowej linii, a nie na spacje, a następnie przywraca środowisko do poprzedniego stanu. W tym momencie możesz rozważyć umieszczenie tego wszystkiego w skrypcie powłoki zamiast ściśnięcia go w jednym wierszu.

Powodzenia!

mightypile
źródło
6
Bash $ (<peptides.txt) jest może bardziej elegancki, ale nadal jest błędny, co Joao powiedział poprawnie, wykonujesz logikę zastępowania poleceń, gdzie spacja lub znak nowej linii to to samo. Jeśli w linii jest spacja, pętla wykonuje DWUKROTNIE lub więcej dla tej jednej linii. Więc twój kod powinien poprawnie czytać: dla słowa w $ (<peptides.txt); zrób ... Jeśli wiesz na pewno, że nie ma spacji, to linia równa się słowu i nic ci nie jest.
maxpolk,
2
@ JoaoCosta, maxpolk: Dobre punkty, których nie wziąłem pod uwagę. Zredagowałem oryginalny post, aby je odzwierciedlić. Dzięki!
mightypile
2
Użycie forpowoduje, że tokeny / linie wejściowe podlegają rozszerzeniom powłoki, co zwykle jest niepożądane; spróbuj tego: for l in $(echo '* b c'); do echo "[$l]"; done- jak zobaczysz *- mimo że pierwotnie cytowany literał - rozwija się do plików w bieżącym katalogu.
mklement0
2
@dblanchard: Ostatni przykład, używając $ IFS, powinien zignorować spacje. Próbowałeś już tej wersji?
mightypile
4
Sposób, w jaki to polecenie staje się o wiele bardziej złożone, gdy naprawiono kluczowe problemy, bardzo dobrze pokazuje, dlaczego foriteracja linii plików jest złym pomysłem. Plus aspekt rozszerzenia wspomniany przez @ mklement0 (nawet jeśli prawdopodobnie można go obejść, wprowadzając znaki ucieczki, co ponownie sprawia, że ​​rzeczy są bardziej złożone i mniej czytelne).
Egor Hans,
69

Kilka innych rzeczy nieobjętych innymi odpowiedziami:

Odczytywanie z pliku rozdzielanego

# ':' is the delimiter here, and there are three fields on each line in the file
# IFS set below is restricted to the context of `read`, it doesn't affect any other code
while IFS=: read -r field1 field2 field3; do
  # process the fields
  # if the line has less than three fields, the missing fields will be set to an empty string
  # if the line has more than three fields, `field3` will get all the values, including the third field plus the delimiter(s)
done < input.txt

Odczytywanie z wyjścia innego polecenia, z wykorzystaniem podstawiania procesów

while read -r line; do
  # process the line
done < <(command ...)

To podejście jest lepsze niż command ... | while read -r line; do ...dlatego, że pętla while działa tutaj w bieżącej powłoce, a nie w podpowłoce, jak w przypadku tej ostatniej. Zobacz powiązany post Zmienna zmodyfikowana w pętli while nie jest zapamiętywana .

Na przykład odczyt z danych rozdzielanych znakami zerowymi find ... -print0

while read -r -d '' line; do
  # logic
  # use a second 'read ... <<< "$line"' if we need to tokenize the line
done < <(find /path/to/dir -print0)

Powiązana lektura: BashFAQ / 020 - Jak znaleźć i bezpiecznie obsługiwać nazwy plików zawierające znaki nowej linii, spacje lub oba?

Odczytywanie z więcej niż jednego pliku na raz

while read -u 3 -r line1 && read -u 4 -r line2; do
  # process the lines
  # note that the loop will end when we reach EOF on either of the files, because of the `&&`
done 3< input1.txt 4< input2.txt

Na podstawie @ chepner za odpowiedź tutaj :

-ujest rozszerzeniem bash. Dla zgodności z POSIX każde połączenie wyglądałoby mniej więcej tak read -r X <&3.

Odczytywanie całego pliku do tablicy (wersje Bash wcześniejsze niż 4)

while read -r line; do
    my_array+=("$line")
done < my_file

Jeśli plik kończy się niepełną linią (na końcu brakuje nowej linii), to:

while read -r line || [[ $line ]]; do
    my_array+=("$line")
done < my_file

Odczytywanie całego pliku do tablicy (wersje Bash 4x i nowsze)

readarray -t my_array < my_file

lub

mapfile -t my_array < my_file

I wtedy

for line in "${my_array[@]}"; do
  # process the lines
done

Powiązane posty:

codeforester
źródło
Zauważ, że zamiast command < input_filename.txtciebie zawsze możesz zrobić input_generating_command | commandlubcommand < <(input_generating_command)
masterxilo
1
Dziękujemy za wczytanie pliku do tablicy. Dokładnie to, czego potrzebuję, ponieważ potrzebuję, aby każdy wiersz parsował dwa razy, dodawał nowe zmienne, dokonywał sprawdzeń itp.
frank_108
45

Użyj pętli while, tak jak to:

while IFS= read -r line; do
   echo "$line"
done <file

Uwagi:

  1. Jeśli nie ustawisz IFSpoprawnie, utracisz wcięcie.

  2. Prawie zawsze powinieneś używać opcji -r z poleceniem read.

  3. Nie czytaj wierszy za pomocą for

Jahid
źródło
2
Dlaczego ta -ropcja?
David C. Rankin
2
@ DavidC.Rankin Opcja -r zapobiega interpretacji ukośnika odwrotnego. Note #2to link, w którym jest szczegółowo opisany ...
Jahid
Połącz to z opcją „czytaj -u” w innej odpowiedzi, a wtedy będzie idealnie.
Florin Andrei
@FlorinAndrei: Powyższy przykład nie potrzebuje -uopcji, czy mówisz o innym przykładzie -u?
Jahid
Przejrzałem twoje linki i zdziwiłem się, że nie ma odpowiedzi, która po prostu łączy twój link w uwadze 2. Ta strona zawiera wszystko, co musisz wiedzieć na ten temat. A może odradza się odpowiedzi zawierające tylko linki, czy coś w tym rodzaju?
Egor Hans,
14

Załóżmy, że masz ten plik:

$ cat /tmp/test.txt
Line 1
    Line 2 has leading space
Line 3 followed by blank line

Line 5 (follows a blank line) and has trailing space    
Line 6 has no ending CR

Istnieją cztery elementy, które zmienią znaczenie danych wyjściowych pliku odczytanych przez wiele rozwiązań Bash:

  1. Pusta linia 4;
  2. Przednie lub końcowe spacje na dwóch liniach;
  3. Utrzymanie znaczenia poszczególnych linii (tj. Każda linia jest zapisem);
  4. Linia 6 nie jest zakończona CR.

Jeśli chcesz, aby plik tekstowy linia po linii obejmował puste linie i linie końcowe bez CR, musisz użyć pętli while i mieć alternatywny test dla ostatniej linii.

Oto metody, które mogą zmienić plik (w porównaniu do tego, co catzwraca):

1) Strać ostatnią linię oraz spacje wiodące i końcowe:

$ while read -r p; do printf "%s\n" "'$p'"; done </tmp/test.txt
'Line 1'
'Line 2 has leading space'
'Line 3 followed by blank line'
''
'Line 5 (follows a blank line) and has trailing space'

(Jeśli to zrobisz while IFS= read -r p; do printf "%s\n" "'$p'"; done </tmp/test.txt, zachowujesz początkowe i końcowe spacje, ale nadal tracisz ostatni wiersz, jeśli nie jest on zakończony CR)

2) Użycie substytucji procesu catspowoduje odczytanie całego pliku w jednym łyku i utratę znaczenia poszczególnych wierszy:

$ for p in "$(cat /tmp/test.txt)"; do printf "%s\n" "'$p'"; done
'Line 1
    Line 2 has leading space
Line 3 followed by blank line

Line 5 (follows a blank line) and has trailing space    
Line 6 has no ending CR'

(Jeśli usuniesz "z $(cat /tmp/test.txt), przeczytasz plik słowo po słowie zamiast jednego łyka. Prawdopodobnie też nie to, co jest zamierzone ...)


Najbardziej niezawodny i najprostszy sposób na odczytanie pliku wiersz po wierszu i zachowanie wszystkich odstępów:

$ while IFS= read -r line || [[ -n $line ]]; do printf "'%s'\n" "$line"; done </tmp/test.txt
'Line 1'
'    Line 2 has leading space'
'Line 3 followed by blank line'
''
'Line 5 (follows a blank line) and has trailing space    '
'Line 6 has no ending CR'

Jeśli chcesz usunąć przestrzenie wiodące i handlowe, usuń IFS=część:

$ while read -r line || [[ -n $line ]]; do printf "'%s'\n" "$line"; done </tmp/test.txt
'Line 1'
'Line 2 has leading space'
'Line 3 followed by blank line'
''
'Line 5 (follows a blank line) and has trailing space'
'Line 6 has no ending CR'

(Plik tekstowy bez zakończenia \n, choć dość powszechny, jest uważany za uszkodzony w POSIX. Jeśli możesz liczyć na końcowe \n, nie potrzebujesz || [[ -n $line ]]w whilepętli.)

Więcej na stronie BASH FAQ

św
źródło
13

Jeśli nie chcesz, aby twój odczyt był przerywany znakiem nowej linii, użyj -

#!/bin/bash
while IFS='' read -r line || [[ -n "$line" ]]; do
    echo "$line"
done < "$1"

Następnie uruchom skrypt z nazwą pliku jako parametrem.

Anjul Sharma
źródło
4
#!/bin/bash
#
# Change the file name from "test" to desired input file 
# (The comments in bash are prefixed with #'s)
for x in $(cat test.txt)
do
    echo $x
done
Sinus
źródło
7
Ta odpowiedź wymaga ostrzeżeń wymienionych w odpowiedzi mightypile i może się nie powieść, jeśli jakaś linia zawiera metaznaki powłoki (z powodu niecytowanego „$ x”).
Toby Speight,
7
Tak naprawdę jestem zaskoczony, że ludzie nie wymyślili jeszcze zwykłego Nie czytaj wierszy dla ...
Egor Hans,
3

Oto mój przykład z życia, w jaki sposób zapętlać linie innego wyjścia programu, sprawdzać podłańcuchy, upuszczać podwójne cudzysłowy ze zmiennej, używać tej zmiennej poza pętlą. Wydaje mi się, że całkiem sporo prędzej czy później zadaje te pytania.

##Parse FPS from first video stream, drop quotes from fps variable
## streams.stream.0.codec_type="video"
## streams.stream.0.r_frame_rate="24000/1001"
## streams.stream.0.avg_frame_rate="24000/1001"
FPS=unknown
while read -r line; do
  if [[ $FPS == "unknown" ]] && [[ $line == *".codec_type=\"video\""* ]]; then
    echo ParseFPS $line
    FPS=parse
  fi
  if [[ $FPS == "parse" ]] && [[ $line == *".r_frame_rate="* ]]; then
    echo ParseFPS $line
    FPS=${line##*=}
    FPS="${FPS%\"}"
    FPS="${FPS#\"}"
  fi
done <<< "$(ffprobe -v quiet -print_format flat -show_format -show_streams -i "$input")"
if [ "$FPS" == "unknown" ] || [ "$FPS" == "parse" ]; then 
  echo ParseFPS Unknown frame rate
fi
echo Found $FPS

Deklaracja zmiennej poza pętlą, ustawienie wartości i użycie jej poza pętlą wymaga ukończonej składni <<< „$ (...)” . Aplikację należy uruchomić w kontekście bieżącej konsoli. Cudzysłowy wokół polecenia zachowują nowe linie strumienia wyjściowego.

Dopasowanie pętli dla podciągów następnie odczytuje parę nazwa = wartość , dzieli prawą część ostatniego = znak, upuszcza pierwszy cytat, upuszcza ostatni cytat, mamy czystą wartość do użycia w innym miejscu.

Kto ja
źródło
3
Chociaż odpowiedź jest prawidłowa, rozumiem, jak się tu znalazła. Podstawowa metoda jest taka sama, jak zaproponowana w wielu innych odpowiedziach. Dodatkowo całkowicie tonie w twoim przykładzie FPS.
Egor Hans,
0

Nadchodzi dość późno, ale z myślą, że może komuś pomóc, dodam odpowiedź. Może to nie być najlepszy sposób. headmożna użyć polecenia z -nargumentem do odczytu n wierszy od początku pliku, podobnie tailmożna użyć polecenia do odczytu od dołu. Teraz, aby pobrać n-tą linię z pliku, kierujemy n liniami , potokujemy dane do końca tylko 1 linię z danych potokowych.

   TOTAL_LINES=`wc -l $USER_FILE | cut -d " " -f1 `
   echo $TOTAL_LINES       # To validate total lines in the file

   for (( i=1 ; i <= $TOTAL_LINES; i++ ))
   do
      LINE=`head -n$i $USER_FILE | tail -n1`
      echo $LINE
   done
madD7
źródło
1
Nie rób tego Pętlowanie numerów linii i pobieranie poszczególnych linii za pomocą sedlub head+ tailjest niezwykle nieefektywne i oczywiście nasuwa się pytanie, dlaczego po prostu nie używasz jednego z innych rozwiązań tutaj. Jeśli potrzebujesz znać numer linii, dodaj licznik do while read -rpętli lub użyj, nl -baaby dodać prefiks numeru linii do każdej linii przed pętlą.
tripleee
-1

@Peter: To może Ci pomóc

echo "Start!";for p in $(cat ./pep); do
echo $p
done

To zwróci wynik

Start!
RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL
Alan Jebakumar
źródło
11
To jest bardzo złe! Dlaczego nie czytasz wierszy za pomocą „for” .
fedorqui „SO przestań krzywdzić”
3
Ta odpowiedź pokonuje wszystkie zasady określone przez dobre odpowiedzi powyżej!
codeforester
3
Usuń tę odpowiedź.
dawg
3
Teraz chłopaki, nie przesadzajcie. Odpowiedź jest zła, ale wydaje się, że działa, przynajmniej w przypadku prostych przypadków użycia. Tak długo, jak to jest zapewnione, bycie złą odpowiedzią nie odbiera jej prawa do istnienia.
Egor Hans,
3
@EgorHans, zdecydowanie się nie zgadzam: celem odpowiedzi jest nauczenie ludzi pisania oprogramowania. Uczenie ludzi robienia rzeczy w sposób, o którym wiesz , że jest dla nich szkodliwy, a ludzie, którzy korzystają z ich oprogramowania (wprowadzanie błędów / nieoczekiwanych zachowań / itp.) Świadomie krzywdzą innych. Odpowiedź, o której wiadomo, że jest szkodliwa, nie ma „prawa do istnienia” w dobrze wyselekcjonowanych zasobach dydaktycznych (a wybranie jej jest dokładnie tym, co my, ludzie głosujący i zgłaszający, powinniśmy tutaj robić).
Charles Duffy,