In shell scripting, you can manipulate the TTY state using the stty tool.
Firstly, stty -g produces a character string which snapshots the current state. Before doing anything else, capture the output of this and save it somewhere. Later you can pass that string as the only argument to stty to restore the TTY settings. This string does not require quoting; the POSIX standard requires stty -g to produce a representation that requires no quoting in shell scripts.
stty raw is one way to get into a raw mode in which character-at-a-time input is possible.
savetty=$(stty -g)
stty raw
...
stty $savetty
It may be a good idea to use the trap command to set up a handler which will restore the tty settings even if the script exits somewhere or is interrupted.
Now, suppose we have this stty dance wrapped around some Awk code. Without loss of generality, let's do that outside of Awk. How can we make Awk read one character at a time?
Awk can only read lines with its implicit scanning strategy or the getline operator. There is no getchar. Ah, but lines actually means records. And in GNU Awk, we have two tools at our disposal not in the POSIX standard:
The RS variable may consist of more than one character, in which case it is a regular expression.
The RT variable holds the piece of text that was matched as the record terminator.
Behold:
$ awk 'BEGIN { RS = "(.)" } { print NF, RT }'
How now brown cow.
0 H
0 o
0 w
0
0 n
0 o
0 w
0
0 b
0 r
0 o
0 w
0 n
0
0 c
0 o
0 w
0 .
If we use the regular expression (.) (match any character) as the record separator, we get empty records that contain no fields at all, and the character which terminated the empty record is available in RT in GNU Awk.
Unfortunately, this does not quite work. When we integrate it into a complete program:
#!/bin/sh
trap 'stty $ttysave' EXIT INT TERM
ttysave=$(stty -g)
stty raw -echo
awk 'BEGIN { RS = "(.)" }
RT ~ /q/ { exit }
{ printf("[%s]", RT) }'
this exhibits the problem that Gawk's record-delimiting regex machine is reading one character ahead. For instance, if we want to quit immediately by typing q, that q is not good enough. Even though a record could be delimited at that point, and RT could be set to q, Gawk invokes another read on the TTY to read a character, and only then does it dispatch the record.
Thus, we have to resort to something very ugly, like looping in a for or while loop and invoking the dd utility:
#!/bin/sh
trap 'stty $ttysave' EXIT INT TERM
ttysave=$(stty -g)
stty raw -echo
awk 'BEGIN { cmd = "dd bs=1 count=1 2> /dev/null"
for (;;)
{ cmd | getline ch
close(cmd)
if (ch == "q")
exit
printf("[%s]", ch) } }'