Zurück zur Übersicht

Mit Launchd Skripte auf dem Mac automatisieren

Mit dem LaunchDaemon-Dienst unter macOS haben Sie auf dem Mac die Möglichkeit, Skripte und Programme automatisch zu starten. Der Start eines Skripts kann sowohl nach zeitlichen Kriterien als auch auf bestimmte Ereignisse hin erfolgen.

Im ersten Abschnitt dieses Artikels wird erklärt, wie man Skripte für die automatische Ausführung vorbereitet und mit den nötigen Tools im Terminal umgeht. Im zweiten Teil finden Sie einige plist-Dateien als Praxis-Beispiele, die Sie für Ihren Mac anpassen können, um so Ihre eigenen Skripte zeit- oder ereignisgesteuert zu starten.

Hier finden Sie ohne Umwege die plist-Beispiele, die in /Library/LaunchDaemons eingebunden werden können.

Dieser Artikel geht davon aus, dass Sie mit dem Terminal grundsätzlich vertraut sind. Alle Befehle, die hier Sie hier finden, werden im Terminal eingegeben.


Vorbereitungen und Umgang mit Launchd

Zunächst müssen wir ein Skript erstellen, das später mit LaunchDaemon ausgeführt wird. Es spielt dabei keine Rolle, ob es sich um ein Bash-, Perl- oder Python-Skript handelt – in diesem Beispiel erstellen wir zur Demonstration ein einfaches Bash-Skript, das auch gleich einen einfachen Logging-Mechanismus eingebaut hat, um die Skript-Ausführung mit launchd prüfen zu können.


Das Verzeichnis /usr/local/bin anlegen

Das Verzeichnis /usr/local/bin ist traditionell der bevorzugte Ort, um Skripte in macOS abzulegen. Viele Tools und Programme nutzen dieses Verzeichnis, allerdings erstellt Apple es nicht bei der System-Installation von macOS.
Wenn Sie sich nicht sicher sind, ob das Verzeichnis bereits existiert, geben Sie einfach ein:

cd /usr/local/bin

Falls Sie bie der Eingabe des cd-Befehls oben die Fehlermeldung cd: no such file or directory: /usr/local/bin erhalten, existiert das Verzeichnis /usr/local/bin noch nicht. In diesem Fall erstellen Sie zunächst das Verzeichnis und setzen anschließend die passenden Userrechte (geben Sie Ihr Benutzer-Passwort bei der Passwort-Abfrage ein):

mkdir -p /usr/local/bin
sudo chown $(whoami) /usr/local/bin
Password:

Ein Shell-Skript erstellen, das ausgeführt werden soll

Erstellen Sie nun ein Skript, dass später von LaunchDaemon ausgeführt wird.
In diesem Beispiel verwenden wir den Editor nano, der bei macOS mitgeliefert wird und auch für Einsteiger einfach zu bedienen ist. Wenn Sie erfahren im Umgang mit der Shell sind, werden Sie sicher einen anderen Editoren wie etwa vi bevorzugen.

nano /usr/local/bin/script_to_start.sh

Fügen Sie nun im Editor folgenden Inhalt in das Skript ein:

#!/bin/bash

logfile="/var/log/script_to_start.log"
datestr=$(date +%d.%m.%Y-%H:%M:%S)

echo $datestr": Testskript wird ausgeführt. Parameter #1: "$1", Parameter #2: "$2", parameter3: "$3". "Username: $(whoami)  >> $logfile
(Falls Sie nano verwenden, beenden Sie den Editor mit der Tastenkombination CTRL-X, drücken dann y für Yes und quittieren anschließend mit Enter den Dateinamen, um nano zu beenden.)


Machen Sie nun das gerade erstellte Skript ausführbar:

chmod +x /usr/local/bin/script_to_start.sh

Ein Logfile für das Skript erstellen

Damit die Ausführung des Skriptes kein Blindflug wird, sollte in einem Logfile jeder Skriptstart und ggf. weitere Informationen protokolliert werden.

Legen Sie dazu in /var/log zunächst eine leere Datei an und stellen deren Rechte so ein, dass das Skript des eingeloggten Users in die Logdatei schreiben darf:

sudo touch /var/log/script_to_start.log
sudo chown $(whoami) /var/log/script_to_start.log

Selbstverständlich muss die Log-Datei nicht script_to_start.log heißen – falls Sie einen anderen Namen bevorzugen, müssen Sie diesen aber auch im Skript oben anpassen.


