Problem reading terminal response string from Zsh

Note: This posting is related to my posting at bash - Reading answer to control string sent to xterm - Stack Overflow , but I could get there a solution only for bash. I can use that solution, but for curiosity, I wonder, whether I could do this in Zsh as well.

The problem is to send a (Posix-) terminal query string to the terminal where the (interactive) shell is running, and to read the response from the terminal. For example, when we send Escape followed by the letter Z, the terminal is supposed to respond with the terminal ID. This response also starts with an Escape, followed by an arbitrary number of characters. Since I don't know the number of characters returned in advance, I have to accumulate them one by one.

This is the solution I achieved with bash, ask_tty.sh:

#!/bin/bash

str='' # Buffer for response
tty=$(tty)

# Send query string to terminal. Example: Esc Z queries for terminal id
echo -e '\e'${1:-Z}  >$tty

# Read response from terminal
while :
do
  read -rs -t 1 -n 1 <$tty
  if [[ -z $REPLY ]]
  then
    break
  fi
  str="${str}$REPLY"
done

# Output response without leading Esc
echo "${str#?}"

If I run in my terminal either ask_tty.sh Z or ask_tty.sh (because I made Z the default value), I get as response on stdout

[?63;1;2;4;6;9;15;22;29c

The actual reading of each character is done by read -rs -t 1 -n 1 <$tty.

I feel that for adapting this solution to Zsh, I have to change the bash read to an equivalent Zsh read, but I could not get it done: Either the command hangs, or it returns only the first character of the answer string. For instance, I tried read -rs -t -k and read -rs -t 1 -k 1.

How can I solve this in Zsh?

Update: I'm using Zsh 5.3 (given the evolution of Zsh, version might matter here), running on Cygwin.

Did you try:

read -rs -t 1 -q <$tty

According to the man page, this can not work. Quoting the man-page:

-q Read only one character from the terminal and set name to `y' if this character was `y' or `Y' and to `n' otherwise.

So, even if it would work, it would not return the answered character, but set my variable to y or n.

Interestingly, the actual effect is that I don't even get this result, but instead the script loops forever, always returning n. It doesn't seem to actually "consume" the character.

In bash you could simplify the logic as -t will timeout if a full line is not received

#!/bin/bash

str='' # Buffer for response
tty=$(tty)

# Send query string to terminal. Example: Esc Z queries for terminal id
echo -e '\e'${1:-Z}  >$tty

# Read response from terminal (200ms timeout empty delimiter)
read -rs -t 0.2 -d "" <$tty

# Output response without leading Esc
echo "Response: ${REPLY#?}"

Under zsh -t is just a timeout for the first character being available. Use return value of read to detect a timeout, as REPLY is not emptied on timeout:

#!/bin/zsh
str='' # Buffer for response
tty=$(tty)

# Send query string to terminal. Example: Esc Z queries for terminal id
echo -e '\e'${1:-Z}  >$tty

# Read response from terminal
while :
do
  read -rs -t 0.2 -k 1 <$tty || break
  str="${str}$REPLY"
done


# Output response without leading Esc
echo "Response: ${str#?}"

From a 1980's script, written by somebody else for Suns:

stty raw >/dev/tty
echo -n "$report" >/dev/tty
ch=`dd </dev/tty count=1 2>/dev/null`
stty cooked >/dev/tty

Where $report is your input string.

The above still works today in a ROXTERM and xfce4-terminal on Linux, and should work on other terminal emulators.

Also note the /dev/tty will use the current terminal without having to find it with tty

Andrew

1 Like

This does work too, indeed, though I don't fully understand the script: Why do we need the count parameter here. I see that we do need it, because when I set it to a higher value or omit it, the script hangs (probably waiting for input). But how does this parameter make it work?

I understand that with count=1, you tell dd to return one block only, where the default block size is 512 bytes. I can understand, how this works, when reading from a file, but how does dd know that it has finished reading? There is, I think, no EOF when reading from /dev/tty, and if there were an EOF condition, the count parameter would not be needed at all.

The assumption is that the terminal will send its entire response to your terminal query in a single burst of characters. That burst of characters will be read as one "block" by dd and with the directive count=1 it will quit after it has successfully read that single block. Without the count=1 , dd will continue reading blocks from the terminal until it hits an EOF condition, which won't happen until you type CTL-D (i.e., hold down the control key while you hit the "d" key).

1 Like