
Dieses Skript nimmt die Benutzereingabe Zeile für Zeile entgegen und führt sie myfunction
in jeder Zeile aus.
#!/bin/bash
SENTENCE=""
while read word
do
myfunction $word"
done
echo $SENTENCE
Um die Eingabe abzubrechen, muss der Benutzer [ENTER]
und dann drücken Ctrl+D
.
Ctrl+D
Wie kann ich mein Skript so umbauen, dass es nur mit der Zeile endet , in der Ctrl+D
gedrückt wurde, und diese verarbeitet?
Antwort1
Dazu müssten Sie Zeichen für Zeichen und nicht Zeile für Zeile lesen.
Warum? Die Shell verwendet höchstwahrscheinlich die Standardfunktion der C-Bibliothek, read()
um die vom Benutzer eingegebenen Daten zu lesen, und diese Funktion gibt die Anzahl der tatsächlich gelesenen Bytes zurück. Wenn sie Null zurückgibt, bedeutet dies, dass ein EOF aufgetreten ist (siehe Handbuch read(2)
; man 2 read
). Beachten Sie, dass EOF kein Zeichen, sondern eine Bedingung ist, d. h. die Bedingung „es gibt nichts mehr zu lesen“.Ende der Datei.
Ctrl+Dsendet eineEnde-der-Übertragung-Zeichen
(EOT, ASCII-Zeichencode 4, $'\04'
in bash
) an den Terminaltreiber. Dies hat zur Folge, dass alles, was gesendet werden soll, an den wartenden read()
Aufruf der Shell gesendet wird.
Wenn Sie Ctrl+Dmitten in der Texteingabe einer Zeile drücken, wird alles, was Sie bisher eingegeben haben, an die Shell 1 gesendet . Das bedeutet, dass, wenn Sie
Ctrl+Dnach der Eingabe einer Zeile zweimal drücken, das erste Mal Daten sendet und das zweite MalNichts, und der read()
Aufruf gibt Null zurück, was die Shell als EOF interpretiert. Wenn Sie ebenso drücken und Enteranschließend Ctrl+D, erhält die Shell sofort EOF, da keine Daten zum Senden vorhanden waren.
Wie lässt sich also vermeiden, zweimal tippen zu müssen Ctrl+D?
Wie gesagt, lesen Sie einzelne Zeichen. Wenn Sie den in die read
Shell integrierten Befehl verwenden, verfügt dieser wahrscheinlich über einen Eingabepuffer und fordert Sie auf, read()
maximal so viele Zeichen aus dem Eingabestrom zu lesen (vielleicht 16 KB oder so). Dies bedeutet, dass die Shell eine Reihe von 16-KB-Eingabeblöcken erhält, gefolgt von einem Block, der möglicherweise kleiner als 16 KB ist, gefolgt von Nullbytes (EOF). Sobald das Ende der Eingabe (oder eine neue Zeile oder ein angegebenes Trennzeichen) erreicht ist, wird die Kontrolle an das Skript zurückgegeben.
Wenn Sie read -n 1
zum Lesen eines einzelnen Zeichens verwenden, nutzt die Shell beim Aufruf von einen Puffer von einem einzelnen Byte read()
, d. h. sie sitzt in einer engen Schleife und liest Zeichen für Zeichen, wobei sie nach jedem Zeichen die Kontrolle an das Shell-Skript zurückgibt.
Das einzige Problem dabei read -n
ist, dass das Terminal in den „Raw-Modus“ versetzt wird, was bedeutet, dass die Zeichen so gesendet werden, wie sie sind, ohne jegliche Interpretation. Wenn Sie beispielsweise drücken Ctrl+D, erhalten Sie ein wörtliches EOT-Zeichen in Ihrer Zeichenfolge. Das müssen wir also überprüfen. Dies hat auch den Nebeneffekt, dass der Benutzer die Zeile nicht bearbeiten kann, bevor er sie an das Skript sendet, beispielsweise durch Drücken von Backspaceoder durch Verwenden von Ctrl+W(zum Löschen des vorherigen Worts) oder Ctrl+U(zum Löschen bis zum Zeilenanfang).
Um es kurz zu machen:Nachfolgend sehen Sie die letzte Schleife, die Ihr
bash
Skript ausführen muss, um eine Eingabezeile zu lesen und dem Benutzer gleichzeitig zu ermöglichen, die Eingabe jederzeit durch Drücken von zu unterbrechen
Ctrl+D:
while true; do
line=''
while IFS= read -r -N 1 ch; do
case "$ch" in
$'\04') got_eot=1 ;&
$'\n') break ;;
*) line="$line$ch" ;;
esac
done
printf 'line: "%s"\n' "$line"
if (( got_eot )); then
break
fi
done
Ohne hier zu sehr ins Detail zu gehen:
IFS=
löscht dieIFS
Variable. Ohne dies könnten wir keine Leerzeichen lesen. Ich verwenderead -N
anstelle vonread -n
, da wir sonst keine Zeilenumbrüche erkennen könnten. Die-r
Option bisread
ermöglicht es uns, Backslashs richtig zu lesen.Die
case
Anweisung wirkt auf jedes gelesene Zeichen ($ch
). Wenn ein EOT ($'\04'
) erkannt wird, wird esgot_eot
auf 1 gesetzt und fällt dann zurbreak
Anweisung durch, die es aus der inneren Schleife herausholt. Wenn ein Newline ($'\n'
) erkannt wird, bricht es einfach aus der inneren Schleife aus. Andernfalls fügt es das Zeichen am Ende derline
Variablen hinzu.Nach der Schleife wird die Zeile in die Standardausgabe gedruckt. Hier rufen Sie Ihr Skript oder Ihre Funktion auf, die verwendet
"$line"
. Wenn wir hierher durch Erkennen eines EOT gelangt sind, verlassen wir die äußerste Schleife.
1 Sie können dies testen, indem Sie cat >file
in einem Terminal und tail -f file
in einem anderen ausführen und dann eine Teilzeile in eingeben
cat
und drücken, Ctrl+Dum zu sehen, was in der Ausgabe von geschieht tail
.
Für ksh93
Benutzer: Die obige Schleife liest ein Wagenrücklaufzeichen statt eines Zeilenumbruchzeichens in ksh93
, was bedeutet, dass der Test für $'\n'
in einen Test für geändert werden muss $'\r'
. Die Shell zeigt diese auch als an ^M
.
So umgehen Sie dieses Problem:
stty_saved="$( stty -g )" stty -echoctl #die Schleife geht hierhin, wobei $'\n' durch $'\r' ersetzt wird stty "$stty_saved"
Möglicherweise möchten Sie auch direkt vor dem explizit eine neue Zeile ausgeben, break
um genau dasselbe Verhalten wie in zu erhalten bash
.
Antwort2
Im Standardmodus des Terminalgeräts read()
würde der Systemaufruf (wenn er mit einem ausreichend großen Puffer aufgerufen wird) ganze Zeilen anzeigen. Die einzigen Fälle, in denen die gelesenen Daten nicht mit einem Zeilenumbruchzeichen enden, sind, wenn Sie drücken Ctrl-D.
In meinen Tests (unter Linux, FreeBSD und Solaris) read()
ergibt ein einzelnes immer nur eine einzige Zeile, selbst wenn der Benutzer zum Zeitpunkt des read()
Aufrufs bereits mehr eingegeben hat. Der einzige Fall, in dem die gelesenen Daten mehr als eine Zeile enthalten könnten, wäre, wenn der Benutzer eine neue Zeile als Ctrl+VCtrl+J(das wörtliche nächste Zeichen gefolgt von einem wörtlichen Zeilenumbruchzeichen (im Gegensatz zu einem Wagenrücklauf, der in einen Zeilenumbruch umgewandelt wird, wenn Sie drücken Enter)) eingibt.
Das read
Shell-Builtin liest die Eingabe jedoch Byte für Byte, bis es ein Newline-Zeichen oder ein Dateiende sieht. DasEnde der Dateiwäre, wenn read(0, buf, 1)
0 zurückgegeben wird, was nur passieren kann, wenn Sie Ctrl-Dauf eine leere Zeile drücken.
Hier möchten Sie große Lesevorgänge durchführen und erkennen, Ctrl-Dwenn die Eingabe nicht mit einem Zeilenumbruchzeichen endet.
Mit dem integrierten Feature ist dies nicht möglich , mit dem integrierten Feature von read
jedoch schon .sysread
zsh
Wenn Sie die Eingaben des Benutzers berücksichtigen möchten ^V^J
:
#! /bin/zsh -
zmodload zsh/system # for sysread
myfunction() printf 'Got: <%s>\n' "$1"
lines=('')
while (($#lines)); do
if (($#lines == 1)) && [[ $lines[1] == '' ]]; then
sysread
lines=("${(@f)REPLY}") # split on newline
continue
fi
# pop one line
line=$lines[1]
lines[1]=()
myfunction "$line"
done
foo^V^Jbar
Wenn Sie es als einen einzelnen Datensatz (mit eingebetteter neuer Zeile) betrachten möchten , d. h. davon ausgehen, dass jeder read()
einen Datensatz zurückgibt:
#! /bin/zsh -
zmodload zsh/system # for sysread
myfunction() printf 'Got: <%s>\n' "$1"
finished=false
while ! $finished && sysread line; do
if [[ $line = *$'\n' ]]; then
line=${line%?} # strip the newline
else
finished=true
fi
myfunction "$line"
done
Alternativ zsh
können Sie mit zsh
den eigenen erweiterten Zeileneditor von verwenden, um die Daten einzugeben und sie ^D
dort einem Widget zuzuordnen, das das Ende der Eingabe signalisiert:
#! /bin/zsh -
myfunction() printf 'Got: <%s>\n' "$1"
finished=false
finish() {
finished=true
zle .accept-line
}
zle -N finish
bindkey '^D' finish
while ! $finished && line= && vared line; do
myfunction "$line"
done
Mit bash
oder anderen POSIX-Shells können Sie für einen gleichwertigen sysread
Ansatz etwas Ähnliches erreichen, indem Sie dd
für die read()
Systemaufrufe Folgendes verwenden:
#! /bin/sh -
sysread() {
# add a . to preserve the trailing newlines
REPLY=$(dd bs=8192 count=1 2> /dev/null; echo .)
REPLY=${REPLY%?} # strip the .
[ -n "$REPLY" ]
}
myfunction() { printf 'Got: <%s>\n' "$1"; }
nl='
'
finished=false
while ! "$finished" && sysread; do
case $REPLY in
(*"$nl") line=${REPLY%?};; # strip the newline
(*) line=$REPLY finished=true
esac
myfunction "$line"
done
Antwort3
Mir ist nicht ganz klar, was Sie verlangen, aber wenn Sie möchten, dass der Benutzer mehrere Zeilen eingeben und dann alle Zeilen als Ganzes verarbeiten kann, können Sie verwenden mapfile
. Es nimmt Benutzereingaben entgegen, bis EOF erreicht wird, und gibt dann ein Array zurück, wobei jede Zeile ein Element im Array ist.
PROGRAMM.sh
#!/bin/bash
myfunction () {
echo "$@"
}
SENTANCE=''
echo "Enter your input, press ctrl+D when finished"
mapfile input #this takes user input until they terminate with ctrl+D
n=0
for line in "${input[@]}"
do
((n++))
SENTANCE+="\n$n\t$(myfunction $line)"
done
echo -e "$SENTANCE"
Beispiel
$: bash PROGRAM.sh
Enter your input, press ctrl+D when finished
this is line 1
this is line 2
this is not line 10
# I pushed ctrl+d here
1 this is line 1
2 this is line 2
3 this is not line 10