macOS: zsh and launchd ====================== As of version 10.15 (Catalina) macOS changed the default shell to zsh_ (Z Shell), prior using bash_ (Bourne again Shell). While *bash* is still available in macOS as of now, it is recommended to zsh, including for shell scripts. .. _zsh: https://www.zsh.org/ .. _bash: https://www.gnu.org/software/bash/ A basic shell script -------------------- Shell scripts use the \*.sh suffix. In order to execute shell scripts directly, like .. code-block:: bash $ my_first_script.sh instead of requiring to pass it as an argument to a shell call like .. code-block:: bash $ zsh ./my_first_script.sh a so called shebang_ line is required in your script as the first line. This declares which type of shell is used when the script is called directly. It is recommended to use this shebang line on macOS: .. code-block:: shell :linenos: #!/usr/bin/env zsh -l .. hint:: The ``env`` tool acts as a pointer towards the shell binary file. Using it makes the script less system dependent. Passing in the ``zsh`` argument defines, that we want the path to the zsh shell (here ``/bin/zsh``). The ``-l`` option defines, that we want a shell with the current user being logged into it. This executes the user specific profile files like ``/etc/zprofile`` or ``~/.zshrc`` (`more info`_), which feed to the PATH variable, which otherwise contains no paths to user-specific executable directories, such as ``/usr/local/bin``. In the shell script this enables us to use commands directly, without having to use their absolute paths: .. code-block:: shell # use binary commands directly match=$(pcregrep -o1 $update_version_regex $tmp_file) say "This worked just fine" # instead of requiring its absolute path match=$(/usr/local/bin/pcregrep -o1 $update_version_regex $tmp_file) /usr/bin/say "This worked as well, but is longer and system dependent" macOS uses a tool called *path_helper* to feed some entries to the PATH variable. It is called from ``/etc/zprofile`` by default. Next up you may define your shell script. Here are some useful resources: * https://scriptingosx.com/ * https://www.gnu.org/software/bash/manual/bash.html * https://zsh.sourceforge.io/Doc/Release/zsh_toc.html Make sure to make your script executable (for security reasons this is not done by default): .. code-block:: bash $ chmod 755 my_first_script.sh .. _shebang: https://en.wikipedia.org/wiki/Shebang_(Unix) .. _more info: https://scriptingosx.com/2019/06/moving-to-zsh-part-2-configuration-files/ launchd: Script management -------------------------- If you created a script and wish to execute regularly or on specific occasion (e.g. after each log-in), macOS provides the `launchd`_ tool, which manages services and daemons, similar to Linux' *systemd*. Similar to *cron* on Linux (or the *Task Scheduler* on Windows) it offers the possibility to schedule executions of shell scripts. .. hint:: There are tools which help generating \*.plist files (although you might still need to do some manual tweaking afterwards), such as: * `LaunchControl`_ (proprietary, needs license) * https://launched.zerowidth.com/ (web form -> save as \*.plist, not \*.xml) Launchd tasks (agents and daemons) are defined in \*.plist config files, which follows the XML syntax. For our script we create a new file in ``~/Library/LaunchAgents`` (the user agent directory): .. code-block:: bash $ touch ~/Library/LaunchAgents/my_first_script.plist Add in the following content: .. code-block:: xml :linenos: Label MY_SCRIPT_NAME ProgramArguments /bin/zsh -c /PATH/TO/MY/SCRIPT.sh StartInterval 3600 StandardOutPath ~/tmp/MY_SCRIPT_NAME.stdout.log StandardErrorPath ~/tmp/MY_SCRIPT_NAME.stderr.log AbandonProcessGroup Now go ahead and replace MY_SCRIPT_NAME and PATH/TO/MY/SCRIPT.sh to match your script. Also edit the *StartInterval* time if needed (above set to 3600 seconds, which is one execution per hour). To run the script only once on login replace .. code-block:: xml :linenos: :lineno-start: 13 StartInterval 3600 with .. code-block:: xml :linenos: :lineno-start: 13 RunAtLoad For more information on the plist options, check https://www.launchd.info/. After you finished configuration file, add it to the *launchd* controller: .. code-block:: bash $ launchctl load ~/Library/LaunchAgents/my_first_script.plist To check the status of the last execution run (replace MY_SCRIPT_NAME with the label you defined in your \*.plist file) .. code-block:: bash $ launchctl list | grep MY_SCRIPT_NAME which lists the return code of your script from the last execution, which should be 0, if the script ran successfully. Check the *stderr.log* and *stdout.log* files which you defined in the \*.plist config file for clues. .. hint:: The return code is always 0 before the script was executed once after loading it. For debugging, change to a low *StartInterval* value to trigger an execution. To remove the script from the launcher run .. code-block:: bash $ launchctl unload ~/Library/LaunchAgents/my_first_script.plist .. hint:: Changes to the shell script are adapted and used at the next execution, but changes to the \*.plist require a reload (*unload* then again *load*) to the launch controller. .. _launchd: https://www.launchd.info/ .. _LaunchControl: https://www.soma-zone.com/LaunchControl/