Das Shell-Skript testen

Starten Sie nun das Skript manuell im Terminal. Wir geben zusätzlich beim Skriptaufruf noch zwei Parameter mit, um später zu zeigen, wie die Parameterübergabe in den plist-Dateien für Launchd funktioniert.

/usr/local/bin/script_to_start.sh Parameter1 Parameter2
Es sollte keine Fehlermeldung erscheinen. Im Logfile sollte ein passender Eintrag vorhanden sein:
tail /var/log/script_to_start.log
07.06-2017-14:45:46: Testskript wird ausgeführt. Parameter #1: Parameter1, Parameter #2: Parameter2. myusername

plist-Dateien mit launchctl starten, stoppen und auflisten

Jetzt, wo alle Vorbereitungen erledigt sind, kann es losgehen mit dem Einbinden des Skriptes mit LaunchDaemon.
Hierzu wird eine plist-Datei im Verzeichnis /Library/LaunchDaemons erstellt, in der festgelegt wird, wann das Skript in /usr/local/bin gestartet werden soll.

Im Verzeichnis /Library/Launchdaemons werden mit dem Tool launchctl die Launchd-Jobs verwaltet.

Wechseln Sie also zunächst ins Verzeichnis /Library/LaunchDaemons:

cd /Library/LaunchDaemons

Die Dateien in diesem Verzeichnis gehören dem root-User. Erstellen Sie daher mittels sudo das erste Startskript:
sudo nano de.ulrich-zentis.5_min_interval.plist

Fügen Sie folgenden Inhalt ein und speichern Sie die Datei (CTRL-X, y, Enter). Diese Demo-plist ist das erste einfache Beispiel aus der Sammlung unten.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>de.ulrich-zentis.5_min_interval</string>
    <key>Program</key>
    <string>/usr/local/bin/script_to_start.sh</string>
    <key>StartInterval</key>
    <integer>300</integer>
    <key>UserName</key>
    <string>myusername</string>
</dict>
</plist>
                

Dieser Befehl lädt eine plist-Datei in LaunchDaemon:

sudo launchctl load -w de.ulrich-zentis.5_min_interval.plist

-w sagt LaunchDaemon, dass das Skript persistent behandelt wird, also unter anderem auch nach einem Neustart des Mac geladen werden soll. Sollten Sie ein Fehlermeldung erhalten, können Sie mit tail /var/log/system.log in den letzten Zeilen des Systemlog nachschauen, ob launchd etwas Konkretes bemängelt.

Mit unload stoppen Sie die automatische Ausführung eines Skriptes wieder:

sudo launchctl unload de.ulrich-zentis.5_min_interval.plist

Eine Liste der gerade aktiven Launchd-Jobs erhalten Sie mit:

sudo launchctl list

Wobei die Liste lang ist und schnell die Übersicht verloren geht. Um die Liste zu kürzen und nur die Jobs anzuzeigen, die etwa mit de. beginnen, hilft grep:

sudo launchctl list | grep de.

Beispiel-Skripte für LaunchDaemon

In diesem Abschnitt finden Sie einige plist-Dateien für Launchd, mit denen Sie Skripte auf Ihrem Mac automatisch starten können.

Der vordere Teil des Namens der plist-Datei kann natürlich frei gewählt werden, allerdings sollte die Referenz unter Label (in diesem Beispiel de.ulrich-zentis.5_min_interval) innerhalb der plist-Datei auch entsprechend angepasst werden.

Fügen Sie nun also den Inhalt aus dem folgenden Fenster in die in nano geöffnete plist-Datei ein und speichern Sie sie anschließend ab (in nano: CTRL-X, y, Enter):


Skripte in einem Zeitintervall starten

Hier die erste plist-Datei de.ulrich-zentis.5_min_interval.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>de.ulrich-zentis.5_min_interval</string>
    <key>Program</key>
    <string>/usr/local/bin/script_to_start.sh</string>
    <key>StartInterval</key>
    <integer>300</integer>
    <key>UserName</key>
    <string>myusername</string>
</dict>
</plist>
                

Der richtige Username

Der Username in jedem Beispiel dieser Seite muss unter dem Key UserName (hier: myusername) durch den korrekten Usernamen ersetzt werden, der das Skript auf Ihrem Mac ausführen soll:

[...]
    <key>UserName</key>
    <string>IhrUserKurzname</string>
