Jak czytać z pliku lub STDIN w Bash?


244

Poniższy skrypt Perla ( my.pl) może czytać z pliku w args wiersza poleceń lub ze STDIN:

while (<>) {
   print($_);
}

perl my.plodczyta ze STDIN, a perl my.pl a.txtodczyta z a.txt. To jest bardzo wygodne.

Zastanawiasz się, czy w Bash istnieje odpowiednik?

Odpowiedzi:


409

Poniższe rozwiązanie czyta z pliku, jeśli skrypt jest wywoływany z nazwą pliku jako pierwszym parametrem, w $1przeciwnym razie ze standardowego wejścia.

while read line
do
  echo "$line"
done < "${1:-/dev/stdin}"

Podstawienie ${1:-...}trwa, $1jeśli zdefiniowano inaczej, używana jest nazwa pliku standardowego wejścia własnego procesu.


1
Fajnie, działa. Kolejne pytanie brzmi: dlaczego dodajesz wycenę? „$ {1: - / proc / $ {$} / fd / 0}”
Dagang,

15
Nazwa pliku, którą podajesz w wierszu poleceń, może być pusta.
Fritz G. Mehner,

3
Czy jest jakaś różnica między używaniem /proc/$$/fd/0a /dev/stdin? Zauważyłem, że ten drugi wydaje się bardziej powszechny i ​​wygląda na bardziej prosty.
knowah

19
Lepiej dodać -rdo swojej readkomendy, aby przypadkowo nie zjadła \ znaków; służy while IFS= read -r linedo zachowania wiodących i końcowych białych znaków.
mklement0

1
@NeDark: To ciekawe; Właśnie sprawdziłem, czy działa na tej platformie, nawet jeśli używasz /bin/sh- czy używasz powłoki innej niż bashlub sh?
mklement0

119

Być może najprostszym rozwiązaniem jest przekierowanie stdin za pomocą łączącego operatora przekierowania:

#!/bin/bash
less <&0

Stdin to zero deskryptorów plików. Powyższe powoduje przesłanie danych wejściowych do skryptu bash do standardowego interfejsu less.

Dowiedz się więcej o przekierowaniu deskryptora pliku .


1
Żałuję, że nie mam więcej pochwał, których szukałem od lat.
Marcus Downing

13
W <&0tej sytuacji nie ma żadnej korzyści - twój przykład będzie działał tak samo z nią, jak i bez niej - najwyraźniej narzędzia, które wywołujesz z poziomu skryptu bash, domyślnie widzą to samo stdin co sam skrypt (chyba że skrypt zużyje go jako pierwszy).
mklement0

@ mkelement0 Więc jeśli narzędzie odczytuje połowę bufora wejściowego, czy następne narzędzie, które wywołuję, odbierze resztę?
Asad Saeeduddin

„Brak nazwy pliku („ mniej - pomoc ”po pomoc)”, gdy to robię ... Ubuntu 16.04
OmarOthman

5
gdzie jest część „lub z pliku” w tej odpowiedzi?
Sebastian

84

Oto najprostszy sposób:

#!/bin/sh
cat -

Stosowanie:

$ echo test | sh my_script.sh
test

Aby przypisać stdin do zmiennej, możesz użyć: STDIN=$(cat -)lub po prostu, STDIN=$(cat)ponieważ operator nie jest konieczny (zgodnie z komentarzem @ mklement0 ).


Aby przeanalizować każdy wiersz ze standardowego wejścia , wypróbuj następujący skrypt:

#!/bin/bash
while IFS= read -r line; do
  printf '%s\n' "$line"
done

Aby odczytać z pliku lub standardowego wejścia (jeśli nie ma argumentu), możesz go rozszerzyć do:

#!/bin/bash
file=${1--} # POSIX-compliant; ${1:--} can be used either.
while IFS= read -r line; do
  printf '%s\n' "$line" # Or: env POSIXLY_CORRECT=1 echo "$line"
done < <(cat -- "$file")

Uwagi:

- read -r- Nie traktuj ukośnika w żaden szczególny sposób. Rozważ każdy ukośnik odwrotny jako część linii wejściowej.

- Bez ustawienia IFS, domyślnie sekwencje SpaceiTab na początku i na końcu linii są ignorowane (przycięte).

- Użyj printfzamiast, echoaby uniknąć drukowania pustych linii, gdy linia składa się z jednego -e, -nlub -E. Jednakże istnieje obejście za pomocą env POSIXLY_CORRECT=1 echo "$line"którego wykonuje swoją zewnętrzną GNU echo, który wspiera go. Zobacz: Jak wykonać echo „-e”?

