Wie lese ich aus einer Datei oder STDIN in Bash?


244

Das folgende Perl-Skript ( my.pl) kann entweder aus der Datei in den Befehlszeilenargumenten oder aus STDIN lesen:

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

perl my.plliest aus STDIN, perl my.pl a.txtliest aus a.txt. Das ist sehr praktisch.

Sie fragen sich, ob es in Bash ein Äquivalent gibt?

Antworten:


409

Die folgende Lösung liest aus einer Datei, wenn das Skript mit einem Dateinamen als erstem Parameter aufgerufen wird, $1andernfalls aus der Standardeingabe.

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

Die Ersetzung ${1:-...}erfolgt, $1wenn anders definiert, der Dateiname der Standardeingabe des eigenen Prozesses.


1
Schön, dass es funktioniert. Eine andere Frage ist, warum Sie ein Angebot dafür hinzufügen? "$ {1: - / proc / $ {$} / fd / 0}"
Dagang

15
Der Dateiname, den Sie in der Befehlszeile angeben, kann Leerzeichen enthalten.
Fritz G. Mehner

3
Gibt es einen Unterschied zwischen /proc/$$/fd/0und /dev/stdin? Mir ist aufgefallen, dass Letzteres häufiger vorkommt und einfacher aussieht.
Knowah

19
Es ist besser, -rIhren readBefehl zu ergänzen , damit er nicht versehentlich \ Zeichen frisst . Verwenden Sie while IFS= read -r linediese Option, um führende und nachfolgende Leerzeichen beizubehalten.
mklement0

1
@NeDark: Das ist merkwürdig; Ich habe gerade überprüft, ob es auf dieser Plattform funktioniert, auch wenn /bin/shSie eine andere Shell als bashoder verwenden sh.
mklement0

119

Die vielleicht einfachste Lösung besteht darin, stdin mit einem zusammenführenden Umleitungsoperator umzuleiten:

#!/bin/bash
less <&0

Stdin ist Dateideskriptor Null. Das Obige sendet die Eingabe, die an Ihr Bash-Skript weitergeleitet wird, an less's stdin.

Lesen Sie mehr über die Umleitung von Dateideskriptoren .


1
Ich wünschte, ich hätte mehr positive Stimmen für dich, ich habe jahrelang danach gesucht.
Marcus Downing

13
<&0In dieser Situation hat die Verwendung keinen Vorteil - Ihr Beispiel funktioniert mit oder ohne es gleich - anscheinend sehen Tools, die Sie innerhalb eines Bash-Skripts aufrufen, standardmäßig denselben Standard wie das Skript selbst (es sei denn, das Skript verwendet es zuerst).
mklement0

@ mkelement0 Wenn also ein Tool die Hälfte des Eingabepuffers liest, erhält das nächste von mir aufgerufene Tool den Rest?
Asad Saeeduddin

"Fehlender Dateiname (" weniger - Hilfe "für Hilfe)", wenn ich das tue ... Ubuntu 16.04
OmarOthman

5
Wo ist der Teil "oder aus Datei" in dieser Antwort?
Sebastian

84

Hier ist der einfachste Weg:

#!/bin/sh
cat -

Verwendung:

$ echo test | sh my_script.sh
test

Um der Variablen stdin zuzuweisen , können Sie Folgendes verwenden: STDIN=$(cat -)oder einfach nur, STDIN=$(cat)da kein Operator erforderlich ist (gemäß @ mklement0-Kommentar ).


Versuchen Sie das folgende Skript, um jede Zeile aus der Standardeingabe zu analysieren :

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

Um aus der Datei oder stdin zu lesen (wenn kein Argument vorhanden ist), können Sie es erweitern auf:

#!/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")

Anmerkungen:

- read -r- Behandeln Sie einen Backslash-Charakter nicht auf besondere Weise. Betrachten Sie jeden Backslash als Teil der Eingabezeile.

- Ohne Einstellung IFSwerden standardmäßig die Sequenzen von Spaceund Tabam Anfang und Ende der Zeilen ignoriert (getrimmt).

- Verwenden Sie printfstatt echo, um zu vermeiden, dass leere Zeilen gedruckt werden, wenn die Zeile aus einer einzelnen Zeile besteht -e, -noder -E. Es gibt jedoch eine Problemumgehung, mit env POSIXLY_CORRECT=1 echo "$line"der Ihre externe GNU ausgeführt wird, echodie sie unterstützt. Siehe: Wie kann ich "-e" wiedergeben?

