Multiple Read
read
is the shell built-in function capable of taking standard input or files. The common usage are usually in a form of:
- prompting user input
- read file
- read a pipe input
The painful problem comes when you use any of the 2 and above usages simultaneously. Here's an example:
#!/bin/sh
old_IFS="$IFS"
while IFS='' read -r line || [ -n "$line" ]; do
[ ! -z "$line" ] || continue
# do something ...
# request another read from user prompt
while true; do
1>&2 printf "Enter password: "
1>&2 stty -echo
read ret
1>&2 stty echo
1>&2 printf "\n"
# verify ret...
done
# continue do something ...
done < /path/to/file
exit_trap() {
stty echo
}
trap exit_trap exit
Notice the above where read
is first used for reading a file, then during the process, you use read
again to get user input for password. In modern terminals, this problem is mitigated automatically. In system console or older terminal however, it is a mess.
You will immediately notice the read ret
will fail miserably by not waiting for user input and stty
failed badly with standard input: Inappropriate ioctl for device
. This is mainly because read
has multiple inputs and it doesn't know what to do.
Hence, you need to specify the read
input carefully using pipe or redirect. Something like:
read ret < /dev/something
Know Your Console
You definitely execute your script in some form of console, be it automated or manual. First, you have to find out whether you're on system level (e.g. grub, initramfs stage etc) or normal terminal. There are generally 2 types of terminal devices:
/dev/console
- system level console, usually on system level configurations like grubs, operating system startup/initialization stage./dev/tty
- current terminal console, usually used inside a fully booted operating system.
Pipe the Read Instruction Clearly
Next is to instruct the existing busy read to take input with the correct device clearly. We use pipe or redirect to make it works. If you're reading from user input via console, then pipe it with your identified console. Example, for /dev/tty
:
read ret < /dev/tty
stty -F /dev/tty -echo
If we amend the script in the example above, we get:
#!/bin/sh
old_IFS="$IFS"
while IFS='' read -r line || [ -n "$line" ]; do
[ ! -z "$line" ] || continue
# do something ...
# request another read from user prompt
while true; do
1>&2 printf "Enter password: "
1>&2 stty -F /dev/tty -echo
read ret < /dev/tty
1>&2 stty -F /dev/tty echo
1>&2 printf "\n"
# verify ret...
done
# continue do something ...
done < /path/to/file
exit_trap() {
# do other graceful cleanup
[ -F /dev/tty ] && stty -F /dev/tty echo
}
trap exit_trap exit
Why trap has a file existence safety check
Since we trap all the exit
signals with exit_trap
handler, there are situation where you might not need to configure things when it does not exist. After all, we are trapping all sort of exit signals.
A good example would be writing a script usable across both system-level boot-up and in conventional terminal. In this scenario, your exit_trap
must be smart enough to identify the device existence before passing it to stty
for configurations. Otherwise, stty
will throw a nasty error output back to you. Here's the handler for the example:
exit_trap() {
# do other graceful cleanup
[ -F /dev/console ] && stty -F /dev/console echo
[ -F /dev/tty ] && stty -F /dev/tty echo
}
trap exit_trap exit
That's all about careful read
input handling. Be careful next time!