shell: read: Unterscheide zwischen EOF und Newline

7

Wie kann ich beim Lesen eines einzelnen Zeichens den Unterschied zwischen null <EOF>und erkennen \n?

Z.B:

f() { read -rn 1 -p "Enter a character: " char &&
      printf "\nYou entered '%s'\n" "$char"; }

Mit einem druckbaren Zeichen:

$ f
Enter a character: x
You entered 'x'

Beim Drücken von Enter:

$ f
Enter a character: 

You entered ''

Wenn Sie Ctrl+ drücken D:

$ f
Enter a character: ^D
You entered ''
$ 

Warum ist die Ausgabe in den letzten beiden Fällen gleich? Wie kann ich zwischen ihnen unterscheiden?

Gibt es eine andere Möglichkeit, dies in der POSIX-Shell zu tun bash?

Tom Hale
quelle

Antworten:

10

Mit read -n "$n"(keine POSIX-Funktion) und wenn stdin ein Endgerät ist, wird readdas Terminal aus dem icanonModus versetzt, da sonst readnur vollständige Zeilen angezeigt werden, die vom internen Zeileneditor der Terminalleitungsdisziplin zurückgegeben werden, und dann jeweils ein Byte gelesen werden, bis $nZeichen oder eine neue Zeile wurden gelesen (möglicherweise werden unerwartete Ergebnisse angezeigt, wenn ungültige Zeichen eingegeben werden).

Es liest bis zu $nZeichen aus einer Zeile. Sie müssen auch leer sein, $IFSdamit keine IFS-Zeichen von der Eingabe entfernt werden.

Da wir den icanonModus verlassen , ^Dist das nichts Besonderes mehr. Wenn Sie also drücken Ctrl+D, wird das ^DZeichen gelesen.

Sie würden eof nicht vom Endgerät sehen, wenn das Terminal nicht irgendwie getrennt wäre. Wenn stdin ein anderer Dateityp ist, wird möglicherweise eof angezeigt (z. B. : | IFS= read -rn 1; echo "$?"wenn stdin eine leere Pipe ist oder wenn stdin von umgeleitet wird /dev/null).

readgibt 0 zurück, wenn $nZeichen (Bytes, die nicht Teil gültiger Zeichen sind, die als 1 Zeichen gezählt werden) oder eine vollständige Zeile gelesen wurden.

In dem speziellen Fall, dass nur ein Zeichen angefordert wird:

if IFS= read -rn 1 var; then
  if [ "${#var}" -eq 0 ]; then
    echo an empty line was read
  else
    printf %s "${#var} character "
    (export LC_ALL=C; printf '%s\n' "made of ${#var} byte(s) was read")
  fi
else
  echo "EOF found"
fi

POSIXly zu machen ist ziemlich kompliziert.

Das wäre ungefähr so ​​(unter der Annahme eines ASCII-basierten Systems (im Gegensatz zu beispielsweise EBCDIC)):

readk() {
  REPLY= ret=1
  if [ -t 0 ]; then
    saved_settings=$(stty -g)
    stty -icanon min 1 time 0 icrnl
  fi
  while true; do
    code=$(dd bs=1 count=1 2> /dev/null | od -An -vto1 | tr -cd 0-7)
    [ -n "$code" ] || break
    case $code in
      000 | 012) ret=0; break;; # can't store NUL in variable anyway
      (*) REPLY=$REPLY$(printf "\\$code");;
    esac
    if expr " $REPLY" : ' .' > /dev/null; then
      ret=0
      break
    fi
  done
  if [ -t 0 ]; then
    stty "$saved_settings"
  fi
  return "$ret"
}

Beachten Sie, dass wir nur zurückkehren, wenn ein vollständiges Zeichen gelesen wurde. Wenn sich die Eingabe in der falschen Codierung befindet (anders als die Codierung des Gebietsschemas), z. B. wenn Ihr Terminal éin iso8859-1 (0xe9) codiert sendet , wenn wir UTF-8 (0xc3 0xa9) erwarten, können Sie so viele eingeben, éwie Sie möchten wird die Funktion nicht zurückkehren. bash's read -n1würde beim zweiten 0xe9 zurückkehren (und beide in der Variablen speichern), was ein etwas besseres Verhalten ist.

Wenn Sie auch ein ^CZeichen lesen möchten Ctrl+C(anstatt es Ihr Skript töten zu lassen; auch für ^Z, ^\...) oder ^S/ ^Qauf Ctrl+S/Q(anstelle der Flusskontrolle), können Sie -isig -ixonder sttyZeile ein hinzufügen . Beachten Sie, dass bash‚s read -n1es nicht tut entweder (es stellt auch isigwenn es aus).

