• Alexios
  • Linux
  • recipes
  • bash

I use emacs a lot. I use ssh a lot. I also use Cluster SSH. Finger macros being what they are, and Cluster SSH being a focus stealing bastard, I sometimes answer ‘yes’ to an SSH or Emacs question in one of 20 terminals and press Enter and inadvertently execute yes(1) on 19 of them, causing an endless column of y to appear. Since these SSH sessions are mostly remote ones, if I press Control-C to stop execution there are enough lines buffered for me to go on a short coffee break.

This won't do. Let's be negative just this once.

Here's a quick wrapper for yes that stops early if its standard output is a terminal.

What the—? What's ‘yes’?

Glad you didn't actually ask. I'll answer regardless: yes is one of those obscure, standard little Unix programs you didn't know you needed. It prints out the same line of text again and again, endlessly. By default, the line it generates is y—short for ‘yes’, of course, and meant to be piped into commands that ask for confirmation and can't be used non-interactively.

The program is meant to be used in pipelines, of course.

$ yes | sudo rm -rf /home/* # Don't try this at home! (see what I did there?)

If it's part of a pipeline, with its standard output sent to another program, it's all good. The trouble starts when you fire off this sucker with its standard output still connected to your terminal and you're connecting from a remote machine. A serious number of very small packets start coming your way en masse, and even after you break out of the program itself, you're still taking delivery of thousands of y lines. So disgustingly positive it probably goes to tree hugging classes.

The Solution

Here's the hack. Originally, when run interactively, the output of yes piped to an awkscript. I rewrote it in pure bash. It's better to avoid spawning external processes, of course, but this is a pipeline and it can't be helped. Might as well keep to a single language in the script.

#!/bin/bash

ORIGINAL_YES=/usr/bin/yes

if tty -s </dev/stdout; then
    "$ORIGINAL_YES" "$@" | {
        declare -i n=1
        while read -r line; do
            [[ "$line" == "$oldline" ]] && n+=1 || n=1
            if [[ $n -gt 10 ]]; then
                echo "... and so on, for ever and ever. Stopping here since stdout is a TTY."
                exit 0
            fi
            oldline="$line"
            echo "$line"
        done
    }
    code=${PIPESTATUS[0]}
    exit $code
else
    exec "$ORIGINAL_YES" "$@"
fi

# End of file.

Save this somewhere like /usr/local/bin/yes where it'll take precedence over /usr/bin/yes. Check your $PATH! While you're at it, set ORIGINAL_YES near the top to the right value. On Debian and RH-like systems, /usr/bin/yes is correct.

The theory of operation is relatively simple. We check if /dev/stdout is a tty. If it is, we pipe yes to a bash while loop. The loop prints out its input until the eleventh identical line is seen in a row; it then prints out a message and exits. This extra logic (rather than just counting ten lines) makes it possible for, e.g. yes --help to work unmodified.

Finally, if /dev/stdout isn't a TTY, we exec the standard yes. We're done with the shell script at this point, so we use exec, to entirely replace the shell wrapper process with yes.