A Weird Imagination

Debouncing shell commands

The problem#

For a compile-on-save workflow where some computation is done in response to every change to a file, if there may sometimes be many changes close together, it may be wasteful to respond to all of them. This is often handled by debouncing the events: instead of responding to every change, ignore changes that occur too close together in time.

The solution#

watch_todo_debounced.sh is a modification of the watch_todo.sh script from my recent post on converting todo.txt files to HTML. It uses a script I found called debounce.sh to wait until there have been no updates to the todo.txt file for 5 seconds before generating the HTML file. The core logic looks like this:

while true
do
    inotifywait -e close_write "$1" >/dev/null
    echo "TODO file updated."
done | debounce.sh read $delay "./do-something.sh \"$1\""

$delay is the delay in seconds to wait before taking an action. $1 is file to watch for changes on and ./do-something.sh is the script to run on it when it changes.

The details#

When to run the action#

Given a quick sequence of events, there's multiple choices of when to run the action which have different trade-offs between computation and latency:

  1. To minimize the runs of the action, wait until it has been $delay seconds since the last event and then run the action, which maximizes the latency to the first execution of the action. This is what debounce.sh does.

  2. Run the action every $delay seconds as long as the events keep coming. oalders/debounce implements this logic.

  3. (2) but wait $delay seconds before running the action for the first time. If the quick sequence of events is shorter than $delay seconds, this will only run the action once but have lower latency than (1). watch_todo.sh or similar can be easily modified to implement this logic simply by adding sleep $delay after the inotifywait call and before running the action.

Specifying the event and action#

Passing a command to be run by another shell command is awkward due to the possibility of having arbitrarily many arguments and escaping can get complicated. But it's further complicated in this scenario because we want to pass two commands into our script: the "wait for event" command and the "perform action" command.

debounce.sh's solution is to just not try that hard. The event command does not support arguments. The action command is passed to bash -c, so it's effectively interpreted as a string of a Bash script, so it can include calling a program with arguments if you're careful with escaping.

As a workaround, instead of passing the "wait for event" script to debounce.sh, the code above just tells it to run read and runs the code to wait for events outside of debounce.sh. It echoes a line to the pipe to debounce.sh for each event, which results in read returning.

Other debounce implementations#

In addition to debounce.sh, I found a few other shell scripts implementing similar ideas:

Comments

Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.