Siehe: Wie lese ich stdin, wenn keine Argumente übergeben werden? bei Stackoverflow SE


Sie könnten vereinfachen [ "$1" ] && FILE=$1 || FILE="-"zu FILE=${1:--}. (Quibble: besser, alle Großbuchstaben Shell- Variablen zu vermeiden, um Namenskollisionen mit Umgebungsvariablen zu vermeiden .)
mklement0

Gern geschehen; eigentlich ${1:--} ist POSIX-konform, so dass es in allen POSIX-wie Muscheln funktionieren soll. Was in all diesen Shells nicht funktioniert, ist die Prozessersetzung ( <(...)); es wird zum Beispiel in bash, ksh, zsh funktionieren, aber nicht in dash. Es ist auch besser, -rIhren readBefehl zu ergänzen , damit er nicht versehentlich \ Zeichen frisst . Stellen Sie vor IFS= , um führende und nachfolgende Leerzeichen beizubehalten.
mklement0

4
Tatsächlich bricht Ihr Code immer noch aus folgenden Gründen echo: Wenn eine Zeile aus oder besteht -e, wird sie nicht angezeigt. Um dies zu beheben, müssen Sie Folgendes verwenden : . Ich habe es nicht in meine vorherige Bearbeitung aufgenommen. Zu oft werden meine Änderungen rückgängig gemacht, wenn ich diesen Fehler behebe . -n-Eprintfprintf '%s\n' "$line":(
gniourf_gniourf

1
Nein, es scheitert nicht. Und das --ist nutzlos, wenn das erste Argument ist'%s\n'
gniourf_gniourf

1
Ihre Antwort ist in Ordnung für mich (ich meine, es gibt keine Fehler oder unerwünschten Funktionen mehr, die mir bekannt sind) - obwohl nicht mehrere Argumente wie bei Perl behandelt werden. Wenn Sie mit mehreren Argumenten umgehen möchten, schreiben Sie am Ende die ausgezeichnete Antwort von Jonathan Leffler - in der Tat wäre Ihre Antwort besser, da Sie sie IFS=mit readund printfanstelle von verwenden würden echo. :).
gniourf_gniourf

19

Ich denke, das ist der einfache Weg:

$ 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
Dies entspricht nicht der Anforderung des Posters, entweder aus stdin oder einem Dateiargument zu lesen, sondern nur aus stdin.
Nash

2
Nashs gültige Einwand verlassen @ beiseite: readliest aus stdin standardmäßig , so gibt es keine Notwendigkeit für < /dev/stdin.
mklement0

13

Die echoLösung fügt immer dann neue Zeilen hinzu, wenn IFSder Eingabestream unterbrochen wird. Die Antwort von @ fgm kann ein wenig geändert werden:

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

Könnten Sie bitte erklären, was Sie unter "Echo-Lösung fügt neue Zeilen hinzu, wenn IFS den Eingabestream unterbricht" verstehen? Falls Sie sich auf readdas Verhalten bezogen haben: while read wird möglicherweise von den Zeichen in mehrere Token aufgeteilt. enthalten $IFS, gibt es nur ein einzelnes Token zurück, wenn Sie nur einen einzelnen Variablennamen angeben (aber standardmäßig werden Leerzeichen und führende und nachfolgende Leerzeichen abgeschnitten).
mklement0

@ mklement0 Ich stimme Ihnen zu 100% in Bezug auf das Verhalten von readund $IFS- echoselbst zu und füge neue Zeilen ohne -nFlag hinzu. "Das Dienstprogramm echo schreibt alle angegebenen Operanden, die durch einzelne Leerzeichen (` ') und gefolgt von einem Zeilenumbruchzeichen (`\ n') getrennt sind, in die Standardausgabe."
David Souther

Verstanden. Um die Perl-Schleife zu emulieren, müssen Sie jedoch das folgende Trailing \nhinzufügen echo: Perl's $_ enthält die Zeile, die \nvon der gelesenen Zeile endet , während Bashs readdies nicht tut. (Wie @gniourf_gniourf jedoch an anderer Stelle ausführt, ist es robuster, printf '%s\n'anstelle von echo) zu verwenden.
mklement0

