27

I have a script I want to run every x seconds until the output changes. With a simple while or until loop, I know how to check the output for a particular string using grep, but what I want to do is continue iterating the loop until the output from the current iteration does not equal the output from the previous iteration.

How can I achieve this in bash?

The closest I've gotten is below, where the command is run twice in each iteration, but this produces extra runs because the while loop cannot remember the output from the previous iteration.

while [ "$(command)" = "$(command)" ]; do sleep 10m; done
1
  • 4
    What have you tried?
    – Ramhound
    Nov 15, 2020 at 2:00

3 Answers 3

56

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'
2
  • 1
    Nice, much simpler than my solution!
    – l0b0
    Nov 15, 2020 at 3:05
  • 4
    @l0b0 Well, funny thing is I have just learnt watch can do this. I was thinking about a script like yours (temporary files? or variables? hashes in variables?) and it occurred to me there already may be a tool that can do this. If anything then watch. I checked and here it is. Nov 15, 2020 at 3:15
9

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.
5
  • It seems to work. rm -r "$output_directory" may or may not be desired. Nov 15, 2020 at 3:23
  • OP might want to inspect the difference, which is why I left it in.
    – l0b0
    Nov 15, 2020 at 3:25
  • I understand. My solution does not allow this. Here you win. Nov 15, 2020 at 3:28
  • Can you use --differences=permanent to achieve the same thing?
    – l0b0
    Nov 15, 2020 at 4:09
  • In my tests --differences=permanent doesn't cease repeating the command. The OP explicitly wants to "keep running until". Nov 15, 2020 at 9:11
6

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.

2
  • 1
    Minor note: command substitution removes trailing empty lines according to the standard.
    – DannyNiu
    Nov 16, 2020 at 4:26
  • @DannyNiu, yes. the question already used comsubs, so I figured that wouldn't be an issue.
    – ilkkachu
    Nov 16, 2020 at 9:06

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .