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!