Zobacz: Jak czytać stdin, gdy nie są przekazywane żadne argumenty? w stackoverflow SE


Można uprościć [ "$1" ] && FILE=$1 || FILE="-"do FILE=${1:--}. (Quibble: lepiej unikać zmiennych wielkimi literami w powłoce , aby uniknąć kolizji nazw ze zmiennymi środowiskowymi .)
mklement0

Cała przyjemność po mojej stronie; w rzeczywistości ${1:--} jest zgodny z POSIX, więc powinien działać we wszystkich powłokach podobnych do POSIX. To, co nie zadziała we wszystkich takich powłokach, to proces substytucji ( <(...)); będzie działał na przykład w bash, ksh, zsh, ale nie w desce rozdzielczej. Lepiej też dodać -rdo swojego readpolecenia, aby przypadkowo nie zjadł \ znaków; starają IFS= się zachować wiodące i końcowe białe znaki.
mklement0

4
W rzeczywistości kod nadal łamie powodu echo: jeśli linia składa się z -e, -nlub -E, nie będą wyświetlane. Aby rozwiązać ten problem, należy użyć printf: printf '%s\n' "$line". Nie uwzględniłem go w mojej poprzedniej edycji… zbyt często moje zmiany są wycofywane, gdy naprawiam ten błąd :(.
gniourf_gniourf

1
Nie, to nie zawodzi. I to --jest bezużyteczne, jeśli pierwszy argument to'%s\n'
gniourf_gniourf

1
Twoja odpowiedź jest dla mnie w porządku (mam na myśli, że nie mam już żadnych błędów ani niepożądanych funkcji, których jestem świadomy) - chociaż nie traktuje wielu argumentów jak Perl. W rzeczywistości, jeśli chcesz obsługiwać wiele argumentów, będziesz skończyć pisanie doskonałe odpowiedź-w rzeczywistości twoje Jonathan Leffler byłoby lepiej, odkąd korzystania IFS=z readi printfzamiast echo. :).
gniourf_gniourf

19

Myślę, że jest to prosty sposób:

$ cat reader.sh
#!/bin/bash
while read line; do
  echo "reading: ${line}"
done < /dev/stdin

-

$ cat writer.sh
#!/bin/bash
for i in {0..5}; do
  echo "line ${i}"
done

-

$ ./writer.sh | ./reader.sh
reading: line 0
reading: line 1
reading: line 2
reading: line 3
reading: line 4
reading: line 5

4
Nie odpowiada to wymaganiom podanym przez plakat do odczytu ze standardowego wejścia lub argumentu pliku, to po prostu czyta ze standardowego wejścia.
nash

2
Pozostawienie @ Nasha ważnego sprzeciwu bok: readczyta z stdin domyślnie , więc nie ma potrzeby na < /dev/stdin.
mklement0

13

echoRozwiązanie dodaje nowe linie gdy IFSprzełamuje strumień wejściowy. Odpowiedź @ fgm można nieco zmodyfikować:

cat "${1:-/dev/stdin}" > "${2:-/dev/stdout}"

Czy możesz wyjaśnić, co rozumiesz przez „rozwiązanie echa dodaje nowe linie, ilekroć IFS przerywa strumień wejściowy”? W przypadku, gdy zostały odnosząc się do read„s zachowanie: gdy read ma potencjalnie podzielone na wiele żetonów przez znaki. zawarty w $IFSnim, zwraca tylko jeden token, jeśli podasz tylko jedną nazwę zmiennej (ale domyślnie przycina i początkowe i końcowe białe znaki).
mklement0

