This is explained some in the trap(1P) man page and more in the Advanced Bash Scripting Guide, which explains that the --
is a Bash built-in, used to denote the "end of options" for a command. Since scripts using sh
are designed to be portable between other systems with it, bash behaves as if it were executed with --posix
, which changes some shell behavior to match the POSIX specification.
When a signal is "trapped", (e.g., EXIT
), it binds the first argument after the command to the signal, eval
-ing it when the signal is issued, and ignoring its normal behavior (except for EXIT
). So when trap 'echo something' EXIT
is run, then exit
, the shell will eval 'echo something'
prior to exiting. This would also work with a different signal, such as TERM
, which could be bound to, for example, a graceful exit function in a script. When trap -- EXIT
is run, the --
is interpreted as an "end of arguments", indicating to trap
that the signal is to be bound to null (''
) (since there were no flags prior to or after --
), indicating to the shell to ignore the signal (however, this doesn't work with EXIT, but works with other signals). Running trap -- 'echo something' EXIT
, however, will still eval 'echo something'
and exit upon exit
. As per the specification of trap
, the command -
by itself indicates that the shell will reset any traps for the signals specified, which is why it works in both sh
and Bash.
It might be more helpful if the doc pointed out that there's no such thing as an ASCII EOF, that the ASCII semantics for ^D is EOT, which is what the terminal driver supplies in canonical mode: it ends the current transmission, the read
. Programs interpret a 0-length read as EOF, because that's what EOF looks like on files that have that, but the terminal driver refusing to deliver character code 4 and instead swallowing it and terminating the read isn't always what you want.
That's what's going on here: control character semantics are part of canonical mode, the mode where the terminal driver buffers until it sees a character to which convention assigns a special meaning. This is true of EOT, BS, CR and a host of others (see stty -a
and man termios
for alll the gory details).
read -N
is an explicit order to just deliver the next N characters. To do that, the shell has to stop asking the terminal driver for canonical semantics.
By the way, EOF isn't actually a condition a terminal can set, or enter.
If you keep reading past eof on anything else, you'll keep getting the EOF indicator, but the only EOF the terminal driver can supply is a fake one—think about it—if the terminal driver actually delivered a real EOF, then the shell couldn't keep reading from it afterwards either. It's all the same terminal. Here:
#include <unistd.h>
#include <stdio.h>
char s[32];
int main(int c, char**v)
{
do {
c=read(0,s,sizeof s);
printf("%d,%.*s\n",c,c,s);
} while (c>=0);
}
try that on the terminal, you'll see that the terminal driver in canonical mode just interprets EOT to complete any outstanding read, and it buffers internally until it sees some canonical input terminator regardless of the read buffer size (type a line longer than 32 bytes).
The text that's confusing you¸
unless EOF is encountered
is referring to a real EOF.
Best Answer
With
read -n "$n"
(not a POSIX feature), and if stdin is a terminal device,read
puts the terminal out of theicanon
mode, as otherwiseread
would only see full lines as returned by the terminal line discipline internal line editor and then reads one byte at a time until$n
characters or a newline have been read (you may see unexpected results if invalid characters are entered).It reads up to
$n
character from one line. You'll also need to empty$IFS
for it not to strip IFS characters from the input.Since we leave the
icanon
mode,^D
is no longer special. So if you press Ctrl+D, the^D
character will be read.You wouldn't see eof from the terminal device unless the terminal is somehow disconnected. If stdin is another type of file, you may see eof (like in
: | IFS= read -rn 1; echo "$?"
where stdin is an empty pipe, or with redirecting stdin from/dev/null
)read
will return 0 if$n
characters (bytes not forming part of valid characters being counted as 1 character) or a full line have been read.So, in the special case of only one character being requested:
Doing it POSIXly is rather complicated.
That would be something like (assuming an ASCII-based (as opposed to EBCDIC for instance) system):
Note that we return only when a full character has been read. If the input is in the wrong encoding (different from the locale's encoding), for instance if your terminal sends
é
encoded in iso8859-1 (0xe9) when we expect UTF-8 (0xc3 0xa9), then you may enter as manyé
as you like, the function will not return.bash
'sread -n1
would return upon the second 0xe9 (and store both in the variable) which is a slightly better behaviour.If you also wanted to read a
^C
character upon Ctrl+C (instead of letting it kill your script; also for^Z
,^\
...), or^S
/^Q
upon Ctrl+S/Q (instead of flow control), you could add a-isig -ixon
to thestty
line. Note thatbash
'sread -n1
doesn't do it either (it even restoresisig
if it was off).That will not restore the tty settings if the script is killed (like if you press Ctrl+C. You could add a
trap
, but that would potentially override othertrap
s in the script.You could also use
zsh
instead ofbash
, whereread -k
(which predatesksh93
orbash
'sread -n/-N
) reads one character from the terminal and handles^D
by itself (returns non-zero if that character is entered) and doesn't treat newline specially.