When writing a shell script that might be used in a pipeline, it can
be useful to bypass the rerouting of stdin
and stdout
to still
interact directly with the user on the terminal. Let’s take a simple
script that lets you pick one of the lines from stdin by number and
then prints it to stdout.
Our first, not working, attempt looks like this:
#!/bin/sh
# select.sh
while read -r line; do
if [ -z "$lines" ]; then
lines="$line"
else
lines="$lines\n$line"
fi
echo "$line"
done
printf "Line number> "
read -r selection
line_num=0
echo $lines | while read -r line; do
line_num=$(( line_num + 1 ))
if [ "$line_num" -eq "$selection" ]; then
echo "$line"
fi
done
If you try and run this script you’ll notice you’re never prompted for input.
$ echo '1 2 3
> 4 5 6
> 7 8 9' | sh select.sh
Line number> select.sh: 17: [: Illegal number:
# more output
The trick is to explicitly read input from /dev/tty
by redirecting
stdin
when doing read
:
#!/bin/sh
# select.sh
while read -r line; do
if [ -z "$lines" ]; then
lines="$line"
else
lines="$lines\n$line"
fi
echo "$line"
done
printf "Line number> "
read -r selection </dev/tty
line_num=0
echo $lines | while read -r line; do
line_num=$(( line_num + 1 ))
if [ "$line_num" -eq "$selection" ]; then
echo "$line"
fi
done
This works great now:
$ echo '1 2 3
4 5 6
7 8 9' | sh select.sh
1 2 3
4 5 6
7 8 9
Line number> 2
4 5 6
But that isn’t actually that useful. You probably want to pipe that into another command:
$ echo '1 2 3
4 5 6
7 8 9' | sh select.sh | wc
When you run it, though, it just hangs with no output. That’s because
all the echos are now going to the pipe instead of to the terminal!
You can use stderr
(aka file descriptor 2) for this:
#!/bin/sh
# select.sh
while read -r line; do
if [ -z "$lines" ]; then
lines="$line"
else
lines="$lines\n$line"
fi
echo "$line"
done >&2
printf "Line number> " >&2
read -r selection </dev/tty
line_num=0
echo $lines | while read -r line; do
line_num=$(( line_num + 1 ))
if [ "$line_num" -eq "$selection" ]; then
echo "$line"
fi
done
Now when you run it, it behaves as expected:
$ echo '1 2 3
4 5 6
7 8 9' | sh select.sh | wc
1 2 3
4 5 6
7 8 9
Line number> 2
1 3 6
Hopefully you’ll find this helpful when you do some shell scripting.