8

Die Perl-Schleife in der Frage liest aus allen Dateinamenargumenten in der Befehlszeile oder aus der Standardeingabe, wenn keine Dateien angegeben sind. Die Antworten, die ich sehe, scheinen alle eine einzelne Datei oder Standardeingabe zu verarbeiten, wenn keine Datei angegeben ist.

Obwohl oft genau als UUOC (Useless Use of cat) verspottet , gibt es Zeiten, in denen catdas beste Werkzeug für den Job ist, und es ist fraglich, ob dies eines davon ist:

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

Der einzige Nachteil dabei ist, dass eine Pipeline erstellt wird, die in einer Sub-Shell ausgeführt wird, also Dinge wie Variablenzuweisungen in der while Schleife außerhalb der Pipeline nicht zugänglich sind. Der bashWeg dahin ist die Prozesssubstitution :

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

Dies verlässt die while Schleife in der Haupt-Shell ausgeführt, sodass auf in der Schleife festgelegte Variablen außerhalb der Schleife zugegriffen werden kann.


1
Hervorragender Punkt zu mehreren Dateien. Ich weiß nicht, wie sich die Ressourcen und die Leistung auswirken würden, aber wenn Sie nicht auf bash, ksh oder zsh sind und daher keine Prozessersetzung verwenden können, können Sie ein Here-Doc mit Befehlssubstitution versuchen (verteilt auf 3) Linien) >>EOF\n$(cat "$@")\nEOF. Schließlich ein Streitpunkt: while IFS= read -r lineist eine bessere Annäherung an das, was while (<>)in Perl funktioniert (behält führende und nachfolgende Leerzeichen bei - obwohl Perl auch die nachfolgenden Leerzeichen beibehält \n).
mklement0

4

Perls Verhalten mit dem im OP angegebenen Code kann kein oder mehrere Argumente annehmen. Wenn ein Argument ein einzelner Bindestrich -ist, wird dies als stdin verstanden. Außerdem ist es immer möglich, den Dateinamen mit zu haben $ARGV. Keine der bisher gegebenen Antworten ahmt Perls Verhalten in dieser Hinsicht wirklich nach. Hier ist eine reine Bash-Möglichkeit. Der Trick ist, execangemessen zu verwenden .

#!/bin/bash

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

Dateiname ist verfügbar in $1.

Wenn keine Argumente angegeben werden, setzen wir künstlich -den ersten Positionsparameter. Wir durchlaufen dann die Parameter. Ist dies kein Parameter -, leiten wir die Standardeingabe vom Dateinamen mit um exec. Wenn diese Umleitung erfolgreich ist, schleifen wir mit einer whileSchleife. Ich verwende die Standardvariable REPLY, und in diesem Fall müssen Sie nicht zurücksetzen IFS. Wenn Sie einen anderen Namen möchten, müssen Sie diesen zurücksetzen IFS(es sei denn, Sie möchten das natürlich nicht und wissen, was Sie tun):

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

2

Genauer...

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

2
Ich gehe davon aus, dass dies im Wesentlichen ein Kommentar zu stackoverflow.com/a/6980232/45375 ist , keine Antwort. Um den Kommentar explizit zu machen: Durch Hinzufügen IFS=und -r zum readBefehl wird sichergestellt, dass jede Zeile unverändert gelesen wird (einschließlich führender und nachfolgender Leerzeichen).
mklement0

2

Bitte versuchen Sie den folgenden Code:

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

