So lesen Sie die Benutzereingabe Zeile für Zeile bis Strg + D und geben die Zeile an, in die Strg + D eingegeben wurde

8

Dieses Skript übernimmt die Benutzereingabe Zeile für Zeile und wird myfunctionin jeder Zeile ausgeführt

#!/bin/bash
SENTENCE=""

while read word
do
    myfunction $word"
done
echo $SENTENCE

Um die Eingabe zu stoppen, muss der Benutzer [ENTER]und dann drücken Ctrl+D.

Wie kann ich mein Skript neu erstellen, um nur mit Ctrl+Dder Zeile zu enden und diese zu verarbeiten, in Ctrl+Dder gedrückt wurde?

user123456
quelle
Was ist in "myfunction" enthalten? Ich führe Ihr Beispiel aus, ich habe ein einfaches verwendet function myfunction { echo "you pressed $1" ; }; und sobald ich die Steuerung drücke, wird die D-Schleife beendet echo $sentenceund das Skript beendet.
George Vasiliou

Antworten:

5

Dazu müssten Sie Zeichen für Zeichen lesen, nicht Zeile für Zeile.

Warum? Die Shell verwendet sehr wahrscheinlich die Standardfunktion der C-Bibliothek read() , um die Daten zu lesen, die der Benutzer eingibt, und diese Funktion gibt die Anzahl der tatsächlich gelesenen Bytes zurück. Wenn es Null zurückgibt, bedeutet dies, dass EOF aufgetreten ist (siehe read(2)Handbuch; man 2 read). Beachten Sie, dass EOF kein Zeichen, sondern eine Bedingung ist, dh die Bedingung "Es ist nichts mehr zu lesen", Dateiende .

Ctrl+Dsendet ein Übertragungsende-Zeichen (EOT, ASCII-Zeichencode 4, $'\04'in bash) an den Terminaltreiber. Dies hat den Effekt, dass alles gesendet wird, was an den wartenden read()Anruf der Shell gesendet werden soll.

Wenn Sie nach der Ctrl+DHälfte der Eingabe des Textes in einer Zeile drücken , wird alles, was Sie bisher eingegeben haben, an die Shell 1 gesendet . Dies bedeutet, dass, wenn Sie Ctrl+Dzweimal eingeben, nachdem Sie etwas in eine Zeile eingegeben haben, der erste einige Daten sendet und der zweite nichts sendet und der read()Aufruf Null zurückgibt und die Shell dies als EOF interpretiert. Wenn Sie Entergefolgt von drücken Ctrl+D, erhält die Shell sofort EOF, da keine Daten zum Senden vorhanden waren.

Wie kann man also vermeiden, Ctrl+Dzweimal tippen zu müssen ?

Wie gesagt, lesen Sie einzelne Zeichen. Wenn Sie den in die readShell integrierten Befehl verwenden, verfügt dieser wahrscheinlich über einen Eingabepuffer und fordert Sie read()auf, maximal so viele Zeichen aus dem Eingabestream zu lesen (möglicherweise 16 KB oder so). Dies bedeutet, dass die Shell eine Reihe von 16-KB-Eingabestücken erhält, gefolgt von einem Block, der weniger als 16 KB groß sein kann, gefolgt von Null-Bytes (EOF). Sobald das Ende der Eingabe (oder eine neue Zeile oder ein angegebenes Trennzeichen) erreicht ist, wird die Steuerung an das Skript zurückgegeben.

Wenn Sie read -n 1ein einzelnes Zeichen lesen, verwendet die Shell beim Aufruf von einen Puffer mit einem einzelnen Byte read(), dh sie befindet sich in einer engen Schleife und liest Zeichen für Zeichen und gibt nach jedem Zeichen die Kontrolle an das Shell-Skript zurück.

Das einzige Problem dabei read -nist, dass das Terminal auf "Raw-Modus" gesetzt wird, was bedeutet, dass Zeichen so gesendet werden, wie sie ohne Interpretation sind. Wenn Sie beispielsweise drücken Ctrl+D, wird in Ihrer Zeichenfolge ein wörtliches EOT-Zeichen angezeigt. Also müssen wir das überprüfen. Dies hat auch den Nebeneffekt, dass der Benutzer die Zeile nicht bearbeiten kann, bevor er sie an das Skript sendet, z. B. durch Drücken Backspaceoder Verwenden von Ctrl+W(zum Löschen des vorherigen Wortes) oder Ctrl+U(zum Löschen am Zeilenanfang). .

Um es kurz zu machen: Das Folgende ist die letzte Schleife, die Ihr bashSkript ausführen muss, um eine Eingabezeile zu lesen, während der Benutzer die Eingabe jederzeit durch Drücken von unterbrechen kann 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 zu sehr ins Detail zu gehen:

  • IFS=löscht die IFSVariable. Ohne dies könnten wir keine Leerzeichen lesen. Ich benutze read -Nstattdessen read -n, sonst könnten wir keine Zeilenumbrüche erkennen. Die -rOption readermöglicht es uns, Backslashes richtig zu lesen.

  • Die caseAnweisung wirkt auf jedes gelesene Zeichen ( $ch). Wenn ein EOT ( $'\04') erkannt wird, wird es got_eotauf 1 gesetzt und fällt dann zu der breakAnweisung durch, die es aus der inneren Schleife herausholt. Wenn ein Zeilenumbruch ( $'\n') erkannt wird, bricht er einfach aus der inneren Schleife aus. Andernfalls wird das Zeichen am Ende der lineVariablen hinzugefügt.

  • Nach der Schleife wird die Zeile auf die Standardausgabe gedruckt. Hier rufen Sie Ihr Skript oder Ihre Funktion auf "$line". Wenn wir durch Erkennen eines EOT hierher gekommen sind, verlassen wir die äußerste Schleife.