[...]
                

Wenn Sie den Kurznamen des Users nicht wissen, liefert Ihnen die Eingabe von echo $(whoami) im Terminal den Usernamen des gerade eingeloggten Users.


Ein Skript ohne Parameter ausführen

Diese plist-Datei de.ulrich-zentis.5_min_interval.plist führt ein Skript ohne weitere Parameter alle 300 Sekunden aus:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>de.ulrich-zentis.5_min_interval</string>
    <key>Program</key>
    <string>/usr/local/bin/script_to_start.sh</string>
    <key>StartInterval</key>
    <integer>300</integer>
    <key>UserName</key>
    <string>myusername</string>
</dict>
</plist>
                

Der Key Program gibt den vollständigen Pfad des Skriptes an, das ausgeführt werden soll. Parameter für das Skript werden in diesem Fall nicht mitgegeben.

Die Angabe unter Startinterval gibt das Startintervall in Sekunden an, siehe hier.


Ein Skript mit Parametern ausführen

Diese plist-Datei de.ulrich-zentis.5_min_interval_w_params.plist führt ein Skript mit zwei Parametern aus. Die Ausführung erfolgt in diesem Beispiel alle 300 Sekunden.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>de.ulrich-zentis.5_min_interval_w_params</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/script_to_start.sh</string>
        <string>Param1</string>
        <string>Param2</string>
    </array>
    <key>StartInterval</key>
    <integer>300</integer>
    <key>UserName</key>
    <string>myusername</string>
</dict>
</plist>
                

Der Username unter dem Key UserName (hier: myusername) muss durch den entsprechenden Usernamen ersetzt werden.
Die Angabe unter Startinterval gibt das Startintervall in Sekunden an.

Das Label ProgramArguments wird anstelle von Program im Beispiel oben verwendet, wenn Parameter für das Skript mitgegeben werden sollen:

Ohne Parameter:

[...]
    <key>Program</key>
    <string>/usr/local/bin/script_to_start.sh</string>
[...]

Mit Parametern:

[...]
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/script_to_start.sh</string>
        <string>Param1</string>
        <string>Param2</string>
    </array>
[...]
                

In diesem Beispiel wird das Skript mit zwei Paramtern gestartet, wie das etwa beim Befehl cp sourcefile.txt targetfile.txt in der Shell der Fall wäre. Weitere Parameter werden dem Array einfach durch eine weitere Zeile mit <string></string>-Tags hinzugefügt.


Ausführung in einem Zeitintervall

Im ersten Beispiel oben werden die Skripte mit dem Label Startinterval gestartet. Dies ist die wohl einfachste Art, wie ein Skript mit Lauchd ausgeführt werden kann – für die meisten Fälle sollte dies völlig ausreichen.

[...]
    <key>StartInterval</key>
    <integer>300</integer>
[...]
                

Gezählt wird in Sekunden. In unserem Beispiel wird das Skript alle 5 Minuten (also alle 300 Sekunden, Sherlock) ausgeführt.


Ein Skript zu einer bestimmten Uhrzeit / einem bestimmten Datum ausführen

Diese plist-Datei de.ulrich-zentis.specific_datetime.plist führt ein Skript mit zwei Parametern am 15. jeden Monats um 22:00 Uhr aus.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>de.ulrich-zentis.specific_datetime</string>
    <key>Program</key>
    <string>/usr/local/bin/script_to_start.sh</string>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Minute</key>
        <integer>00</integer>
        <key>Hour</key>
        <integer>22</integer>
        <key>Day</key>
        <integer>15</integer>
    </dict>
    <key>UserName</key>
    <string>myusername</string>
</dict>
</plist>
                

Der Username unter dem Key UserName (hier: myusername) muss durch den entsprechenden Usernamen ersetzt werden.

Soll das Skript zu jeder vollen Stunde ausgeführt werden, wird lediglich der Parameter Minute angegeben:

[...]
    <key>StartCalendarInterval</key>
    <dict>
        <key>Minute</key>
        <integer>00</integer>
    </dict>
[...]

                

Analog wird das Skript nur einmal am Tag ausgeführt, wenn nur Minute und Hour angegeben werden:

[...]
    <key>StartCalendarInterval</key>
    <dict>
        <key>Minute</key>
        <integer>00</integer>
        <key>Hour</key>
        <integer>22</integer>
    </dict>
