shell: read: différencier entre EOF et newline

En lisant un seul caractère, comment puis-je faire la différence entre le null <EOF> et le \n ?

Par exemple:

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

Avec un caractère imprimable:

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

Lorsque vous appuyez sur Entrée :

 $ f Enter a character: You entered '' 

Lorsque vous appuyez sur Ctrl + D :

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

Pourquoi la sortie est-elle la même dans les deux derniers cas? Comment puis-je distinguer entre eux?

Existe-t-il une autre façon de le faire dans POSIX shell vs bash ?

Avec read -n "$n" (et non une fonction POSIX), et si stdin est un terminal, read met le terminal hors mode icanon , sinon les lignes pleines ne seront renvoyées que par la ligne line line line éditeur, puis lit un octet à la fois jusqu'à ce que $n caractères ou une nouvelle ligne aient été lus (vous pouvez voir des résultats inattendus si des caractères invalides sont entrés).

Il lit jusqu'à $n caractère d'une ligne. Vous devrez également vider $IFS pour ne pas supprimer les caractères IFS de l'input.

Depuis que nous quittons le mode icanon , ^D n'est plus spécial. Donc, si vous appuyez sur Ctrl + D , le caractère ^D sera lu.

Vous ne verriez pas d'eof du terminal à less que le terminal soit déconnecté d'une manière ou d'une autre. Si stdin est un autre type de file, vous pouvez voir eof (comme dans : | IFS= read -rn 1; echo "$?" Où stdin est un tube vide, ou redirection stdin from /dev/null )

read renvoie 0 si $n caractères (les octets ne faisant pas partie des caractères valides étant comptés comme 1 caractère) ou une ligne pleine ont été lus.

Ainsi, dans le cas particulier d'un seul personnage demandé:

 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 

Le faire POSIXly est plutôt compliqué.

Ce serait quelque chose comme (en supposant un système basé sur ASCII (par opposition à EBCDIC par exemple)):

 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" } 

Notez que nous revenons seulement quand un caractère complet a été lu. Si l'input est dans le mauvais enencoding (différent de l'enencoding de la locale), par exemple si votre terminal envoie é encodé en iso8859-1 (0xe9) lorsque nous attendons UTF-8 (0xc3 0xa9), vous pouvez entrer autant d' é que vous aimez, la fonction ne reviendra pas. la read -n1 bash read -n1 reviendrait sur le second 0xe9 (et stocke les deux dans la variable) ce qui est un comportement légèrement meilleur.

Si vous vouliez aussi lire un caractère ^C en Ctrl + C (au lieu de le laisser tuer votre script, aussi pour ^Z , ^\ …), ou ^S / ^Q en Ctrl + S / Q contrôle de stream), vous pouvez append un -isig -ixon à la ligne stty . Notez que la read -n1 bash read -n1 ne le fait pas non plus (elle restaure même isig si elle était désactivée).

Cela ne restaurera pas les parameters tty si le script est tué (comme si vous appuyez sur Ctrl + C. Vous pouvez append un trap , mais cela pourrait replace les autres trap du script.

Vous pouvez également utiliser zsh au lieu de bash , où read -k (qui est antérieur à ksh93 ou bash read -n/-N ) lit un caractère du terminal et gère ^D par lui-même (renvoie non nul si ce caractère est entré ) et ne traite pas spécialement newline.

 if read -kk; then printf '1 character entered: %q\n' $k fi 

Dans f() changez le %s en %q :

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

Sortie, si l'user entre une nouvelle ligne , alors ' Ctrl-D ':

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

De `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. 

En fait, si vous exécutez read -rn1 dans Bash et que vous frappez ^D , il est traité comme le caractère de contrôle littéral, et non comme une condition EOF. Le caractère de contrôle n'est pas visible lors de l'printing, il n'apparaît donc pas avec printf "'%s'" . Le fait de sortinger la sortie vers quelque chose comme od -c le montrerait, tout comme printf "%q" quelles autres réponses ont déjà été mentionnées.

Avec en fait rien en input, le résultat est différent, ici vide même avec printf "%q" :

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

La nouvelle ligne n'est pas returnnée par read ici pour deux raisons. Tout d'abord, c'est le délimiteur de ligne par défaut de read, et donc renvoyé en sortie. Deuxièmement, il fait également partie de l' IFS par défaut, et read supprime les espaces de début et de fin s'ils font partie d' IFS .

Nous avons donc besoin de read -d pour changer le délimiteur par défaut et rendre IFS vide:

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

read -d "" rend le délimiteur effectivement l'octet NUL, ce qui signifie que cela ne fait toujours pas la différence entre une input de rien et une input d'un octet NUL:

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

Bien que rien ne soit en input, read renvoie false, donc nous pourrions vérifier $? pour détecter cela.

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

Les cas de nouvelle ligne et Ctrl-D sont distingués par la variable d'état.

Dans le cas d'une nouvelle ligne, le statut est vrai (0) tandis que lorsque Ctrl-D est donné, l'état est faux (1)