Keep running command until output differs from previous run in Bash

From man 1 watch:

-g, --chgexit
Exit when the output of command changes.

watch is not required by POSIX but it's quite common anyway. In Debian or Ubuntu it's in the procps package along with kill and ps (and few other tools).

Example:

watch -g -n 5 'date +%H:%M'

This should do the trick - explanations inline:

#!/usr/bin/env bash

# Exit if any errors occur or any undefined variables are dereferenced
set -o errexit -o nounset

# Store the outputs in a separate directory
output_directory="$(mktemp --directory)"
last_output_file="${output_directory}/last"
current_output_file="${output_directory}/current"
touch "$current_output_file"
ln --symbolic "$current_output_file" "$last_output_file"
# The count is just here to show that we always run at least twice, and that
# after that we run the command a random number of times.
count=0

while true
do
    echo "$((++count))"

    # This is our command; it prints 0 or 1 randomly
    if [[ "$RANDOM" -gt 16383 ]]
    then
        echo 0 > "$current_output_file"
    else
        echo 1 > "$current_output_file"
    fi

    # Abort the loop when the files differ
    if ! diff --brief "$last_output_file" "$current_output_file"
    then
        break
    fi

    # Shunt the current output to the last
    mv "$current_output_file" "$last_output_file"
done

There are probably simpler ways to do this, but it has a couple useful features:

  • It avoids creating any extraneous files, such as using mktemp for every output.
  • By starting with the last output file being a symbolic link to the current output file we guarantee that the first diff command sees two identical files. This avoids duplicating the command we're testing for, at the expense of one extra diff execution.

Obviously, watch is the way to go here, as described in another answer. But if you ever do want or need to do this in the shell, here's one way to do it:

#!/bin/sh
unset prev
while output=$(some_command);
      [ "${prev+set}" != set ] || [ "$output" = "$prev" ];
do
    prev=$output
    sleep 10;
done

"${prev+set}" expands to set if prev was set to a value after being unset at the start, so in effect it forces the contents of the loop to run at least once. Alternatively we could just initialize prev as prev=$(some_command) at the cost of running some_command an additional time at the start of the loop.

Also, as noted in comments, command substitution removes all trailing newlines from the command output, so if the only changes in the result are with those, this won't be able to tell the outputs apart. That should rarely matter, though.

Tags:

Bash