[...]
                

Es gibt noch weitere Zeitangaben, die für StartCalendarInterval verwendet werden können: Month und CalendarDay. Month ist natürlich für die Ausführung in einem bestimmten Monat zuständig, während CalendarDay einen bestimmten Wochentag angibt. Die Zählung startet Sonntags mit 0, folgende Zeitangabe startet das Skript beispielsweise jeden Donnerstag zu jeder vollen Stunde:

[...]
    <key>StartCalendarInterval</key>
    <dict>
        <key>Minute</key>
        <integer>00</integer>
        <key>CalendarDay</key>
        <integer>4</integer>
    </dict>
[...]
                

Der Vollständigkeit halber sei erwähnt, dass mehrere Zeitangaben einer Art in einem Array of Dicts kombinierbar sind – theoretisch. Mit einem Array können Skripte zu mehreren bestimmten Minuten oder Stunden gestartet werden. Leider ist aufgrund der miesen Implementierung durch Apple eine Kombination mit anderen Keys nicht möglich bzw. führt zu unvorhersehbaren Ergebnissen, wie etwa einer unbeabsichtigten minütlichen Ausführung. Dieser Abschnitt startet beipielsweise ein Skript zur Minute 30 und 33, eine Kombination mit anderen Keys innerhalb von CalendarInterval sollte aber wegen der erwähnten Probleme tunlichst vermieden bzw. ausgiebig getestet werden, sofern launchctl das Skript überhaupt akzeptiert.

[...]
    <key>StartCalendarInterval</key>
    <array>
        <dict>
            <key>Minute</key>
            <integer>30</integer>
        </dict>
        <dict>
            <key>Minute</key>
           <integer>33</integer>
        </dict>
    </array>
[...]
                

Dieses Beispiel der Nicht-Kombinierbarkeit zeigt, dass das gut abgehangene cron durchaus seine Vorzüge hat und XML nicht notwendigerweise Vorteile bringt.


RunAtLoad - Ausführung beim Laden und Booten

Der Key RunAtLoad wird in Beispielen sehr häufig verwendet, weil er zur Fehlerdignose sehr praktisch ist – das Skript wird damit bereits beim Laden mit launchctl load skriptname.plist zum ersten Mal ausgeführt, was eine sofortige Reaktion im Logfile zur Folge hat. Auf diese Weise kann sofort geprüft werden, ob sich das Skript so benimmt wie erwartet.

[...]
    <key>RunAtLoad</key>
    <true/>
[...]
                

Allerdings wird dann auch beim Booten des Macs das Skript ausgeführt, was zu unerwarteten Effekten führen kann – insbesondere wenn das Skript nur laufen soll, wenn Veränderungen in einem Verzeichnis registriert werden, kann dies zu unerwünschten Nebeneffekten führen.


Ein Skript bei Veränderungen in einem Ordner ausführen

Eine sehr praktische Startmethode bietet WatchPaths. Damit wird ein Skript durch Auswertung sogenannter Filesystem Events gestartet, sobald in einem bestimmten Ordner Veränderungen registriert werden. Werden beispielsweise Dateien oder weitere Ordner hinzugefügt, gelöscht oder verändert, startet das eingetragene Skript.

Diese Startmethode ist extrem praktisch für Backupjobs, Datentranfers oder interaktive Workflows, die indirekt durch den Anwender gestartet werden sollen. So kann durch Abspeichern einer Medien- oder Projektdatei in einem Grafik- oder Audioprogramm eine Konvertierung oder ein Datentransfer angestossen werden, ohne dass auf einen bestimmten Zeitpunkt gewartet werden muss.

Hier die de.ulrich-zentis.watchfolder.plist, die den Ordner WatchFolder auf dem Schreibtisch des Benutzers auf Veränderungen überwacht:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>de.ulrich-zentis.watchfolder</string>
    <key>Program</key>
    <string>/usr/local/bin/script_to_start.sh</string>
    <key>WatchPaths</key>
    <array>
        <string>/Users/myusername/Desktop/WatchFolder</string>
    </array>
    <key>UserName</key>
    <string>myusername</string>
</dict>
</plist>
                

Natürlich muss neben der gewohnten Anpassung des Usernamens unter dem Key UserName hier zusätzlich noch der korrekte Username (hier: myusername) im Pfad des WatchFolder <string>/Users/myusername/Desktop/WatchFolder</string> angepasst werden.