Unix Shell Scripting
While building a simple HTTP Server in Racket, I wanted to automatically restart the server when I changed the source file.
To achieve this, I wrote a simple shell script.
First, we need to detect when the file has been changed. We can do this by checking the timestamp for changes repeatedly, using stat -c '%Y' "$FILE"
#!/usr/bin/env sh
FILE="$1"
CHANGETIME="$(stat -c '%Y' "$FILE")"
while true; do
NEWCHANGETIME="$(stat -c '%Y' "$FILE")"
if [ $NEWCHANGETIME -ne $CHANGETIME ]; then
echo "$FILE has changed!"
CHANGETIME="$NEWCHANGETIME"
fi;
sleep 1
done;
Save this to a file, and make it executable (chmod +x $SCRIPT_FILE
).
Let's test this:
touch testfile.txt
# In an second terminal:
./watch.sh testfile.txt
# In the original terminal:
touch testfile.txt
Each time we run touch testfile.txt
, the file changed time will be updated, and our script should detect this and print testfile.txt has changed!
.
#!/usr/bin/env sh
CMD="$1"
FILE="$2"
# get the file changed time as Unix time
getChangeTime () {
stat -c '%Y' "$FILE"
}
# Start the program, store its process id as PSID,
# and set CHANGETIME to the file changed time of $FILE
startCmd () {
$CMD $FILE &
PSID=$!
CHANGETIME="$(getChangeTime)"
echo "Started $CMD $FILE at $(TZ=UTC date)"
}
# Initial start
startCmd
# Check for changes every second, if the file has
# changed, then kill the process and restart it
while true; do
NEWCHANGETIME="$(getChangeTime)"
if [ $NEWCHANGETIME -ne $CHANGETIME ]; then
echo "$FILE has changed!"
kill $PSID
startCmd
fi;
sleep 1
done;
Let's test this:
./watch.sh racket ./server.rkt
# In a second terminal
echo '; A trailing comment' >> ./server.rkt
And we should see the program killed, and restarted with a message:
Started racket ./server.rkt at Sun 13 Mar 13:31:12 UTC 2022
But what if we want to watch multiple files? We can use the following commands to get the latest time that any file from a directory was changed, and then use this in our script.
# Set the directory to watch
DIR="./src"
# Get all the last updated times for all files in the directory, sort this descending,
# and take the first one.
find "$DIR" -printf '%T@\n' | sort -r | sed '1q'
find "$DIR" -printf '%T@\n'
will print the time as a decimal, so we also need to change $NEWCHANGETIME -ne $CHANGETIME
to $NEWCHANGETIME != $CHANGETIME
- we will now compare the values as strings, not as integers.
Making these changes, and adding the directory as a third argument to the script, gives us the following new script:
#!/usr/bin/env sh
CMD="$1"
FILE="$2"
DIR="$3"
getChangeTime () {
find "$DIR" -printf '%T@\n' | sort -r | sed 1q
}
startCmd () {
$CMD $FILE &
PSID=$!
CHANGETIME="$(getChangeTime)"
echo "Started $CMD $FILE at $(TZ=UTC date)"
}
echo "Watching directory $DIR"
startCmd
while true; do
NEWCHANGETIME="$(getChangeTime)"
if [ $NEWCHANGETIME != $CHANGETIME ]; then
echo "A file has changed!"
kill $PSID
startCmd
fi;
sleep 1
done;
We can run this as follows:
./watch.sh racket server3.rkt .
And test by touching files in the directory:
echo foo > somefile
# Each time we touch a file, the script should restart our program.
touch somefile
If this has been useful, you have any comments or questions, or I have made a mistake in this post, please get in touch!