diff --git a/mpsd-software-environment.py b/mpsd-software-environment.py
index 08eea1494a0c5bfdaa5c3aee71b6621097cb78fd..14d5ac4295a2d47a2c7f7dc1c66bf7a8b7917d2b 100755
--- a/mpsd-software-environment.py
+++ b/mpsd-software-environment.py
@@ -4,6 +4,7 @@ import datetime
 import os
 import subprocess
 import sys
+import time
 from pathlib import Path
 from typing import List, Tuple
 
@@ -44,6 +45,90 @@ class os_chdir:
         os.chdir(self.saved_dir)
 
 
+def run(*args, counter=[0], **kwargs):
+    """
+    Call subprocess.run(*args, **kwargs) and print logging data.
+
+    Conveniene function to call `subprocess.run` and provide some metadata
+    about the call.
+
+    Parameters
+    ----------
+    args : tuple
+        passed on to subprocess.run(*args). For example
+        ("ls -l") or (["ls", "-l"])
+    counter : TYPE, optional
+        list with one integer, starting from [0].
+        This is (a Python hack) to count the number of
+        calls of this function, so the different calls of subprocess.run
+        are easier to follow in the log files.
+    kwargs : dict
+        keyword-value arguments to be passed to subprocess.run. For example,
+        `shell=True`.
+
+    Returns
+    -------
+    process : subprocess.CompletedProcess
+        CompletedProcess object as returned by `subprocess.run` .
+
+    Examples
+    --------
+    >>> run(['date', '+%Y-%m-%d'])
+    ##-03 Starting subprocess.run(['date', '+%Y-%m-%d']) with options
+    ##-03   getcwd=/Users/fangohr/git/mpsd-software-environments
+    ##-03   COMMAND=date +%Y-%m-%d
+    2023-05-30
+    ##-03   Completed in 0.0054s.
+    ##-03
+    CompletedProcess(args=['date', '+%Y-%m-%d'], returncode=0)
+
+    >>> run(['date +%Y-%m-%d'], shell=True)
+    ##-04 Starting subprocess.run(['date +%Y-%m-%d']) with options shell=True
+    ##-04   getcwd=/Users/fangohr/git/mpsd-software-environments
+    ##-04   COMMAND=date +%Y-%m-%d
+    2023-05-30
+    ##-04   Completed in 0.0069s.
+    ##-04
+    CompletedProcess(args=['date +%Y-%m-%d'], returncode=0)
+    """
+    # token is printed in front of every meta-data line - useful for
+    # searching the logs. Starts with "##-00", then "##-01", ...
+    token = f"##-{counter[0]:02d}"
+
+    counter[0] += 1  # increase counter
+
+    # make command nicely readable: ["ls", "-l"] -> "ls -l"
+    assert isinstance(args, tuple)
+    assert len(args) == 1
+    arg = args[0]
+    # either args is a tuple containing a string | Example: ('ls -1',)
+    if isinstance(arg, str):
+        command = arg
+        # or we have a tuple containing a list of strings.
+        # Example: (['ls', '-1'],)
+    elif isinstance(arg, list):
+        command = " ".join(arg)
+    else:
+        # we do not expect this to happen
+        raise NotImplementedError(f"{arg=}, {args=}")
+
+    # make options (such as `shell=True`) nicely readable
+    options = ", ".join([f"{key}={value}" for key, value in kwargs.items()])
+
+    # provide information about upcoming subprocess.run call
+    print(f"{token} Starting subprocess.run({arg}) with options {options}")
+    print(f"{token}   getcwd={os.getcwd()}")
+    print(f"{token}   COMMAND={command}")
+
+    time_start = time.time()
+    process = subprocess.run(*args, **kwargs)
+    execution_time = time.time() - time_start
+
+    print(f"{token}   Completed in {execution_time:.4f}s.")
+    print(f"{token}")  # near-empty line to make reading logs easier
+    return process
+
+
 def setup_log_cmd(
     mpsd_release: str, script_dir: str, msg: str = None, *args, **kwargs
 ) -> None:
@@ -86,7 +171,7 @@ def setup_log_cmd(
                 # script branch and commit hash
                 with os_chdir(script_dir):
                     script_branch = (
-                        subprocess.run(
+                        run(
                             ["git", "rev-parse", "--abbrev-ref", "HEAD"],
                             stdout=subprocess.PIPE,
                             check=True,
@@ -95,7 +180,7 @@ def setup_log_cmd(
                         .strip()
                     )
                     script_commit_hash = (
-                        subprocess.run(
+                        run(
                             ["git", "rev-parse", "--short", "HEAD"],
                             stdout=subprocess.PIPE,
                             check=True,
@@ -139,7 +224,7 @@ def create_dir_structure(mpsd_release: str, script_dir: Path) -> None:
     with os_chdir(release_base_dir):
         # Clone the spack-environments repo if it doesn't exist
         if not os.path.exists("spack-environments"):
-            subprocess.run(
+            run(
                 [
                     "git",
                     "clone",
@@ -150,14 +235,16 @@ def create_dir_structure(mpsd_release: str, script_dir: Path) -> None:
         with os_chdir("spack-environments"):
             # Git fetch and checkout the release branch and git pull
             # to be sure that the resulting repo is up to date
-            subprocess.run(["git", "fetch", "--all"], check=True)
-            checkout_result = subprocess.run(["git", "checkout", mpsd_release])
+            run(["git", "fetch", "--all"], check=True)
+            checkout_result = run(["git", "checkout", mpsd_release], check=True)
+
             if checkout_result.returncode != 0:
                 raise Exception(
                     "Release branch does not exist in spack-environment repo \n."
                     "Check for typos."
                 )
-            subprocess.run(["git", "pull"], check=True)
+            run(["git", "pull"], check=True)
+
 
 
 def get_release_info(mpsd_release: str, script_dir: Path) -> Tuple[str, str, List[str]]:
@@ -176,27 +263,27 @@ def get_release_info(mpsd_release: str, script_dir: Path) -> Tuple[str, str, Lis
       toolchains for the release.
 
     Raises:
-    - Exception: If the release directory does not exist. Run `create_dir_structure()`
-      first.
+    - FileNotFoundError: If the release directory does not exist. Run
+      `create_dir_structure()` first.
     """
     # Get the info for release
     release_base_dir = script_dir / mpsd_release
     if not os.path.exists(release_base_dir):
-        raise Exception(
+        raise FileNotFoundError(
             "Release directory does not exist. Run create_dir_structure() first."
         )
     with os_chdir(release_base_dir):
         with os_chdir("spack-environments"):
             # Get the branch and commit hash of the spack-environments repo
             spe_commit_hash = (
-                subprocess.run(
+                run(
                     ["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE, check=True
                 )
                 .stdout.decode()
                 .strip()
             )
             spe_branch = (
-                subprocess.run(
+                run(
                     ["git", "rev-parse", "--abbrev-ref", "HEAD"],
                     stdout=subprocess.PIPE,
                     check=True,
@@ -261,7 +348,7 @@ def install_environment(
                     cache when installing toolchains. Defaults to False.
 
     Raises:
-        Exception: If a requested toolchain is not available in the specified release.
+        ValueError: If a requested toolchain is not available in the specified release.
 
     Returns:
         None
@@ -299,7 +386,7 @@ def install_environment(
 
     for toolchain in toolchains:
         if toolchain not in available_toolchains:
-            raise Exception(
+            raise ValueError(
                 f"Toolchain '{toolchain}' is not available in release {mpsd_release}."
             )
 
@@ -334,7 +421,7 @@ def install_environment(
                     "{toolchain}"
                 ),
             )
-            subprocess.run(
+            run(
                 f"bash {spack_setup_script} {' '.join(install_flags)} {toolchain} 2>&1 "
                 f"| tee -a {install_log_file} ",
                 shell=True,