@ mklement0 Zgadzam się w 100% z tobą w sprawie zachowania readi $IFS- echosama dodaje nowe linie bez -nflagi. „Narzędzie echo zapisuje na standardowym wyjściu dowolne określone operandy, oddzielone pojedynczymi pustymi znakami (` `), a następnie znakiem nowej linii (` \ n '). ”
David Souther

Rozumiem. Jednak, aby emulować pętlę Perla, musisz\n dodać znak końcowy echo: Perl $_ zawiera linię kończącą się \nna czytanej linii, podczas gdy bash readnie. (Jednak, jak wskazuje @gniourf_gniourf w innym miejscu, bardziej niezawodne podejście można zastosować printf '%s\n'zamiast echo).
mklement0

8

Pętla Perla w pytaniu czyta wszystkie argumenty nazwy pliku w wierszu poleceń lub ze standardowego wejścia, jeśli nie określono żadnych plików. Wszystkie odpowiedzi, które widzę, wydają się przetwarzać pojedynczy plik lub standardowe dane wejściowe, jeśli nie określono pliku.

Chociaż często wyśmiewane jako UUOC (Bezużyteczne użycie cat), zdarzają się chwile, kiedy catjest to najlepsze narzędzie do pracy i można argumentować, że jest to jedno z nich:

cat "$@" |
while read -r line
do
    echo "$line"
done

Jedynym minusem jest to, że tworzy potok działający w podpowłoce, więc rzeczy takie jak przypisania zmiennych w whilepętli nie są dostępne poza potokiem. Rozwiązaniem bashtego problemu jest proces zastępowania :

while read -r line
do
    echo "$line"
done < <(cat "$@")

To powoduje, że whilepętla działa w głównej powłoce, więc zmienne ustawione w pętli są dostępne poza nią.


1
Doskonały punkt na temat wielu plików. Nie wiem, jakie byłyby implikacje dotyczące zasobów i wydajności, ale jeśli nie korzystasz z bash, ksh lub zsh, a zatem nie możesz korzystać z zastępowania procesów, możesz wypróbować dokument tutaj z podstawieniem poleceń (rozłożone na 3 linie) >>EOF\n$(cat "$@")\nEOF. Wreszcie, spór: while IFS= read -r linejest lepszym przybliżeniem tego, co while (<>)robi w Perlu (zachowuje wiodące i końcowe białe spacje - chociaż Perl również utrzymuje końcowe \n).
mklement0

4

Zachowanie Perla, z kodem podanym w OP, nie może przyjmować żadnego lub kilku argumentów, a jeśli argument jest pojedynczym łącznikiem, -jest to rozumiane jako standardowe. Co więcej, zawsze można mieć nazwę pliku $ARGV. Żadna z podanych do tej pory odpowiedzi nie naśladuje zachowania Perla pod tym względem. Oto czysta możliwość Bash. Sztuka polega na execodpowiednim użyciu .

#!/bin/bash

(($#)) || set -- -
while (($#)); do
   { [[ $1 = - ]] || exec < "$1"; } &&
   while read -r; do
      printf '%s\n' "$REPLY"
   done
   shift
done

Nazwa pliku jest dostępna w $1 .

Jeśli nie podano żadnych argumentów, sztucznie ustawiamy -jako pierwszy parametr pozycyjny. Następnie zapętlamy parametry. Jeśli parametr nie jest -, przekierowujemy standardowe wejście z nazwy pliku za pomocą exec. Jeśli przekierowanie się powiedzie, zapętlamy whilepętlę. Używam REPLYzmiennej standardowej , w tym przypadku nie trzeba resetować IFS. Jeśli chcesz mieć inną nazwę, musisz IFStak zresetować (chyba że oczywiście nie chcesz tego i wiesz, co robisz):

while IFS= read -r line; do
    printf '%s\n' "$line"
done

2

Dokładniej...

while IFS= read -r line ; do
    printf "%s\n" "$line"
done < file

2
Zakładam, że jest to zasadniczo komentarz na stackoverflow.com/a/6980232/45375 , a nie odpowiedź. Aby komentarz był wyraźny: dodawanie IFS=i -r do readpolecenia zapewnia, że ​​każdy wiersz jest odczytywany w niezmodyfikowany sposób (w tym początkowe i końcowe białe znaki).
mklement0

2

Wypróbuj następujący kod:

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

1
Zauważ, że nawet po poprawkach nie będzie to odczytane ze standardowego wejścia lub z wielu plików, więc nie jest to pełna odpowiedź na pytanie. (Zaskakujące jest także to, że dwie zmiany pojawiły się w ciągu kilku minut ponad 3 lata po pierwszym złożeniu odpowiedzi.)
Jonathan Leffler

@JonathanLeffler przepraszam za edycję tak starej (i niezbyt dobrej) odpowiedzi… ale nie mogłem znieść widoku tego biednego readbez IFS=i -r, i biednego $linebez jego zdrowych cytatów.
gniourf_gniourf

1
@gniourf_gniourf: Nie podoba mi się read -rnotacja. IMO, POSIX źle to zrozumiał; opcja powinna włączyć specjalne znaczenie dla końcowych ukośników odwrotnych, a nie wyłączyć je - aby istniejące skrypty (sprzed istnienia POSIX-a) nie uległy awarii, ponieważ -rzostały pominięte. Zauważam jednak, że był on częścią IEEE 1003.2 1992, który był najwcześniejszą wersją standardu powłoki i narzędzi POSIX, ale już wtedy został oznaczony jako dodatek, więc to dziwne o dawno minionych szansach. Nigdy nie miałem problemów, ponieważ mój kod nie używa -r; Muszę mieć szczęście. Zignoruj ​​mnie w tej sprawie.
Jonathan Leffler

1
@JathanathanLeffler Naprawdę zgadzam się, że -rpowinien być standardem. Zgadzam się, że jest to mało prawdopodobne w przypadkach, gdy nieużywanie go prowadzi do problemów. Chociaż uszkodzony kod jest uszkodzony. Moja edycja została po raz pierwszy uruchomiona przez tę kiepską $linezmienną, która bardzo pominęła swoje cytaty. Naprawiłem readczas, w którym tam byłem. Nie naprawiłem tego, echoponieważ taka zmiana jest wycofywana. :(.
gniourf_gniourf

1

Kod ${1:-/dev/stdin}po prostu zrozumie pierwszy argument, więc co powiesz na to.

ARGS='$*'
if [ -z "$*" ]; then
  ARGS='-'
fi
eval "cat -- $ARGS" | while read line
do
   echo "$line"
done

1

Żadna z tych odpowiedzi nie jest do przyjęcia. W szczególności zaakceptowana odpowiedź obsługuje tylko pierwszy parametr wiersza poleceń i ignoruje resztę. Program Perla, który próbuje emulować, obsługuje wszystkie parametry wiersza poleceń. Tak więc zaakceptowana odpowiedź nawet nie odpowiada na pytanie. Inne odpowiedzi używają rozszerzeń bash, dodają niepotrzebne polecenia „cat”, działają tylko w prostym przypadku echa wejścia do wyjścia lub są po prostu niepotrzebnie skomplikowane.

Muszę jednak podziękować im, ponieważ dali mi pomysły. Oto pełna odpowiedź:

#!/bin/sh

if [ $# = 0 ]
then
        DEFAULT_INPUT_FILE=/dev/stdin
else
        DEFAULT_INPUT_FILE=
fi

# Iterates over all parameters or /dev/stdin
for FILE in "$@" $DEFAULT_INPUT_FILE
do
        while IFS= read -r LINE
        do
                # Do whatever you want with LINE here.
                echo $LINE
        done < "$FILE"
done

1

Połączyłem wszystkie powyższe odpowiedzi i stworzyłem funkcję powłoki, która odpowiada moim potrzebom. To pochodzi z terminalu cygwin na moich 2 komputerach z systemem Windows10, gdzie miałem między nimi folder współdzielony. Muszę być w stanie poradzić sobie z następującymi kwestiami:

  • cat file.cpp | tx
  • tx < file.cpp
  • tx file.cpp

Jeśli określona nazwa pliku jest określona, ​​muszę użyć tej samej nazwy pliku podczas kopiowania. Tam, gdzie strumień danych wejściowych został potokowany, muszę wygenerować tymczasową nazwę pliku mającą godzinę, minutę i sekundy. Udostępniony główny folder ma podfoldery dni tygodnia. To jest do celów organizacyjnych.

Oto najlepszy skrypt dla moich potrzeb:

tx ()
{
  if [ $# -eq 0 ]; then
    local TMP=/tmp/tx.$(date +'%H%M%S')
    while IFS= read -r line; do
        echo "$line"
    done < /dev/stdin > $TMP
    cp $TMP //$OTHER/stargate/$(date +'%a')/
    rm -f $TMP
  else
    [ -r $1 ] && cp $1 //$OTHER/stargate/$(date +'%a')/ || echo "cannot read file"
  fi
}

Jeśli jest jakiś sposób na dalszą optymalizację tego, chciałbym wiedzieć.


0

Poniższe działa ze standardem sh(Testowane z dashDebianem) i jest dość czytelne, ale to kwestia gustu:

if [ -n "$1" ]; then
    cat "$1"
else
    cat
fi | commands_and_transformations

Szczegóły: jeśli pierwszy parametr nie jest pusty, to catten plik, w przeciwnym razie catstandardowe wejście. Następnie dane wyjściowe całej ifinstrukcji są przetwarzane przez commands_and_transformations.


IMHO najlepsza odpowiedź tak, ponieważ wskazuje na prawdziwego rozwiązania: cat "${1:--}" | any_command. Odczytywanie zmiennych do powłoki i ich echo może działać dla małych plików, ale nie skaluje się tak dobrze.
Andreas Spindler,

[ -n "$1" ]Mogą być uproszczone [ "$1" ].
agc

0

Ten jest łatwy w użyciu na terminalu:

$ echo '1\n2\n3\n' | while read -r; do echo $REPLY; done
1
2
3

-1

Co powiesz na

for line in `cat`; do
    something($line);
done

Dane wyjściowe catzostaną umieszczone w wierszu polecenia. Wiersz polecenia ma maksymalny rozmiar. Również to nie będzie czytać wiersz po wierszu, ale słowo po słowie.
Notinlist
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.