Profile picture

Oliver Baumann

A flock of fish: locking functions for the fish-shell

As I was hacking away at a shell-script the other day, I realised the script should prevent multiple instances from running at the same time. In my case, I wanted a simple egg-timer on the command line that runs for X minutes, shows a notification and beeps annoyingly, then exits. Obviously, there’s no point in having multiple timers stack up, so there should only ever be one ticking away.

There are multiple other reasons you may only want a single instance running: a long-running backup-task should complete before the next iteration starts, or all writes to a file should be synced to disk before modifying the file again.

Enter flock

Luckily, most Unixes come with the flock(2) syscall, conveniently wrapped by flock(1) provided by the util-linux package. flock manages locks from shell scripts, allowing us to express the notion of critical sections in our scripts: while the lock is held by another process, the critical section cannot be entered. A “lock” in this case is simply a file or directory on the filesystem.

This is the basic invocation:

# flock /tmp/timer.lock sleep 10m

In this basic form, flock will…

  • create /tmp/timer.lock if it doesn’t exist,
  • acquire a lock on this file,
  • execute the specified command, sleep 10m

This will acquire an exclusive lock on /tmp/timer.lock, meaning only one process may hold the lock at any given time1. If the lock is being held, flock will wait until it is free again; this is called a “blocking wait”.

Don’t block the lock

While this blocking and waiting is all nice and dandy, I want my script to fail if the lock is being held. flock has us covered:

 -n, --nb, --nonblock
     Fail rather than wait if the lock cannot be immediately acquired.
     See the -E option for the exit status used.

Putting it all together

Armed with all of this info, I perused the manpage a little more and stumbled upon this gem:

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :

If the variable FLOCKER is not set to the name of the current script, it is explicitly set, and flock re-executes the script (identified by $0) with all arguments ($@), using the script itself as a lock. If you think about it, this is pretty nifty, as it doesn’t clutter the filesystem with useless lock-files!

Teach the script to fish

Anyway, this bash oneliner won’t work in fish, so some translation is necessary. In the end, I came up with this:

function timer

    set numargs $(count $argv)
    if test $numargs -eq 0
        echo "missing duration: 60s, 10m, 1h"
        set FILE (status filename)
        if test "{$FLOCKER}" != "{$FILE}"
            set me fish -c "timer $argv"
            env FLOCKER=$FILE flock -n $FILE $me || echo "Timer already running"

        sleep {$argv[1]}
        notify-send -t 6000 "$argv[1] over!"
        paplay /usr/share/sounds/freedesktop/stereo/bell.oga

Some argument handling first, then a transliteration from bash to fish beginning at line 7. One thing to note here is that I’ve got this thing as an autoloaded function because I didn’t want to mess around with $PATH and wellwhynotafterall. Therefore, line 9 has fish -c "timer $argv" rather than fish -c "$FILE $argv", because the new instance of fish knows that timer is an autofunction, but doesn’t know what to do with the file itself2.

Wrapping up

This post got longer than I thought, but I learned a couple of things about flock, and yet more stuff about fish functions and syntax. If we learned anything, then it’s that we don’t need to futz around creating, testing, and removing files by hand to implement lockfiles: just use flock and be merry :)

  1. flock also supports shared locks, which I haven’t completely grokked yet. From what I understand, a shared lock can be held by multiple processes, iff all are acquired shared; attempting to acquire a held shared lock in exclusive-mode will fail. However, I believe the semantics of when to share and when to exclude is up to the programmer and is not enforced in any way by the command itself. ↩︎

  2. This is incredibly handwavy, because this post is getting too long already. Suffice to say, you can just as well put your logic in a file with a shebang and have fish execute that. ↩︎

Go to top