6

Can I use read to capture the \n \012 or newline character?

Define test function:

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

Run the function, press Enter:

$ f;
Enter a character: 

You entered: ''

Hmmm. It's a null string.

How do I get my expected output:

$ f;
Enter a character:

You entered: $'\012'
$ 

I want the same method to be able to capture ^D or \004.

If read can't do it, what is the work around?

  • To do this in POSIX sh, see here. – Tom Hale Aug 1 '17 at 15:27
  • 2
    You need to prefix the read into a variable with IFS= – meuh Aug 1 '17 at 16:19
2

To read 1 character, use -N instead, which reads one character always and doesn't do $IFS processing:

read -rN1 var

read -rn1 reads one record up to one character and still does $IFS processing (and newline is in the default value of $IFS which explains why you get an empty result even though you read NUL-delimited records). You'd use it instead to limit the length of the record you read.

In your case, with NUL-delimited records (with -d ''), IFS= read -d '' -rn1 var would work the same as bash cannot store a NUL character in its variables anyway, so printf '\0' | read -rN1 var would leave $var empty and return a non-zero exit status.

To be able to read arbitrary characters including NUL, you'd use the zsh shell instead where the syntax is:

read -k1 'var?Enter a character: '

(no need for -r or IFS= there. However note that read -k reads from the terminal (k is for key; zsh's -k option predates bash's and even ksh93's -N by decades). To read from stdin, use read -u0 -k1).

Example (here pressing Ctrl+Space to enter a NUL character):

$ read -k1 'var?Enter a character: '
Enter a character: ^@
$ printf '%q\n' $var
$'\0'

Note that to be able to read a character, read may have to read more than one byte. If the input starts with the first byte of multi-byte character, it will read at least one more byte, so you could end up with $var containing something that the shell considers having a length greater than 1 if the input contains byte sequences not forming valid characters.

For instance in a UTF-8 locale:

$ printf '\xfc\x80\x80\x80\x80XYZ' | bash -c 'read -rN1 a; printf "<%q>\n" "$a" "${#a}"; wc -c'
<$'\374\200\200\200\200X'>
<6>
2
$ printf '\xfc\x80\x80\x80\x80XYZ' | zsh -c 'read -u0 -k1 a; printf "<%q>\n" $a $#a; wc -c'
<$'\374'$'\200'$'\200'$'\200'$'\200'X>
<6>
2

In UTF-8, 0xFC is the first byte of a 6-byte long character, the 5 other ones meant to have the 8th bit set and the 7th bit unset, however we provide only 4. read still reads that extra X to try and find the end of the character which ends up in $var along with those 5 bytes that don't form a valid character and end up being counted as one character each.

3

bash builtin read can do this:

read -rd $'\0' -p ... -n 1
printf '\nYou typed ASCII %02x\n' "'$REPLY"

(this piece of code won't work with multi-byte characters)

Notice how I didn't put what is read in variable char as you did. This is because doing so would remove the characters in IFS from char. With a standard IFS, you wouldn't be able to differentiate between space, tab and newline.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.