In this note, we will talk about file locks in Bash scripts🔒 using the specialized utility - flock.
🖐️Hey!
Subscribe to our Telegram channel @r4ven_me📱, so you don’t miss new posts on the website 😉. If you have questions or just want to chat about the topic, feel free to join the Raven chat at @r4ven_me_chat🧐.
Once I had a task to write a script with the ability to prevent it from being launched again if another instance was already running. This is quite a common case. Most often, this is implemented by creating a regular flag file with the .lock suffix or something like that. This variant is quite common, but far from ideal🙂.
An obvious problem can be the so-called race condition. This is when, for example, several script instances start asynchronously and can interfere with each other’s work or something worse😳.
To solve this task in Linux scripts, there is a standard (often preinstalled) utility flock - so let’s work with it🧑💻.
Suppose we have script_1.sh with the following content:
#!/bin/bash
SCRIPT_LOCK="${BASH_SOURCE[0]%.*}.lock"
if [[ -f "$SCRIPT_LOCK" ]]; then
echo "Script 1 is already running. Exiting."
exit 1
else
touch "$SCRIPT_LOCK"
echo "Starting script 1..."
sleep 999
rm -f "$SCRIPT_LOCK"
fi
$BASH_SOURCE[0]- a bash system variable containing the script path and name%.*- removes.shfrom the file name.
At first glance, everything is logical and clear. When launched:
chmod +x ./script_1.sh
./script_1.shIt checks for the presence of a lock file and, if it finds one, exits. If not, it creates it and performs further actions. If, while the script is running, you start it again in a neighboring terminal, it will accordingly complain that another instance is already running (the lock file exists) and exit.
On normal completion, the script removes the lock file so that it does not fail on the next launch.
But what if the script is interrupted, for example with Ctrl+c, and the lock file is not removed? This case will have to be handled separately. We will talk about this later.
Let’s stop (Ctrl+c) the running script and consider asynchronous launch, that is, create a condition for a race condition:
rm -f ./script_1.lock
./script_1.sh & ./script_1.sh &The
&operator in bash sends the process to run in the background.
And we will see that both started successfully?!:
[1] 1696362
[2] 1696363
Starting script 1...
Starting script 1...You can view processes running in the current session using the jobs command:
jobsHere is our pair🤨:
[1] - running ./script_1.sh
[2] + running ./script_1.shTo solve the problem, you can use flock. Create a 2nd script, script_2.sh, with the following content:
#!/bin/bash
SCRIPT_LOCK="${BASH_SOURCE[0]%.*}.lock"
exec {fd_lock}> "$SCRIPT_LOCK"
if ! flock -n "$fd_lock"; then
echo "Script 2 is already running. Exiting."
exit 1
fi
echo "Staring script 2..."
sleep 999
{fd_lock}>- bash syntax that creates the$fd_lockvariable and assigns it the number of the nearest free descriptor.
In this variant, we open the $SCRIPT_LOCK file for writing and assign it a named descriptor from $fd_lock (for example 10).
Then, using the flock command, we try to lock the resulting descriptor.
If successful, we continue working; if not, we exit with an error.
With a running script instance:
chmod +x ./script_2.sh
./script_2.shAnd when it is launched again, our script will also complain that a lock is present and exit.
If the running instance is interrupted, the lock file will not be removed, but this does not matter because the descriptor is locked. And in the case of a race condition, as last time:
./script_2.sh & ./script_2.sh &Our script will behave as expected:
[1] 1734870
[2] 1734871
Staring script 2...
Script 2 is already running. Exiting. 9ms
[2] + 1734871 exit 1 ./script_2.shThe first instance started, the second exited with an error. Check with jobs:
jobsWe see 1 instance🥳:
[1] + running ./script_2.shBut the question of the physical lock file remains🤔 It would be nice to remove it correctly too… even though this is not mandatory😉.
Below is an example of bash code for configuring a locking mechanism with explicit closing of the descriptor for flock (&-) and automatic removal of the .lock file, both on correct script completion and in case of an error or interruption⚡️.
You can save it as a template and use it when needed. Of course, everything is at your own risk⚠️.
vim script.sh && chmod +x ./script.sh#!/usr/bin/env bash
SCRIPT_LOCK="${BASH_SOURCE[0]%.*}.lock"
# cleanup function in case handlers are triggered
cleanup() {
# disables all previously set traps
trap - SIGINT SIGTERM ERR EXIT
# closes the lock file descriptor
[[ -n "${fd_lock:-}" ]] && exec {fd_lock}>&-
# removes the lock file if it belongs to the current PID
if [[ -f "$SCRIPT_LOCK" && $(<"$SCRIPT_LOCK") -eq $$ ]]; then
rm -f "$SCRIPT_LOCK"
fi
}
# sets handlers (trap)
trap cleanup SIGINT SIGTERM ERR EXIT
# opens the file for writing and saves the descriptor in $fd_lock
exec {fd_lock}>> "${SCRIPT_LOCK}"
# tries to acquire an exclusive lock (without waiting)
if ! flock -n "$fd_lock"; then
echo "Script is already running. Exiting."
exit 1
fi
# writes the current PID to the lock file
echo "$$" > "$SCRIPT_LOCK"
echo "Staring script..."
# your script logic
sleep 999 # command for example
$(<"$SCRIPT_LOCK")- substitution syntax that allows assigning the file contents to a variable;{fd_lock}>>- the append operator » is used to prevent overwriting the$SCRIPT_LOCKfile on repeated launches.
If you have a more elegant solution to the task, please write it in the comments to the post👇
P.S. Honestly, I still have not fully understood how exactly locking through descriptors works🤯 it seems the kernel is involved in the process. Well, fine.
Another example of using flock with “interesting” comments can be viewed in this Habr article📝
👨💻And…
Don’t forget about our Telegram channel 📱 and chat
Or maybe you want to become a co-author? Then click here🔗
💬 All the best ✌️
That should be it. If not, check the logs 🙂


