So lesen Sie die Benutzereingaben zeilenweise bis Strg+D und schließen die Zeile ein, in der Strg+D eingegeben wurde

So lesen Sie die Benutzereingaben zeilenweise bis Strg+D und schließen die Zeile ein, in der Strg+D eingegeben wurde

Dieses Skript nimmt die Benutzereingabe Zeile für Zeile entgegen und führt sie myfunctionin 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+DWie kann ich mein Skript so umbauen, dass es nur mit der Zeile endet , in der Ctrl+Dgedrü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 readShell 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 1zum 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 -nist, 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 bashSkript 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 die IFSVariable. Ohne dies könnten wir keine Leerzeichen lesen. Ich verwende read -Nanstelle von read -n, da wir sonst keine Zeilenumbrüche erkennen könnten. Die -rOption bis readermöglicht es uns, Backslashs 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 zur breakAnweisung 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 der lineVariablen 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 >filein einem Terminal und tail -f filein einem anderen ausführen und dann eine Teilzeile in eingeben catund drücken, Ctrl+Dum zu sehen, was in der Ausgabe von geschieht tail.


Für ksh93Benutzer: 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, breakum 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 readShell-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 readjedoch schon .sysreadzsh

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^JbarWenn 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 zshkönnen Sie mit zshden eigenen erweiterten Zeileneditor von verwenden, um die Daten einzugeben und sie ^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 einen gleichwertigen sysreadAnsatz etwas Ähnliches erreichen, indem Sie ddfü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

verwandte Informationen