Wesley's cyber space

How to run a command every time a program closes in Linux

I have been trying to solve a problem for a while now. That problem is how to synchronize certain programs of mine that don't have built in synchronization across multiple devices. In less abstract terms, how do I synchronize my newsboat configuration and data between my laptop and my PC? I won't be going into that specifically here since I haven't fully completed that yet, but I do want to talk about one issue that I think I have solved for myself. How to run a command after a specific program has finished running.

The issue doesn't seem that complicated, but the more you think about it, the more weird edge cases you find. Let's look at a few solutions and their cons.

Solution 1: aliases

The first solution that came to mind was to simply alias the name of the program I want to have (what I am calling) a post command. For example:

alias newsboat="newsboat ; newsboatPostScript.sh"

But this doesn't seem reliable to me. This would likely work for a terminal based application like newsboat that I can be reasonably sure will run in an environment with this alias set, but for programs that aren't run from the terminal I worry that the alias won't suffice. Imagine a program that is called by dmenu, or by another script. In both cases, an alias will likely not be used. In addition, what if I want to pass flags or arguments to the original program and not to my post script? Well with the alias method this won't work either.

Solution 2: Wrapping the program

This solution was proposed to me by Joe on mastodon when I asked about this question:
This is definetely an interesting solution, but I have a few problems with it.

This solution would ensure that the script is run every single time after the program, but I think it would be just a bit too cumbersome. I don't generally like messing around with my bin and worry that any updates to the program by my package manager would overwrite my work, forcing me to regularly set things back up. In addition, I could simply leave the original binary alone and create a new script which I would explicitly use whenever I want to run the program in question, but if this program is also run by other programs or scripts, there is no way to be sure that I could get them to run my altenrative launch script as well.

Definitely closer, but not quite perfect for my taste.

My solution

The following script is the solution I have come up with, that I think I will be using in my case.

#!/bin/sh

# Default interval
INTERVAL=1

# Parse arguments
while [ $# -gt 0 ];
do
	case "$1" in
		-h|--help) 
			echo "Syntax: $0 [options]  "
			echo "-h --help: display this information"
			echo "-i --interval: interval to check process"
			echo "-n --no-repeat: do not repeat script after complete"
			echo "-x --intense: no interval between checking processes (much more intensive, not recommended)"
			exit 0
			;;
		-i|--interval)
			shift
			INTERVAL=$1
			shift
			;;
		-n|--no-repeat)
			REPEAT="yes"
			shift
			;;
		-x|--intense)
			EXP="yes"
			shift
			;;
		*)
			# If program is not set, set program
			[ -z "$PROGRAM" ] && PROGRAM=$1 && shift && continue
			# If program is set, set remaining arguments as post command
			[ -n "$PROGRAM" ] && SCRIPT=$* && shift $#
			;;
	esac
done

# Loop until program is found running
while [ -z "$(ps -e | grep -x ".* $PROGRAM$")" ]; 
do 
[ -z $EXP ] && sleep $INTERVAL
done

# Loop until program is no longer found running
while [ -n "$(ps -e | grep -x ".* $PROGRAM$")" ]; 
do 
[ -z $EXP ] && sleep $INTERVAL
done

# Execute post command
$SCRIPT

# Run script again if necessary
if [[ -z "$REPEAT" ]]; then
	exec $0 -i $INTERVAL $PROGRAM $SCRIPT
fi
	

Everthing there should be pretty self explanatory to anybody familiar with shell scripts, but in short. The script runs a loop checking to see if a program shows up in the running processes list. If it does, the loop stops and it enters a second loop which checks if the program is no longer in the running processes list. If it is no longer there, the loop stops and we run the post command as it is clear the program has stopped running (after knowing previously that it had been running). It then calls itself again (assuming the -n flag wasn't passed), waiting for the program to show up again.

This solved most of the preious problems. Since this is a script running indepentently it doesn't matter where or how a program is called. And there is no need to move, rename, or modify any binaries (or anything in the bin for that matter). In fact, the best part of the solution is that it doesn't actually impact the original program at all. It simply quietly observes in the bakground.

Well, not always quietly. If the script is run with the -x flag, then it will not sleep at all during either of it's loops checking the running programs. In my testing this seemed to launch its resource intesivity to the moon, which makes sense. This is why I added a customized sleep interval that defaults to 1 second. Because it spends most of it's time sleeping, it does end up being very quiet and out of the way.

This is by no means a more perfect solution in any broad sense. Each solution has its own pros and cons. If you aren't willing to sacrifice background processing power for this script, then one of the others may be better suited to your scenario. However, I believe this script is quite perfectly suited for my needs, and I find it very neat :).

If you know of any other alternative solutions, or have any ideas for how to improve this script, please reach out and let me know.

Thus is completed one more step in my journey to make my linux setups more streamlined and less conveluted.

If you want to comment on this post, or contact me in general, feel free to contact me on Mastodon. :)