Dadurch werden die tty-Einstellungen nicht wiederhergestellt, wenn das Skript beendet wird (z. B. wenn Sie drücken Ctrl+C. Sie könnten ein hinzufügen trap, dies würde jedoch möglicherweise andere traps im Skript überschreiben .

Sie können auch verwenden , zshstatt bash, wo read -k(die früher ksh93oder bash‚s read -n/-N) liest ein Zeichen aus dem Terminal und Griffe ^Dspeziell für sich allein (kehrt nicht Null , wenn das Zeichen eingegeben wird ) und nicht behandeln Newline.

if read -k k; then
  printf '1 character entered: %q\n' $k
fi
Stéphane Chazelas
quelle
1
Ich liebe deine Antworten, @ Stéphane Chazelas. Wenn wir den icanon-Modus verlassen und erfassen können ^D, warum können wir\n dann nicht erfassen ?
Tom Hale
@ TomHale, siehe Bearbeiten, wenn Sie verwenden können zsh. Für den POSIX-Ansatz können Sie sich im Fall 012 um Newline kümmern.
Stéphane Chazelas
Mit -icannoneinem ctrl-s(als ein Beispiel für nicht verwaltete Eingabe) wird der Code ausgesetzt. Dadurch wird die TTY blockiert, bis a ctrl-qausgegeben wird. Es gibt mehrere andere Schlüssel, die nicht gelesen werden, aber die tty beeinflussen, als zusätzliches Beispiel ctrl-C.
Isaac
1
Kein großes Problem, aber: Es ist ratsam, den Wert in zu ändern printf "\\$code", printf '%s' "\\$code"da der Wert von $codealles sein kann, wenn der -tTest fehlschlägt.
Isaac
Warum brauchen Sie, REPLY=$REPLY……wenn die Funktion sowieso ein Zeichen liest?
Isaac
2

Im f()Wechsel das %szu %q:

f() { read -rn 1 -p "Enter a character: " char && \
      printf "\nYou entered '%q'\n" "$char"; }
f;f

Ausgabe, wenn der Benutzer eine neue Zeile eingibt , dann ' Strg-D ':

Enter a character: 

You entered ''''
Enter a character: ^D
You entered '$'\004''

Von `man printf:

 %q       ARGUMENT is printed in a format that can be reused as shell input, 
          escaping non-printable characters with the proposed POSIX $'' syntax.
agc
quelle
Wie kann ich den Zeilenumbruch mit You entered '$'\012''dem aktuell angezeigten Nullzeichen anzeigen ?
Tom Hale
@ TomHale Die Newline beendet die Eingabe, sie ist nicht Teil davon
n.caillou
@ TomHale Sie können die Newline erfassen, wenn Sie hinzufügen-d ''
meuh
Nicht mit: f() { read -rd '' -n1 -p "Enter a character: " char && printf "\nYou entered: %q\n" "$char"; }Ich habe dazu eine separate Frage gestellt .
Tom Hale
2

Wenn Sie read -rn1in Bash laufen und drücken ^D, wird dies als wörtliches Steuerzeichen und nicht als EOF-Bedingung behandelt. Das Steuerzeichen ist beim Drucken einfach nicht sichtbar und wird daher nicht mit angezeigt printf "'%s'". Das Weiterleiten der Ausgabe an etwas wie od -cwürde es zeigen, ebenso wie printf "%q"die anderen bereits erwähnten Antworten.

Mit eigentlich nichts als Eingabe ist das Ergebnis anders, hier sogar leer mit printf "%q":

$ f()  { read -rn 1  x ; printf "%q\n" "$x"; }
$ printf "" | f
''

Die neue Zeile wird readhier aus zwei Gründen nicht zurückgegeben . Erstens ist es das Standardzeilentrennzeichen für Lesen und wird daher als Ausgabe zurückgegeben. Zweitens ist es auch Teil der Standardeinstellung IFSund readentfernt führende und nachfolgende Leerzeichen, wenn sie Teil von sind IFS.

Wir müssen also read -ddas Trennzeichen vom Standard ändern undIFS leer machen :

$ g() { IFS= read -rn 1 -d '' x ; printf "%q\n" "$x"; }
$ printf "\n" | g
$'\n'

read -d "" macht das Trennzeichen effektiv zum NUL-Byte, was bedeutet, dass dies immer noch nicht den Unterschied zwischen einer Eingabe von nichts und einer Eingabe eines NUL-Bytes erkennt:

$ printf "" | g
''
$ printf "\000" | g
''

Obwohl nichts als Eingabe verwendet wird, wird readfalse zurückgegeben, sodass wir dies überprüfen $?können.

ilkkachu
quelle
0
read -r var
status=$?
echo "\$var='$var':\$?=$status"

Die Fälle newline und Strg-D werden durch die Statusvariable unterschieden.

Bei Zeilenumbrüchen ist der Status wahr (0), während bei Strg-D der Status falsch ist (1).


quelle
Nicht mit -n 1. Der Status ist 0.
meuh
-n 1und Status 0 zeigt ein "\ n" an, das auf mysteriöse Weise entfernt wird .
Tom Hale