1
Beachten Sie, dass dies auch in der geänderten Fassung nicht aus Standardeingaben oder aus mehreren Dateien gelesen werden kann, sodass die Frage nicht vollständig beantwortet wird. (Es ist auch überraschend, zwei Änderungen innerhalb von Minuten mehr als 3 Jahre nach der ersten
Jonathan Leffler

@ JonathanLeffler Entschuldigung, dass ich eine so alte (und nicht wirklich gute) Antwort bearbeitet habe… aber ich konnte es nicht ertragen, diese Armen readohne IFS=und -rund die Armen $lineohne ihre gesunden Zitate zu sehen.
gniourf_gniourf

1
@gniourf_gniourf: Ich mag die read -rNotation nicht. IMO, POSIX hat das falsch verstanden; Die Option sollte die spezielle Bedeutung für nachfolgende Backslashes aktivieren und nicht deaktivieren, damit vorhandene Skripte (von vor POSIX) nicht beschädigt werden, da die -rweggelassen wurden. Ich stelle jedoch fest, dass es Teil von IEEE 1003.2 1992 war, der frühesten Version des POSIX-Shell- und Utilities-Standards, aber es wurde bereits damals als Ergänzung markiert, sodass es um längst vergangene Möglichkeiten geht. Ich bin nie in Schwierigkeiten geraten, weil mein Code nicht verwendet wird -r. Ich muss Glück haben. Ignoriere mich dabei.
Jonathan Leffler

1
@ JonathanLeffler Ich stimme wirklich zu, dass -rdies Standard sein sollte. Ich bin damit einverstanden, dass dies in Fällen unwahrscheinlich ist, in denen die Nichtverwendung zu Problemen führt. Fehlerhafter Code ist jedoch fehlerhafter Code. Meine Bearbeitung wurde zuerst durch diese schlechte $lineVariable ausgelöst , die ihre Anführungszeichen stark verfehlt hat. Ich habe das behoben, readwährend ich dabei war. Ich habe das nicht behoben, echoweil dies die Art von Bearbeitung ist, die zurückgesetzt wird. :(.
gniourf_gniourf

1

Code ${1:-/dev/stdin}wird nur das erste Argument verstehen, also wie wäre es damit?

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

1

Ich finde keine dieser Antworten akzeptabel. Insbesondere behandelt die akzeptierte Antwort nur den ersten Befehlszeilenparameter und ignoriert den Rest. Das Perl-Programm, das es zu emulieren versucht, verarbeitet alle Befehlszeilenparameter. Die akzeptierte Antwort beantwortet also nicht einmal die Frage. Andere Antworten verwenden Bash-Erweiterungen, fügen unnötige 'cat'-Befehle hinzu, funktionieren nur für den einfachen Fall, dass Eingabe zu Ausgabe wiederholt wird, oder sind einfach unnötig kompliziert.

Ich muss ihnen jedoch etwas Anerkennung zollen, weil sie mir einige Ideen gegeben haben. Hier ist die vollständige Antwort:

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

Ich habe alle oben genannten Antworten kombiniert und eine Shell-Funktion erstellt, die meinen Anforderungen entspricht. Dies ist von einem Cygwin-Terminal meiner 2 Windows 10-Computer, auf dem sich ein freigegebener Ordner befand. Ich muss in der Lage sein, Folgendes zu handhaben:

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

Wenn ein bestimmter Dateiname angegeben ist, muss beim Kopieren derselbe Dateiname verwendet werden. Wenn der Eingabedatenstrom durchgeleitet wurde, muss ich einen temporären Dateinamen mit der Stunde, Minute und Sekunde generieren. Der freigegebene Hauptordner enthält Unterordner der Wochentage. Dies dient organisatorischen Zwecken.

Siehe, das ultimative Skript für meine Bedürfnisse:

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
}

Wenn es eine Möglichkeit gibt, dies weiter zu optimieren, würde ich gerne wissen.


0

Das Folgende funktioniert mit Standard sh(Getestet mit dashauf Debian) und ist gut lesbar, aber das ist Geschmackssache:

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

Details: Wenn der erste Parameter nicht leer ist, dann catdiese Datei, sonst catStandardeingabe. Dann wird die Ausgabe der gesamten ifAnweisung von der verarbeitet commands_and_transformations.


IMHO die beste Antwort also, weil es auf die wahre Lösung hinweist : cat "${1:--}" | any_command. Das Lesen und Wiedergeben von Shell-Variablen funktioniert möglicherweise für kleine Dateien, ist jedoch nicht so gut skalierbar.
Andreas Spindler

Das [ -n "$1" ]kann vereinfacht werden [ "$1" ].
Agc

0

Dieser ist einfach auf dem Terminal zu bedienen:

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

-1

Wie wäre es mit

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

Die Ausgabe von catwird in die Befehlszeile eingefügt. Die Befehlszeile hat eine maximale Größe. Auch dies wird nicht Zeile für Zeile, sondern Wort für Wort gelesen.
Notinlist
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.