1 Sie können dies testen, indem Sie cat >filein einem Terminal und tail -f filein einem anderen laufen und dann eine Teilzeile in das eingeben catund drücken, um Ctrl+Dzu sehen, was in der Ausgabe von passiert tail.


Für ksh93Benutzer: In der obigen Schleife wird ein Wagenrücklaufzeichen anstelle eines Zeilenumbruchzeichens gelesen. Dies ksh93bedeutet, dass der Test für $'\n'in einen Test für geändert werden muss $'\r'. Die Shell zeigt diese auch als an ^M.

Um dies zu umgehen:

stty_saved = "$ (stty -g)"
stty -echoctl

# Die Schleife geht hierher, wobei $ '\ n' durch $ '\ r' ersetzt wird.

stty "$ stty_saved"

Möglicherweise möchten Sie auch eine neue Zeile explizit kurz vor dem ausgeben break, um genau das gleiche Verhalten wie in zu erhalten bash.

Kusalananda
quelle
1
Es sollte jedoch beachtet werden, dass der Benutzer bei diesem Ansatz read -Nden Backslash / Strg-W / Strg-U nicht verwenden kann, um den Text zu bearbeiten, oder Strg-V, um Spezial einzugeben , da das tty-Gerät den kanonischen Modus verlässt Figuren.
Stéphane Chazelas
@ StéphaneChazelas Danke. Ja, das werde ich in meiner Antwort vermerken.
Kusalananda
1

Im Standardmodus des Endgeräts würde der read()Systemaufruf (wenn er mit einem ausreichend großen Puffer aufgerufen wird) zu vollen Leitungen führen. Das einzige Mal, wenn die gelesenen Daten nicht mit einem Zeilenumbruch enden, ist das Drücken von Ctrl-D.

In meinen Tests (unter Linux, FreeBSD und Solaris) read()ergibt eine einzelne immer nur eine einzelne Zeile, selbst wenn der Benutzer zum Zeitpunkt des Aufrufs mehr eingegeben hat read(). Der einzige Fall , in dem die gelesenen Daten mehr als eine Zeile enthalten könnten , wäre, wenn der Benutzer eine neue Zeile als eintritt Ctrl+VCtrl+J(die wörtlichen-nächsten Zeichen durch ein wörtliches Newline - Zeichen gefolgt (im Gegensatz zu einem Wagenrücklauf im Gegensatz zu Newline konvertierten , wenn Sie drücken Enter)) .

Die readeingebaute Shell liest die Eingabe jedoch byteweise, bis ein Zeilenumbruchzeichen oder ein Dateiende angezeigt wird. Dieses Dateiende ist , 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 feststellen, Ctrl-Dwann die Eingabe nicht mit einem Zeilenumbruch endet.

Sie können das nicht mit dem readeingebauten tun, aber Sie könnten es mit dem sysreadeingebauten von tun zsh.

Wenn Sie den Benutzer berücksichtigen möchten, der Folgendes eingibt ^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

Wenn Sie foo^V^Jbareinen einzelnen Datensatz (mit einem eingebetteten Zeilenumbruch) betrachten möchten, wird davon ausgegangen, 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 können zshSie mit zshden eigenen erweiterten Zeileneditor verwenden, um die Daten einzugeben und ^Ddort 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 bashoder anderen POSIX-Shells können Sie für ein Äquivalent des sysreadAnsatzes etwas Annäherendes tun, indem ddSie die read()Systemaufrufe ausführen:

#! /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
Stéphane Chazelas
quelle
Ich habe immer gedacht zsh, dass ich unter "Feature Bloat" leide, aber es stellt sich heraus, dass einige der Features manchmal recht praktisch zu sein scheinen. Über das ddim shSkript: 8192Können Sie kommentieren, warum Sie Bytes (8 KB) lesen ?
Kusalananda
1
@Kusalananda, 8192 wird sysreadstandardmäßig von zsh verwendet (auch das dort unterstützte Maximum). Welche Funktion von zshfinden Sie nicht nützlich? Beachten Sie, dass sich zshCode auch in verschiedenen dynamischen Modulen befindet, sodass das Aufblähen ihn nicht so stark beeinflusst wie andere Shells wie ksh93 oder bash.
Stéphane Chazelas
Vielen Dank. Nein, ich mag nur einfache Muscheln. Es gibt einfach zu viele Dinge, zum Beispiel mit Fertigstellungen und anderen "magischen" Dingen, die ich nicht vollständig verstehe . Es ist also nur ein weiterer Fall, Angst vor dem Unbekannten zu haben, nehme ich an. Persönliche Meinung natürlich. Ref , Ref
Kusalananda
0

Ich bin mir nicht ganz sicher, wonach Sie fragen, aber wenn Sie möchten, dass der Benutzer mehrere Zeilen eingeben und dann alle Zeilen als Ganzes verarbeiten kann, können Sie sie verwenden mapfile. Es nimmt Benutzereingaben auf, bis EOF angetroffen wird, und gibt dann ein Array zurück, wobei jede Zeile ein Element im Array ist.

SENTANCE=''
echo "Enter your input, press ctrl+D when finished"
mapfile input   #this takes user input until they terminate with ctrl+D 
for line in "${input[@]}
do 
    myfunction $line
done
echo $SENTANCE
jeffpkamp
quelle