udev Rule Script Template


When writing shell scripts invoked by udev rules, I find the following template particularly useful:

#!/usr/bin/env bash
set -euo pipefail

CMD="`basename "${0:-udevscript}"`"
exec 1> >(tee >(logger -t "${CMD}"))
exec 2> >(ts '[stderr]' | tee >(logger -t "${CMD}"))

DEBUG="${1:-false}"
if [ "${DEBUG}" = true ]; then set -x; fi

echo "running: ${0}"
env

## Actual script starts here

We can then use such a script in a udev rule like we always would:

SUBSYSTEM=="usb", \
  ACTION=="remove", \
  ENV{ID_VENDOR_ID}=="17e9", \
  ENV{ID_MODEL_ID}=="6015", \
  RUN+="/usr/lib/udev/scripts/undock.sh"

And when the rule runs the script, everything printed is sent to the system log. Stderr messages will be prefixed with [stderr]. Which makes checking what the script does as simple as:

$ journalctl -e -t undock.sh
Oct 25 22:58:35 puddle undock.sh[32366]: running: /usr/lib/udev/scripts/undock.sh
Oct 25 22:58:35 puddle undock.sh[32366]: ID_SERIAL=DisplayLink_ThinkPad_Hybrid_USB-C_with_USB-A_Dock_10391787
Oct 25 22:58:35 puddle undock.sh[32366]: ID_MODEL_ID=6015
Oct 25 22:58:35 puddle undock.sh[32366]: ACTION=remove
...

The template also supports debug output. Setting the first argument to true will print commands and their arguments as they are executed to stderr.

Bonus: A Nix Expression

The script above is derived from the following nix expression.

{ pkgs ? import <nixpkgs> {}, ... }:

let

  logger = "${pkgs.utillinux}/bin/logger";
  ts = "${pkgs.moreutils}/bin/ts";
  tee = "${pkgs.coreutils}/bin/tee";

in name : script: {
    strict ? true,
    log ? true,
    debug ? false,
    printenv ? true,
  }: let

    strictCmd = if strict then
      ''
      set -euo pipefail
      ''
    else "";

    logCmd = if log then
      ''
      exec 1> >(${tee} >(${logger} -t "${name}"))
      exec 2> >(${ts} '[stderr]' | ${tee} >(${logger} -t "${name}"))
      ''
    else "";

    debugCmd = if debug then
      ''
      set -x
      ''
    else "";

    printenvCmd = if printenv then
      ''
      echo "running: ${name}"
      env
      ''
    else "";

  in pkgs.writeShellScript name
    ''
      ${strictCmd}
      ${logCmd}
      ${debugCmd}
      ${printenvCmd}

      ${script}
    ''

If invoked, the expression writes such a script to the nix store.

nix-repl> writeLoggedScript = import ./writeLoggedScript.nix {}

nix-repl> :b writeLoggedScript "test.sh" "echo this is just a test" {}

this derivation produced the following outputs:
  out -> /nix/store/bzjllwyhy0zwm2f352a0gya8fp13qc7q-test.sh