diff --git a/bin/create_cmakelists.py b/bin/create_cmakelists.py
new file mode 100755
index 0000000000000000000000000000000000000000..3a8d4d0fe387fc7c904508e39401c099138a08b0
--- /dev/null
+++ b/bin/create_cmakelists.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+
+"""
+Create files CMakeLists.txt for the given folder and all subfolders,
+including the add_subdirectory(...) and install(...) commands.
+Defaults to the folder `dumux` that contains the header files,
+if no folder was specified.
+"""
+
+import os
+import argparse
+
+if __name__ == "__main__":
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument('folder', type=str, nargs='?', help='the folder to create CMakeLists.txt\'s for', default=None)
+    args = vars(parser.parse_args())
+
+    # default to the dumux folder (relative path to the location of this script)
+    if args['folder'] is None:
+        rootDir = os.path.dirname(os.path.abspath(__file__)) + "/../../dumux"
+    else:
+        rootDir = args['folder']
+
+
+    ignore_folders = ["", "io/format/fmt", "io/xml"]
+    extensions = [".hh", ".inc"]
+    for fullFolderName, subFolders, files in os.walk(rootDir):
+        # alphabetically sort
+        subFolders = sorted(subFolders)
+        files = sorted(files)
+        # get folder name relative to dumux
+        folderName = fullFolderName.replace(rootDir + '/', '').replace(rootDir, '')
+        if folderName not in ignore_folders:
+            with open(fullFolderName + "/CMakeLists.txt", "w") as cmakelists:
+                # add subfolders
+                for subFolder in subFolders:
+                    cmakelists.write("add_subdirectory({})\n".format(subFolder))
+
+                headersExist = False
+                for fileName in files:
+                    ext = os.path.splitext(fileName)[1]
+                    if ext in extensions:
+                        headersExist = True
+                        break
+
+                if headersExist:
+                    if subFolders: cmakelists.write("\n")
+                    # collect all files to be installed in a CMake variable
+                    headers_variable = "DUMUX_" + folderName.upper().replace("/", "_") + "_HEADERS"
+                    cmakelists.write("file(GLOB {}{})\n".format(headers_variable, " *".join([''] + extensions)))
+                    cmakelists.write("install(FILES ${{{}}}\n".format(headers_variable))
+                    cmakelists.write("        DESTINATION ${{CMAKE_INSTALL_INCLUDEDIR}}/dumux/{})\n".format(folderName))
diff --git a/bin/create_dockerimage.py b/bin/create_dockerimage.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed3d002631ff7cf4509d72290730b16dbf7170c4
--- /dev/null
+++ b/bin/create_dockerimage.py
@@ -0,0 +1,152 @@
+#!/usr/bin/env python3
+import os
+import sys
+import string
+import shutil
+import argparse
+import subprocess
+from util.moduleinfo import getModuleFile
+from util.moduleinfo import extractModuleInfos
+
+# require python 3
+if sys.version_info[0] < 3:
+    sys.exit("\nERROR: Python3 required")
+
+if __name__ == "__main__":
+
+    # input argument parser
+    parser = argparse.ArgumentParser(
+        description="Create a docker image for a given module and install script."
+    )
+
+    parser.add_argument('-m', '--modulepath',
+                        required=True,
+                        help='the path to the your module')
+    parser.add_argument('-i', '--installScript',
+                        required=True,
+                        help="Specify the installation script")
+    parser.add_argument('-t', '--templateFolder',
+                        required=False,
+                        help="Specify the folder with the template files")
+
+    args = vars(parser.parse_args())
+
+    # get information on the module
+    modulePath = os.path.abspath(args['modulepath'])
+    modInfo = extractModuleInfos(getModuleFile(modulePath),
+                                ['Module', 'Maintainer'])
+    moduleName = modInfo['Module']
+    moduleMaintainer = modInfo['Maintainer']
+    dockerTag = moduleName.lower()  # docker only supports lower case
+
+    # get folder with the template files
+    templateFolder = args['templateFolder']
+    if not templateFolder:
+        templateFolder = os.path.join(modulePath, '../dumux/docker')
+    if not os.path.exists(templateFolder):
+        sys.exit("Template folder {} could not be found".format(templateFolder))
+
+    print("*"*54)
+    print("\n-- Creating a Docker image for module " + moduleName + " --\n")
+    print("*"*54)
+
+    if os.path.exists("docker"):
+        print("\nA docker folder already exists. "
+            "Continue anyway? - will be overwritten - [y/N]\n")
+        delete = input()
+        if delete == "y" or delete == "Y":
+            shutil.rmtree("docker")
+            print("--> Deleted old docker folder.")
+        else:
+            sys.exit("Abort.")
+
+    os.mkdir("docker")
+    print("--> Created the folder 'docker'.")
+
+    # copy install script into docker folder and make it executable
+    installScriptPath = args['installScript']
+    installScriptName = os.path.split(installScriptPath)[1]
+    installScript = os.path.join(os.path.join(os.getcwd(), 'docker'),
+                                installScriptName)
+    shutil.copy(installScriptPath, installScript)
+    os.system("chmod +x {}".format(installScript))
+    print("--> Using install script: {} to install dependencies for module {}."
+        .format(installScript, moduleName))
+
+
+    # substitute content from template and write to target
+    def substituteAndWrite(template, target, mapping):
+        if not os.path.exists(template):
+            sys.exit("Template file '" + template + "' could not be found")
+        with open(target, 'w') as targetFile:
+            raw = string.Template(open(template).read())
+            targetFile.write(raw.substitute(**mapping))
+
+
+    # write setpermissions helper script
+    template = os.path.join(templateFolder, 'setpermissions.sh.template')
+    target = os.path.join(os.getcwd(), 'docker/setpermissions.sh')
+    substituteAndWrite(template, target, {})
+    print("--> Created permission helper script for easier container setup.")
+
+    # write welcome message file
+    template = os.path.join(templateFolder, 'WELCOME.template')
+    target = os.path.join(os.getcwd(), 'docker/WELCOME')
+    substituteAndWrite(template, target,
+                    {'modName': moduleName, 'modFolder': moduleName})
+    print("--> Created welcome message displayed on Docker container startup.")
+
+    # write readme file
+    template = os.path.join(templateFolder, 'README.md.template')
+    target = os.path.join(os.getcwd(), 'docker/README.md')
+    substituteAndWrite(template, target,
+                    {'modName': moduleName, 'dockerTag': dockerTag})
+    print("--> Created README.md on how to use the docker image.")
+
+    # write helper file for container spin-up (make it executable after creation)
+    template = os.path.join(templateFolder, 'docker.sh.template')
+    target = os.path.join(os.getcwd(), 'docker/docker_{}.sh'.format(dockerTag))
+    substituteAndWrite(template, target, {'dockerTag': dockerTag})
+    os.system("chmod +x " + target)
+    print("--> Created helper script to spin up the docker container.")
+
+    # write the docker file
+    template = os.path.join(templateFolder, 'Dockerfile.template')
+    target = os.path.join(os.getcwd(), 'docker/Dockerfile')
+    substituteAndWrite(template, target,
+                    {
+                        'modName': moduleName,
+                        'modMaintainer': moduleMaintainer,
+                        'dockerTag': dockerTag,
+                        'instScript': installScriptName
+                    })
+    print("--> Created Dockerfile. You can adapt it to your needs.")
+    print()
+    print("Do you want to directly build the Docker image? [y/N]")
+
+    build = input()
+    if build == "y" or build == "Y":
+        print("Building Docker image... this may take several minutes.")
+        try:
+            os.chdir('docker')
+            subprocess.run(['docker', 'build',
+                            '-f', 'Dockerfile',
+                            '-t', dockerTag, '.'], check=True)
+            os.chdir('../')
+        except Exception:
+            os.chdir('../')
+            sys.exit("ERROR: docker image build failed")
+
+        print()
+        print("Successfully built image: {}. "
+            "Have a look at docker/README.md.".format(dockerTag))
+        print("Check the container by running "
+            "'docker run -it {} /bin/bash' in the same".format(dockerTag))
+        print("directory as the Dockerfile, and try using the convenience script "
+            "docker_{}.sh".format(dockerTag))
+        print("See docker/README.md for more information.")
+    else:
+        print("You can build your Docker image later by running "
+            "'docker build -f Dockerfile -t {}'".format(dockerTag))
+        print("from within the folder 'docker' that was created by this script, "
+            "and in which you should find the 'Dockerfile'.")
diff --git a/bin/extract_as_new_module.py b/bin/extract_as_new_module.py
new file mode 100755
index 0000000000000000000000000000000000000000..cca14fc2b8694d9ee9e8fc4416915d5ea5befc8c
--- /dev/null
+++ b/bin/extract_as_new_module.py
@@ -0,0 +1,706 @@
+#!/usr/bin/env python3
+"""
+This script extracts some specified applications into a separate Dune module.
+For example make a dumux-pub repository accompanying a scientific paper.
+"""
+
+import os
+import os.path
+import subprocess
+import argparse
+import shutil
+import textwrap
+import itertools
+import multiprocessing as mp
+from distutils.dir_util import copy_tree
+from pathlib import Path
+from functools import partial
+
+from util.moduleinfo import getDependencies
+from util.common import (
+    callFromPath,
+    runCommand,
+    userQuery,
+    queryYesNo,
+    includedCppProjectHeaders,
+    findMatchingFiles,
+    versionTable,
+)
+from util.installscript import (
+    makeInstallScript,
+    supportedLanguages,
+    getScriptExtension,
+    filterDependencies,
+    addDependencyVersions,
+    addDependencyPatches,
+    makeScriptWriter,
+)
+
+
+def readmeFileName():
+    """The default readme filename"""
+    return "README.md"
+
+
+def makeStringList(items, indentation="   "):
+    """Make a markdown list given the items"""
+    return "\n".join([(indentation + "- " + str(it)) for it in items])
+
+
+def replaceFileContent(file, newContent):
+    """Replace the files content with the new content"""
+    with open(file, "w") as newFile:
+        newFile.write(newContent)
+
+
+def appendFileContent(file, content):
+    """Append some content to the given file"""
+    with open(file, "a") as newFile:
+        newFile.write(content)
+
+
+def isInSubTree(file, base):
+    """Check if a file is in the folder tree starting from base"""
+    return Path(base).resolve() in Path(file).resolve().parents
+
+
+def removeRedundantFolders(folders):
+    """Remove folders that are duplicates or that are contained in a parent folder"""
+    uniqueFolders = list(set(folders))
+    return [
+        sf
+        for sf in uniqueFolders
+        if not any(isInSubTree(sf, base) for base in uniqueFolders)
+    ]
+
+
+def checkModuleFolder(moduleDirectory):
+    """Verify a module folder (module to be extracted from)"""
+    if not os.path.isdir(moduleDirectory):
+        raise Exception(
+            f"Module folder {moduleDirectory} not found. "
+            f"Make sure to run this script from one level above {moduleDirectory}."
+        )
+
+
+def checkSubFolders(moduleDirectory, subDirectories):
+    """Verify that subfolders exist in the module"""
+    for subDirectory in subDirectories:
+        path = Path(moduleDirectory) / Path(subDirectory)
+        errMsg = "Cannot handle the given folder {}".format(str(path))
+        if not path.exists():
+            raise Exception(errMsg + " because it does not exist.")
+        if not path.is_dir():
+            raise Exception(errMsg + " because it is not a directory.")
+
+
+def extractSourceFiles(moduleDirectory, subFolder):
+    """Find all applications (*.cc files) in the given subfolder"""
+    sources = []
+    for folder in subFolder:
+        folderPath = os.path.abspath(os.path.join(moduleDirectory, folder))
+        curSources = findMatchingFiles(folderPath, "*.cc")
+        sources += [os.path.normpath(os.path.join(folderPath, s)) for s in curSources]
+
+    if not sources:
+        raise Exception(
+            "No sources found in the provided subFolder.",
+            f" Run '{os.path.abspath(__file__)} --help' for details.",
+        )
+
+    return sources
+
+
+def runDuneProject():
+    """Run the duneproject script"""
+    duneProjectCommand = shutil.which("duneproject", path="dune-common/bin")
+    if duneProjectCommand is None:
+        raise IOError("Could not find 'duneproject' in dune-common/bin.")
+    try:
+        subprocess.run([duneProjectCommand], check=True)
+    except Exception as exc:
+        raise Exception("Error during creation of the new module.") from exc
+
+
+def detectNewModule():
+    """
+    Detect a new module created by dunecontrol (using the timestamp)
+    This has to be used right after runDuneProject.
+    """
+
+    print("\nDetecting the newly created module")
+    newModule = max([d for d in os.listdir() if os.path.isdir(d)], key=os.path.getmtime)
+
+    isCorrectModule = queryYesNo(
+        f"Found '{newModule}' to be the new module. Is this correct?", default=None
+    )
+    if not isCorrectModule:
+        newModule = userQuery("Please provide the name of the new module:")
+
+    duneModuleFile = f"{newModule}/dune.module"
+    if not os.path.exists(duneModuleFile):
+        print(f"Could not find module file {duneModuleFile}")
+    return newModule
+
+
+def copySubFolders(subFolder, oldPath, newPath):
+    """Copy folders from old path to new path"""
+    for b in subFolder:
+        copy_tree(os.path.join(oldPath, b), os.path.join(newPath, b))
+
+
+def addFoldersToCMakeLists(modulePath, subFolder):
+    """Make sure CMakeLists.txt files exist and contain the necessary add_subdirectory calls"""
+    for subFolderPATH in subFolder:
+        nestedLevel = subFolderPATH.count(os.sep) + 1
+        cmakeListsFile = os.path.join(modulePath, "CMakeLists.txt")
+        for i in range(nestedLevel):
+            if os.path.exists(cmakeListsFile):
+                with open(cmakeListsFile, "r") as cml:
+                    lines = list(reversed(cml.readlines()))
+
+                idx = -1
+                for line in lines:
+                    if line.startswith("add_subdirectory"):
+                        idx = lines.index(line)
+                        break
+
+                newLines = lines[0:idx]
+                newLines += [
+                    f"add_subdirectory({subFolderPATH.split(os.path.sep)[i]})\n"
+                ]
+                newLines += lines[idx:]
+                newContent = "".join(line for line in reversed(newLines))
+
+                replaceFileContent(cmakeListsFile, newContent)
+            else:
+                with open(cmakeListsFile, "w") as cml:
+                    cml.write(
+                        f"add_subdirectory({subFolderPATH.split(os.path.sep)[i]})\n"
+                    )
+            cmakeListsFile = os.path.join(
+                os.path.dirname(cmakeListsFile), subFolderPATH.split(os.path.sep)[i], "CMakeLists.txt"
+            )
+
+
+def findHeaders(modulePath, sourceFiles):
+    """Find header included (recursively) in the given source files"""
+    with mp.Pool() as p:
+        headers = itertools.chain.from_iterable(
+            p.map(
+                partial(includedCppProjectHeaders, projectBase=modulePath), sourceFiles
+            )
+        )
+    return list(set(headers))
+
+
+def copyFiles(filePaths, oldModulePath, newModulePath):
+    """Copy files from the old to the new module"""
+    newFiles = []
+    for filePath in filePaths:
+        newDirectory = os.path.join(
+            newModulePath, os.path.relpath(os.path.dirname(filePath), oldModulePath)
+        )
+        newFile = os.path.join(newModulePath, os.path.relpath(filePath, oldModulePath))
+
+        newFiles.append(newFile)
+        os.makedirs(newDirectory, exist_ok=True)
+        shutil.copy(filePath, newFile)
+    return newFiles
+
+
+def foldersWithoutSourceFiles(modulePath, checkSubFolder, sources):
+    """Find those folders that do not contain a *.cc source file (application)"""
+    sourceDirectories = [os.path.dirname(s) for s in sources]
+    sourceDirectories = list(set(sourceDirectories))
+
+    def hasChildSourceDirectory(directory):
+        return any(isInSubTree(s, directory) for s in sourceDirectories)
+
+    def isNotASourceDirectory(directory):
+        return directory not in sourceDirectories and not hasChildSourceDirectory(
+            directory
+        )
+
+    noSourceDirectories = []
+    for sf in checkSubFolder:
+        for root, dirs, _ in os.walk(os.path.join(modulePath, sf)):
+            for directory in dirs:
+                directory = os.path.join(root, directory)
+                if isNotASourceDirectory(directory):
+                    noSourceDirectories.append(directory)
+    noSourceDirectories = removeRedundantFolders(noSourceDirectories)
+
+    def removeEmptyParents():
+        folderMap = {}
+        for f in noSourceDirectories:
+            parent = os.path.dirname(f)
+            if parent not in folderMap:
+                folderMap[parent] = []
+            folderMap[parent].append(f)
+
+        for parent, folders in folderMap.items():
+            found = set(folders)
+            for root, dirs, _ in os.walk(parent):
+                dirs = [os.path.join(root, d) for d in dirs]
+                if set(dirs) == found and isNotASourceDirectory(parent):
+                    for entry in found:
+                        noSourceDirectories.remove(entry)
+                    noSourceDirectories.append(parent)
+        return noSourceDirectories
+
+    return removeEmptyParents()
+
+
+def removeFolder(modulePath, subFolder):
+    """Remove a folder from the newly created module (adjust CMakeLists.txt)"""
+
+    def removeAddSubdirectoryCommand(cmakeLists, folder):
+        with open(cmakeLists, "r") as cml:
+            content = cml.read()
+
+        key = f"add_subdirectory({folder})\n"
+        if key in content:
+            replaceFileContent(cmakeLists, content.replace(key, ""))
+            return True
+        return False
+
+    subFolderPath = os.path.abspath(os.path.join(modulePath, subFolder))
+    subFolderName = os.path.basename(subFolderPath.rstrip(os.sep))
+
+    mainCML = os.path.join(modulePath, "CMakeLists.txt")
+    parentCML = os.path.join(subFolderPath, "../CMakeLists.txt")
+    if not removeAddSubdirectoryCommand(mainCML, subFolder):
+        if os.path.exists(parentCML):
+            removeAddSubdirectoryCommand(parentCML, subFolderName)
+    shutil.rmtree(subFolderPath)
+
+
+def guideFolderDeletion(modulePath, candidates):
+    """Interactive process to delete folders that may not be needed"""
+    candidateList = makeStringList(candidates)
+    print(
+        "\n"
+        "Could not automatically determine if the following directories\n"
+        "contain data that is essential for the extracted applications:\n"
+        "\n"
+        f"{candidateList}\n"
+    )
+
+    deleted = []
+    if queryYesNo(
+        "Do you want to remove some of them "
+        "(by choosing 'no' they are all preserved)?",
+        default="no",
+    ):
+        for folder in foldersWithoutSources:
+            if queryYesNo(f"Do you want to delete the folder {folder}?", default="yes"):
+                relativePath = os.path.relpath(folder, modulePath)
+                removeFolder(newModulePath, relativePath)
+                deleted.append(relativePath)
+    return deleted
+
+
+def pubProjectURL(projectName):
+    """Default URL for dumux-pub modules"""
+    baseURL = "https://git.iws.uni-stuttgart.de/dumux-pub/"
+    return baseURL + "{}.git".format(projectName.lower())
+
+
+def queryEmptyRemoteURL():
+    """Interactive process to determine the remote URL of the new repository"""
+    while True:
+        if queryYesNo("Is your repository hosted in dumux-pub?"):
+            project = input(
+                "Please provide the name of your project "
+                "(usually AuthorLastNameYearX, e.g. Luigi2020b)\n"
+            )
+            remote = pubProjectURL(project)
+        else:
+            remote = input("Please provide the URL of your repository:\n")
+
+        try:
+            print("Checking the repo (you may have to introduce credentials):")
+            remoteContent = runCommand(
+                "git ls-remote {}".format(remote), suppressTraceBack=True
+            )
+        except subprocess.CalledProcessError:
+            print(" - Could not find your repo at {}. ".format(remote))
+            print(" - Please revisit the provided information.")
+            continue
+
+        if remoteContent == "":
+            return remote
+
+        print(
+            "- Remote repository is not empty. Cannot push to non-empty repository.\n"
+            "- Please provide a URL of an empty/bare Git repository."
+        )
+
+
+def runGitCommand(path, cmd):
+    """Helper function to call and log git commands"""
+    print(f"Running {cmd} (you may have to provide your credentials)")
+    callFromPath(path)(runCommand)(cmd)
+
+
+def pushRepository(modulePath, remoteURL, defaultBranch="master"):
+    """Push to the main branch of the new repository"""
+    runGitCommand(modulePath, f"git push -u {remoteURL} {defaultBranch}")
+
+
+def guideRepositoryInitialization(modulePath):
+    """Initialize new module as git repo (and push if remote is provided)"""
+    hasRepo = queryYesNo(
+        "Do you have an empty remote repository to push the code to?"
+        " (If not, we recommend that you create one and answer with 'yes')"
+    )
+    remoteURL = None if not hasRepo else queryEmptyRemoteURL()
+
+    runGitCommand(modulePath, "git init")
+    runGitCommand(modulePath, "git add .")
+    runGitCommand(modulePath, 'git commit -m "Initial commit"')
+
+    if hasRepo:
+        runGitCommand(modulePath, "git remote add origin {}".format(remoteURL))
+        pushRepository(modulePath, remoteURL)
+
+    return remoteURL
+
+
+def dependenciesAndPatches(modulePath, skip=[]):
+    """Determine the module's dependencies"""
+    try:
+        print(
+            f"Determining dependencies of the module: {modulePath}..."
+            " this may take several minutes"
+        )
+        deps = getDependencies(modulePath)
+        deps = filterDependencies(deps, skip)
+        deps = addDependencyVersions(deps, ignoreUntracked=True)
+        deps = addDependencyPatches(deps)
+    except Exception as exc:
+        raise Exception("Error getting the dependencies.") from exc
+    return deps
+
+
+def guideVersionsReadme(modulePath, dependencies, readme, remoteURL=None):
+    """Write detailed version information to the new readme file"""
+    writeVersionInfo = queryYesNo(
+        "Write detailed version information"
+        f" (folder/branch/commits/dates) into {readmeFileName()}?"
+    )
+    if writeVersionInfo:
+        table = versionTable(dependencies)
+        appendFileContent(readme, f"\n## Version Information\n\n{table}\n")
+        runGitCommand(
+            modulePath, f'git commit {readme} -m "[readme] Update version information"'
+        )
+
+        if remoteURL:
+            pushRepository(modulePath, remoteURL)
+
+
+def guideInstallScriptGeneration(modulePath, dependencies, scriptNameBody):
+    """Interactively add an install script (or not)"""
+    language = userQuery(
+        "In which language would you like to generate the install script?"
+        " (choose 'none' if you don't want an installation script)",
+        supportedLanguages() + ["none"],
+    )
+
+    if language == "none":
+        return None
+
+    installScriptName = scriptNameBody + getScriptExtension(language)
+
+    try:
+        makeInstallScript(
+            modPath=modulePath,
+            dependencies=dependencies,
+            scriptName=installScriptName,
+            writer=makeScriptWriter(language),
+            topFolderName="",
+        )
+    except Exception as e:
+        print(f"Error during install script generation: {e}")
+
+    return installScriptName
+
+
+def processInstallScript(script, modulePath, readme, remoteURL=None):
+    """Add install script and installation instructions to the new module"""
+    newScript = os.path.join(modulePath, script)
+    shutil.move(script, newScript)
+    subprocess.call(["chmod", "u+x", newScript])
+    runGitCommand(modulePath, f"git add {script}")
+    runGitCommand(modulePath, 'git commit -m "Add installation script"')
+
+    appendFileContent(readme, infoReadmeInstallation(remoteURL, script, newModuleName))
+    runGitCommand(
+        modulePath,
+        f'git commit {readme} -m "[readme] Update installation instructions"',
+    )
+
+    if remoteURL:
+        pushRepository(modulePath, remoteURL)
+    else:
+        print(
+            "\n"
+            "Please remember to manually fix the installation instructions\n"
+            "once you know the remote URL where your repository will be hosted"
+        )
+
+
+def noRemoteURLInfo(newModule):
+    """Print information if not remote is present"""
+    return textwrap.dedent(
+        f"""\
+        No remote URL given for new module {newModule}.
+        Please remember to replace the placeholder `$remoteurl`
+        in README.md of the new module {newModule} once a remote is available.
+    """
+    )
+
+
+def infoInitial(moduleDirectory, subFolder, sourceFiles):
+    """Some general information for users of the script"""
+
+    sourcesList = makeStringList(sourceFiles)
+    subFolderList = makeStringList(subFolder)
+
+    return textwrap.dedent(
+        """\
+        This script will extract the following subfolder(s) of
+        the module '{0}':
+        {1}
+
+        and all headers contained in '{0}'
+        that are required to build the exectutables from the sources:
+        {2}
+
+        The extracted files are copied into a new DUNE module retaining the directory
+        structure. The files required for creating a working DUNE module (such as
+        CMakeLists.txt) will be created and/or updated.
+
+        In the next step, the 'duneproject' script will be executed to guide the
+        creation of your new DUNE module. Please answer all upcoming queries to the
+        best of your knowledge.
+
+        Important: the new module should NOT depend on the module '{0}'
+    """
+    ).format(moduleDirectory, subFolderList, sourcesList)
+
+
+def infoReadmeMain(moduleDirectory, subFolder, sourceFiles):
+    """Main part of the README.md document"""
+
+    def relativePath(p):
+        return os.path.relpath(p, moduleDirectory)
+
+    subFolderString = "".join([f"*   `{d}`\n" for d in subFolder])
+    sourceString = "".join([f"*   `{relativePath(s)}`\n" for s in sourceFiles])
+
+    return textwrap.dedent(
+        """\
+        This file has been created automatically. Please adapt it to your needs.
+
+        ## Content
+
+        The content of this DUNE module was extracted from the module `{0}`.
+        In particular, the following subFolder of `{0}` have been extracted:
+        {1}
+
+        Additionally, all headers in `{0}` that are required to build the
+        executables from the sources
+        {2}
+
+        have been extracted. You can configure the module just like any other DUNE
+        module by using `dunecontrol`. For building and running the executables,
+        please go to the build folders corresponding to the sources listed above.
+
+    """
+    ).format(moduleDirectory, subFolderString, sourceString)
+
+
+def infoReadmeInstallation(remoteURL, installScriptName, newModuleName):
+    """Installation part of the README.md document"""
+
+    installScriptPath = os.path.join(newModuleName, installScriptName)
+    remoteHints = ""
+    if not remoteURL:
+        remoteURL = "$remoteurl"
+        remoteHints = "\nImportant: $remoteurl has to be corrected!\n"
+    return textwrap.dedent(
+        """\
+
+        ## Installation
+
+        The installation procedure is done as follows:
+        Create a root folder, e.g. `DUMUX`, enter the previously created folder,
+        clone this repository and use the install script `{0}`
+        provided in this repository to install all dependent modules.
+        {1}
+        ```sh
+        mkdir DUMUX
+        cd DUMUX
+        git clone {2} {3}
+        ./{4}
+        ```
+
+        This will clone all modules into the directory `DUMUX`,
+        configure your module with `dunecontrol` and build tests.
+
+    """
+    ).format(installScriptName, remoteHints, remoteURL, newModuleName, installScriptPath)
+
+
+def infoFinal(newModuleName):
+    """Print success message with information"""
+    return textwrap.dedent(
+        f"""\
+        ========================================================================
+
+        The module was extracted successfully!
+
+        The extracted module is contained in the subfolder '{newModuleName}'.
+        You can configure it with
+        ./dune-common/bin/dunecontrol --opts={newModuleName}/cmake.opts --only={newModuleName} all
+    """
+    )
+
+
+###############
+# Main script #
+if __name__ == "__main__":
+
+    # set script parameters
+    EPILOG = """
+    -----------------------------------------------------------
+    The script has to be called one level above moduleDirectory.
+    At least one of the subFolder (FOLDER_1 [FOLDER_2 ...]) has
+    to contain a source file *.cc of an executable for which
+    you would like to timber a table in dumux-pub.)
+    -----------------------------------------------------------
+
+    Example usage:
+    ./dumux/bin/extractmodule/extract_as_new_module.py dumux-fracture appl test
+
+    (extracts the subFolder appl and test from the module dumux-fracture)
+
+    """
+
+    parser = argparse.ArgumentParser(
+        prog="extract_as_new_module.py",
+        usage="./dumux/bin/extractmodule/extract_as_new_module.py"
+        " module_dir SUBFOLDER_1 [SUBFOLDER_2 ...]",
+        description="This script extracts subFolder of a given DUNE module"
+        " into a new DUNE module.",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog=EPILOG,
+    )
+    parser.add_argument(
+        "module_dir", help="Module from which the subfolder is extracted"
+    )
+    parser.add_argument(
+        "subfolder", nargs="+", help='subfolder(s) of "module_dir" to be extracted'
+    )
+
+    # prepare input
+    args = vars(parser.parse_args())
+    moduleDirectory = args["module_dir"].strip(os.sep)
+    modulePath = os.path.abspath(moduleDirectory)
+    baseFolder = os.path.abspath(os.path.join(moduleDirectory, "../"))
+    subFolder = removeRedundantFolders(args["subfolder"])
+
+    # make sure module directory is valid
+    checkModuleFolder(moduleDirectory)
+    # make sure subdirectories exist
+    checkSubFolders(moduleDirectory, subFolder)
+
+    # find main source files (i.e. applications) to extract
+    sourceFiles = extractSourceFiles(moduleDirectory, subFolder)
+
+    # guide user through new module creation
+    print(infoInitial(moduleDirectory, subFolder, sourceFiles))
+    input(
+        "Please read the above carefully and press [Enter] to proceed or abort with [Ctrl-C]..."
+    )
+
+    # duneproject creates a new Dune module
+    runDuneProject()
+
+    # find the new module (newest folder) created by duneproject
+    # we could also parse the output of duneproject but this solution
+    # seems easier to code
+    newModuleName = detectNewModule()
+    newModulePath = os.path.join(baseFolder, newModuleName)
+
+    # prepare all data in new module
+    copySubFolders(subFolder, modulePath, newModulePath)
+    addFoldersToCMakeLists(newModulePath, subFolder)
+
+    # find all headers necessary to build the applications
+    headers = findHeaders(modulePath, sourceFiles)
+    newHeaders = copyFiles(headers, modulePath, newModulePath)
+    newSourceFiles = copyFiles(sourceFiles, modulePath, newModulePath)
+
+    # copy the gitignore file from dumux
+    copyFiles(
+        [os.path.join(baseFolder, "dumux/.gitignore")],
+        os.path.join(baseFolder, "dumux"),
+        newModulePath,
+    )
+
+    # copy the cmake.opts file from dumux
+    copyFiles(
+        [os.path.join(baseFolder, "dumux/cmake.opts")],
+        os.path.join(baseFolder, "dumux"),
+        newModulePath,
+    )
+
+    # guide user through removal of possibly unnecessary folders
+    foldersWithoutSources = foldersWithoutSourceFiles(
+        newModulePath, subFolder, newHeaders + newSourceFiles
+    )
+
+    actualSubFolder = subFolder
+    if foldersWithoutSources:
+        deletedFolders = guideFolderDeletion(newModulePath, foldersWithoutSources)
+        actualSubFolder = [s for s in subFolder if s not in deletedFolders]
+
+    # remove stuff that is created when running duneproject
+    if os.path.join(newModulePath, "dune") not in foldersWithoutSources:
+        removeFolder(newModulePath, "dune")
+    if os.path.join(newModulePath, "src") not in foldersWithoutSources:
+        removeFolder(newModulePath, "src")
+
+    # delete auto-generated README file
+    duneReadme = os.path.join(newModulePath, "README")
+    if os.path.exists(duneReadme):
+        os.remove(duneReadme)
+
+    # prepare new README.md file
+    newReadme = os.path.join(newModulePath, readmeFileName())
+    replaceFileContent(
+        newReadme, infoReadmeMain(moduleDirectory, actualSubFolder, sourceFiles)
+    )
+
+    # try to initialize repo (to use its URL in later steps)
+    remoteURL = guideRepositoryInitialization(newModulePath)
+    if not remoteURL:
+        print(noRemoteURLInfo(newModuleName))
+
+    # make install script & finalize readme
+    deps = dependenciesAndPatches(newModulePath, [modulePath])
+    if deps:
+        guideVersionsReadme(newModulePath, deps, newReadme, remoteURL)
+
+        installScript = "install_" + newModuleName
+        installScript = guideInstallScriptGeneration(newModulePath, deps, installScript)
+        if installScript is not None:
+            processInstallScript(installScript, newModulePath, newReadme, remoteURL)
+    else:
+        print("No dependencies found. Skipping install script generation")
+
+    print(infoFinal(newModuleName))
diff --git a/bin/make_installscript.py b/bin/make_installscript.py
new file mode 100755
index 0000000000000000000000000000000000000000..12103d310100d5be9972dc59f02b3b77274b1ce2
--- /dev/null
+++ b/bin/make_installscript.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+
+""""
+Script to generate an install script for a dune-module,
+accounting for non-published commits and local changes
+"""
+
+import sys
+import os
+import argparse
+import subprocess
+
+from util.moduleinfo import getDependencies, getModuleInfo
+from util.installscript import (
+    addDependencyPatches,
+    addDependencyVersions,
+    getDefaultScriptName,
+    filterDependencies,
+    makeInstallScript,
+    makeScriptWriter,
+    printProgressInfo,
+    printFoundDependencies,
+    printFoundVersionInfo,
+    printFinalMessage,
+    supportedLanguages,
+)
+
+
+if __name__ == "__main__":
+
+    ###################
+    # parse arguments
+    parser = argparse.ArgumentParser(
+        description="This script generates an install script for your module, "
+        "taking into account non-published commits & changes.\n"
+        "This expects that all modules are git repositories and "
+        "have a remote origin URL defined."
+    )
+    parser.add_argument(
+        "-p", "--path", required=True, help="The path to your dune module"
+    )
+    parser.add_argument(
+        "-f",
+        "--filename",
+        required=False,
+        help="File in which to write the install script",
+    )
+    parser.add_argument(
+        "-i",
+        "--ignoreuntracked",
+        required=False,
+        action="store_true",
+        help="Use this to ignore untracked files present",
+    )
+    parser.add_argument(
+        "-t",
+        "--topfoldername",
+        required=False,
+        default="DUMUX",
+        help="Folder that the install script creates upon"
+        "execution to install the modules in. If you "
+        "pass an empty string, no folder will be created "
+        "and installation happens in place.",
+    )
+    parser.add_argument(
+        "-o",
+        "--optsfile",
+        required=False,
+        help="Provide custom opts file to be used for project "
+        "configuration. Note that this file is required "
+        "to be contained and committed in the module or "
+        "one of its dependencies.",
+    )
+    parser.add_argument(
+        "-s",
+        "--skipfolders",
+        required=False,
+        nargs="+",
+        help="a list of module folders to be skipped",
+    )
+    parser.add_argument(
+        "-l",
+        "--language",
+        required=False,
+        default="python",
+        choices=supportedLanguages(),
+        help="Language in which to write the install script",
+    )
+
+    cmdArgs = vars(parser.parse_args())
+
+    modPath = cmdArgs["path"]
+    skipFolders = cmdArgs["skipfolders"]
+    if skipFolders:
+        skipFolders = list(set(skipFolders))
+
+    printProgressInfo(["Determining the module dependencies"])
+    deps = getDependencies(modPath, verbose=True, includeSelf=True)
+    deps = filterDependencies(deps, skipFolders)
+    printFoundDependencies(deps)
+    if not deps:
+        sys.exit("No dependencies found. Exiting.")
+
+    printProgressInfo(["Determining the module versions"])
+    deps = addDependencyVersions(deps, cmdArgs.get("ignoreuntracked", False))
+    printFoundVersionInfo(deps)
+
+    printProgressInfo(["Making patches for unpublished & uncommited changes"])
+    deps = addDependencyPatches(deps)
+
+    # actual script generation
+    modPath = os.path.abspath(modPath)
+    modName = getModuleInfo(modPath, "Module")
+    printProgressInfo(
+        [
+            "Creating install script for module '{}' in folder '{}'".format(
+                modName, modPath
+            )
+        ]
+    )
+
+    language = cmdArgs["language"]
+    scriptName = cmdArgs.get("filename", None)
+    if not scriptName:
+        scriptName = getDefaultScriptName(modName, language)
+
+    makeInstallScript(
+        modPath=modPath,
+        dependencies=deps,
+        scriptName=scriptName,
+        writer=makeScriptWriter(language),
+        topFolderName=cmdArgs.get("topfoldername", None),
+        optsFile=cmdArgs.get("optsFile", None),
+    )
+
+    subprocess.call(["chmod", "u+x", scriptName])
+    printProgressInfo([f"Successfully created install script '{scriptName}'"])
+    printFinalMessage(scriptName, cmdArgs.get("topfoldername", None))
diff --git a/bin/util/remove_clutter_after_last_endif.py b/bin/remove_clutter_after_last_endif.py
similarity index 100%
rename from bin/util/remove_clutter_after_last_endif.py
rename to bin/remove_clutter_after_last_endif.py
diff --git a/bin/util/__init__.py b/bin/util/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/bin/util/common.py b/bin/util/common.py
index 70296eba3b87a21baecbb1f0e56fab3af711301d..70a1fe378618c792cb5f8476de90dbf0e3e13bec 100644
--- a/bin/util/common.py
+++ b/bin/util/common.py
@@ -1,8 +1,77 @@
 import os
+import re
 import sys
+import fnmatch
 import functools
 import subprocess
 import traceback
+import datetime
+import shlex
+
+
+TERM_FORMATTING = {
+    "warning": "\033[1;31m",
+    "highlight": "\033[1;34m",
+    "reset": "\033[0m",
+    "none": "",
+}
+
+
+def styledBotPrint(s, style="none", **kwargs):
+    sys.stdout.write("\n🤖 ")
+    sys.stdout.write(TERM_FORMATTING[style])
+    print(s, **kwargs)
+    sys.stdout.write(TERM_FORMATTING["reset"])
+
+
+def addPrefix(prefix, text, separator=' '):
+    return prefix + separator + text
+
+
+def addPrefixToLines(prefix, text, separator=' '):
+    return '\n'.join(
+        addPrefix(prefix, line, separator) for line in text.split('\n')
+    )
+
+
+def escapeCharacter(text, character, escCharacter="\\"):
+    return text.replace(character, f'{escCharacter}{character}')
+
+
+def escapeCharacters(text, characters, escCharacter="\\"):
+    for char in characters:
+        text = escapeCharacter(text, char, escCharacter)
+    return text
+
+
+def indent(text, indentation='  '):
+    text = text.split('\n')
+    text = [indentation + line for line in text]
+    return '\n'.join(text)
+
+
+def makeTable(dictList, config=None, padding=2):
+    if config is None:
+        config = {key: key for d in dictList for key in d}
+
+    def getColWidth(row):
+        return max(len(str(r)) for r in row) + padding*2
+
+    def getCol(key):
+        return [config[key]] + [d.get(key, "") for d in dictList]
+
+    widths = {key: getColWidth(getCol(key)) for key in config}
+
+    def makeRow(rowValues):
+        row = "|"
+        for key in config:
+            row += "{}|".format(rowValues.get(key, "").center(widths[key]))
+        return row
+
+    table = [makeRow({key: config[key] for key in config})]
+    table.append('|' + '|'.join('-'*widths[key] for key in config) + '|')
+    table.extend(makeRow(row) for row in dictList)
+    return '\n'.join(table)
 
 
 def getCommandErrorHints(command):
@@ -13,12 +82,13 @@ def getCommandErrorHints(command):
     return None
 
 
-# execute a command and retrieve the output
-def runCommand(command, suppressTraceBack=False, errorMessage=''):
+def runCommand(command, check=True, suppressTraceBack=False, errorMessage=''):
+    """execute a command and retrieve the output"""
+
     try:
-        return subprocess.run(command,
-                              shell=True, check=True,
-                              text=True, capture_output=True).stdout
+        return subprocess.run(
+            shlex.split(command), check=check, text=True, capture_output=True
+        ).stdout
     except Exception:
         eType, eValue, eTraceback = sys.exc_info()
         if suppressTraceBack:
@@ -35,8 +105,9 @@ def runCommand(command, suppressTraceBack=False, errorMessage=''):
                 print(hints)
 
 
-# decorator to call function from within the given path
 def callFromPath(path):
+    """decorator to call function from within the given path"""
+
     def decorator_callFromPath(callFunc):
         @functools.wraps(callFunc)
         def wrapper_callFromPath(*args, **kwargs):
@@ -47,3 +118,245 @@ def callFromPath(path):
             return result
         return wrapper_callFromPath
     return decorator_callFromPath
+
+
+def userQuery(query, choices=None):
+    """query something from the user"""
+
+    choicesString = ', '.join(str(c) for c in choices) if choices else ''
+    querySuffix = f" (choices: {choicesString})\n" if choices else ' '
+
+    while True:
+        styledBotPrint(f"{query.strip()}{querySuffix}", style="highlight")
+        inp = input()
+
+        if choices and inp not in choices:
+            styledBotPrint(
+                f"Invalid answer: '{inp}'. Choose from {choicesString}.",
+                style="warning"
+            )
+        else:
+            return inp
+
+
+def queryYesNo(question, default="yes"):
+    """query a yes/no answer from the user"""
+
+    affirmative = ["yes", "y", "ye"]
+    negative = ["no", "n"]
+
+    def getChoices():
+        return ", ".join(c for c in affirmative + negative)
+
+    def isAffirmative(choice): return choice in affirmative
+    def isNegative(choice): return choice in negative
+    def isValid(choice): return isAffirmative(choice) or isNegative(choice)
+
+    if default is not None and not isValid(default):
+        raise ValueError("\nInvalid default answer: '{}', choices: '{}'\n"
+                         .format(default, getChoices()))
+
+    if default is None:
+        prompt = " [y/n] "
+    else:
+        prompt = " [Y/n] " if isAffirmative(default) else " [y/N] "
+
+    while True:
+        styledBotPrint(f"{question.strip()}{prompt}", style="highlight", end="")
+        choice = input().lower()
+
+        if default is not None and choice == "":
+            return True if isAffirmative(default) else False
+
+        if not isValid(choice):
+            styledBotPrint(
+                f"Invalid answer: '{choice}'. Choose from '{getChoices()}'",
+                style="warning"
+            )
+        else:
+            return True if isAffirmative(choice) else False
+
+
+def cppHeaderFilter():
+    return lambda fileName: fileName == 'config.h'
+
+
+def includedCppProjectHeaders(file,
+                              projectBase,
+                              headers=[],
+                              headerFilter=cppHeaderFilter()):
+    """get all project headers included by a cpp file"""
+
+    filePath = os.path.join(projectBase, file)
+    if not os.path.exists(filePath):
+        raise IOError(f'Cpp file {filePath} does not exist')
+
+    with open(filePath, 'r') as f:
+        content = f.read()
+        headerInBracket = re.findall(r'#include\s+<(.+?)>', content)
+        headerInQuotation = re.findall(r'#include\s+"(.+?)"', content)
+
+        def process(pathInProject):
+            headerPath = os.path.join(projectBase, pathInProject)
+            if os.path.exists(headerPath):
+                if not headerFilter(pathInProject):
+                    if headerPath not in headers:
+                        headers.append(headerPath)
+                        includedCppProjectHeaders(
+                            headerPath, projectBase,
+                            headers, headerFilter
+                        )
+
+        for header in headerInBracket:
+            process(header)
+        for header in headerInQuotation:
+            absHeaderPath = os.path.join(os.path.dirname(file), header)
+            projectPath = os.path.relpath(absHeaderPath, projectBase)
+            process(projectPath)
+    return headers
+
+
+def findMatchingFiles(path, pattern):
+    """find all files below the given folder that match the given pattern"""
+
+    result = []
+    for root, dirs, files in os.walk(path):
+        relativeRootPath = os.path.relpath(root, path)
+        for file in files:
+            if fnmatch.fnmatch(file, pattern):
+                result.append(os.path.join(relativeRootPath, file))
+    return result
+
+
+def isGitRepository(pathToRepo='.'):
+    try:
+        run = callFromPath(pathToRepo)(runCommand)
+        run('git status')
+        return True
+    except Exception:
+        return False
+
+
+def getRemote(pathToRepo='.'):
+    run = callFromPath(pathToRepo)(runCommand)
+    return run('git ls-remote --get-url').strip('\n')
+
+
+def fetchRepo(remote, pathToRepo='.'):
+    run = callFromPath(pathToRepo)(runCommand)
+    run('git fetch {}'.format(remote))
+
+
+def hasUntrackedFiles(pathToRepo='.'):
+    run = callFromPath(pathToRepo)(runCommand)
+    return run('git ls-files --others --exclude-standard') != ''
+
+
+def isPersistentBranch(branchName):
+    if branchName == 'origin/master':
+        return True
+    if branchName.startswith('origin/releases/'):
+        return True
+    return False
+
+
+# get the most recent commit that also exists on remote master/release branch
+# may be used to find a commit we can use as basis for a pub module
+def mostRecentCommonCommitWithRemote(modFolderPath,
+                                     branchFilter=isPersistentBranch):
+    run = callFromPath(modFolderPath)(runCommand)
+
+    def findBranches(sha):
+        candidates = run('git branch -r --contains {}'.format(sha)).split('\n')
+        candidates = [branch.strip().split(' ->')[0] for branch in candidates]
+        return list(filter(branchFilter, candidates))
+
+    revList = run('git rev-list HEAD').split('\n')
+    for rev in revList:
+        branches = findBranches(rev)
+        if branches:
+            return branches[0], rev
+
+    raise RuntimeError('Could not find suitable ancestor commit'
+                       ' on a branch that matches the given filter')
+
+
+# function to extract persistent, remotely available git versions for all
+def getPersistentVersions(modFolderPaths, ignoreUntracked=False):
+
+    result = {}
+    for modFolderPath in modFolderPaths:
+
+        if not isGitRepository(modFolderPath):
+            raise Exception('Folder is not a git repository')
+
+        if hasUntrackedFiles(modFolderPath) and not ignoreUntracked:
+            raise Exception(
+                "Found untracked files in '{}'. "
+                "Please commit, stash, or remove them. Alternatively, if you "
+                "are sure they are not needed set ignoreUntracked=True"
+                .format(modFolderPath)
+            )
+
+        result[modFolderPath] = {}
+        result[modFolderPath]['remote'] = getRemote(modFolderPath)
+
+        # update remote to make sure we find all upstream commits
+        fetchRepo(result[modFolderPath]['remote'], modFolderPath)
+
+        branch, rev = mostRecentCommonCommitWithRemote(modFolderPath)
+        run = callFromPath(modFolderPath)(runCommand)
+
+        result[modFolderPath]['revision'] = rev
+        result[modFolderPath]['date'] = run(
+            'git log -n 1 --format=%ai {}'.format(rev)
+        ).strip('\n')
+        result[modFolderPath]['author'] = run(
+            'git log -n 1 --format=%an {}'.format(rev)
+        ).strip('\n')
+
+        # this may return HEAD if we are on some detached HEAD tree
+        result[modFolderPath]['branch'] = branch
+
+    return result
+
+
+def getPatches(persistentVersions):
+    result = {}
+    for path, gitInfo in persistentVersions.items():
+        run = callFromPath(path)(runCommand)
+
+        uncommittedPatch = run('git diff')
+        unpublishedPatch = run(
+            'git format-patch --stdout {}'.format(gitInfo['revision'])
+        )
+        untrackedPatch = ''
+        untrackedFiles = run('git ls-files --others --exclude-standard')
+        binaryExtension = (
+            '.png', '.gif', '.jpg', '.tiff', '.bmp', '.DS_Store', '.eot', '.otf', '.ttf', '.woff', '.rgb', '.pdf',
+        )
+        if untrackedFiles:
+            for file in untrackedFiles.splitlines():
+                if not str(file).endswith(binaryExtension):
+                    untrackedPatch += run('git --no-pager diff /dev/null {}'.format(file), check=False)
+
+        result[path] = {}
+        result[path]['untracked'] = untrackedPatch if untrackedPatch else None
+        result[path]['unpublished'] = unpublishedPatch if unpublishedPatch else None
+        result[path]['uncommitted'] = uncommittedPatch if uncommittedPatch else None
+    return result
+
+
+def versionTable(versions,
+                 config={
+                     'name': 'module name',
+                     'branch': 'branch name',
+                     'revision': 'commit sha',
+                     'date': 'commit date'
+                 },
+                 padding=2):
+    return makeTable(versions, config)
+
+
+def printVersionTable(versions):
+    print(versionTable(versions=versions))
diff --git a/bin/util/create_cmakelists.py b/bin/util/create_cmakelists.py
deleted file mode 100755
index 63092955909a0558d71614bf82ac4ac1e4c2f096..0000000000000000000000000000000000000000
--- a/bin/util/create_cmakelists.py
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/usr/bin/env python
-
-"""
-Create files CMakeLists.txt for the given folder and all subfolders,
-including the add_subdirectory(...) and install(...) commands.
-Defaults to the folder `dumux` that contains the header files,
-if no folder was specified.
-"""
-
-import os
-import argparse
-
-parser = argparse.ArgumentParser()
-parser.add_argument('folder', type=str, nargs='?', help='the folder to create CMakeLists.txt\'s for', default=None)
-args = vars(parser.parse_args())
-
-# default to the dumux folder (relative path to the location of this script)
-if args['folder'] is None:
-    rootDir = os.path.dirname(os.path.abspath(__file__)) + "/../../dumux"
-else:
-    rootDir = args['folder']
-
-
-ignore_folders = ["", "io/format/fmt", "io/xml"]
-extensions = [".hh", ".inc"]
-for fullFolderName, subFolders, files in os.walk(rootDir):
-    # alphabetically sort
-    subFolders = sorted(subFolders)
-    files = sorted(files)
-    # get folder name relative to dumux
-    folderName = fullFolderName.replace(rootDir + '/', '').replace(rootDir, '')
-    if folderName not in ignore_folders:
-        with open(fullFolderName + "/CMakeLists.txt", "w") as cmakelists:
-            # add subfolders
-            for subFolder in subFolders:
-                cmakelists.write("add_subdirectory({})\n".format(subFolder))
-
-            headersExist = False
-            for fileName in files:
-                ext = os.path.splitext(fileName)[1]
-                if ext in extensions:
-                    headersExist = True
-                    break
-
-            if headersExist:
-                if subFolders: cmakelists.write("\n")
-                # collect all files to be installed in a CMake variable
-                headers_variable = "DUMUX_" + folderName.upper().replace("/", "_") + "_HEADERS"
-                cmakelists.write("file(GLOB {}{})\n".format(headers_variable, " *".join([''] + extensions)))
-                cmakelists.write("install(FILES ${{{}}}\n".format(headers_variable))
-                cmakelists.write("        DESTINATION ${{CMAKE_INSTALL_INCLUDEDIR}}/dumux/{})\n".format(folderName))
diff --git a/bin/util/createdockerimage.py b/bin/util/createdockerimage.py
deleted file mode 100644
index 96619ea5f35c8d5ab09edf7381818c28bb38c786..0000000000000000000000000000000000000000
--- a/bin/util/createdockerimage.py
+++ /dev/null
@@ -1,150 +0,0 @@
-#!/usr/bin/env python3
-import os
-import sys
-import string
-import shutil
-import argparse
-import subprocess
-from getmoduleinfo import getModuleFile
-from getmoduleinfo import extractModuleInfos
-
-# require python 3
-if sys.version_info[0] < 3:
-    sys.exit("\nERROR: Python3 required")
-
-# input argument parser
-parser = argparse.ArgumentParser(
-    description="Create a docker image for a given module and install script."
-)
-
-parser.add_argument('-m', '--modulepath',
-                    required=True,
-                    help='the path to the your module')
-parser.add_argument('-i', '--installScript',
-                    required=True,
-                    help="Specify the installation script")
-parser.add_argument('-t', '--templateFolder',
-                    required=False,
-                    help="Specify the folder with the template files")
-
-args = vars(parser.parse_args())
-
-# get information on the module
-modulePath = os.path.abspath(args['modulepath'])
-modInfo = extractModuleInfos(getModuleFile(modulePath),
-                             ['Module', 'Maintainer'])
-moduleName = modInfo['Module']
-moduleMaintainer = modInfo['Maintainer']
-dockerTag = moduleName.lower()  # docker only supports lower case
-
-# get folder with the template files
-templateFolder = args['templateFolder']
-if not templateFolder:
-    templateFolder = os.path.join(modulePath, '../dumux/docker')
-if not os.path.exists(templateFolder):
-    sys.exit("Template folder {} could not be found".format(templateFolder))
-
-print("*"*54)
-print("\n-- Creating a Docker image for module " + moduleName + " --\n")
-print("*"*54)
-
-if os.path.exists("docker"):
-    print("\nA docker folder already exists. "
-          "Continue anyway? - will be overwritten - [y/N]\n")
-    delete = input()
-    if delete == "y" or delete == "Y":
-        shutil.rmtree("docker")
-        print("--> Deleted old docker folder.")
-    else:
-        sys.exit("Abort.")
-
-os.mkdir("docker")
-print("--> Created the folder 'docker'.")
-
-# copy install script into docker folder and make it executable
-installScriptPath = args['installScript']
-installScriptName = os.path.split(installScriptPath)[1]
-installScript = os.path.join(os.path.join(os.getcwd(), 'docker'),
-                             installScriptName)
-shutil.copy(installScriptPath, installScript)
-os.system("chmod +x {}".format(installScript))
-print("--> Using install script: {} to install dependencies for module {}."
-      .format(installScript, moduleName))
-
-
-# substitute content from template and write to target
-def substituteAndWrite(template, target, mapping):
-    if not os.path.exists(template):
-        sys.exit("Template file '" + template + "' could not be found")
-    with open(target, 'w') as targetFile:
-        raw = string.Template(open(template).read())
-        targetFile.write(raw.substitute(**mapping))
-
-
-# write setpermissions helper script
-template = os.path.join(templateFolder, 'setpermissions.sh.template')
-target = os.path.join(os.getcwd(), 'docker/setpermissions.sh')
-substituteAndWrite(template, target, {})
-print("--> Created permission helper script for easier container setup.")
-
-# write welcome message file
-template = os.path.join(templateFolder, 'WELCOME.template')
-target = os.path.join(os.getcwd(), 'docker/WELCOME')
-substituteAndWrite(template, target,
-                   {'modName': moduleName, 'modFolder': moduleName})
-print("--> Created welcome message displayed on Docker container startup.")
-
-# write readme file
-template = os.path.join(templateFolder, 'README.md.template')
-target = os.path.join(os.getcwd(), 'docker/README.md')
-substituteAndWrite(template, target,
-                   {'modName': moduleName, 'dockerTag': dockerTag})
-print("--> Created README.md on how to use the docker image.")
-
-# write helper file for container spin-up (make it executable after creation)
-template = os.path.join(templateFolder, 'docker.sh.template')
-target = os.path.join(os.getcwd(), 'docker/docker_{}.sh'.format(dockerTag))
-substituteAndWrite(template, target, {'dockerTag': dockerTag})
-os.system("chmod +x " + target)
-print("--> Created helper script to spin up the docker container.")
-
-# write the docker file
-template = os.path.join(templateFolder, 'Dockerfile.template')
-target = os.path.join(os.getcwd(), 'docker/Dockerfile')
-substituteAndWrite(template, target,
-                   {
-                       'modName': moduleName,
-                       'modMaintainer': moduleMaintainer,
-                       'dockerTag': dockerTag,
-                       'instScript': installScriptName
-                   })
-print("--> Created Dockerfile. You can adapt it to your needs.")
-print()
-print("Do you want to directly build the Docker image? [y/N]")
-
-build = input()
-if build == "y" or build == "Y":
-    print("Building Docker image... this may take several minutes.")
-    try:
-        os.chdir('docker')
-        subprocess.run(['docker', 'build',
-                        '-f', 'Dockerfile',
-                        '-t', dockerTag, '.'], check=True)
-        os.chdir('../')
-    except Exception:
-        os.chdir('../')
-        sys.exit("ERROR: docker image build failed")
-
-    print()
-    print("Successfully built image: {}. "
-          "Have a look at docker/README.md.".format(dockerTag))
-    print("Check the container by running "
-          "'docker run -it {} /bin/bash' in the same".format(dockerTag))
-    print("directory as the Dockerfile, and try using the convenience script "
-          "docker_{}.sh".format(dockerTag))
-    print("See docker/README.md for more information.")
-else:
-    print("You can build your Docker image later by running "
-          "'docker build -f Dockerfile -t {}'".format(dockerTag))
-    print("from within the folder 'docker' that was created by this script, "
-          "and in which you should find the 'Dockerfile'.")
diff --git a/bin/util/extractmodulepart.py b/bin/util/extractmodulepart.py
deleted file mode 100755
index fbd7cfc69223df929214d97f8f575155a14944a8..0000000000000000000000000000000000000000
--- a/bin/util/extractmodulepart.py
+++ /dev/null
@@ -1,388 +0,0 @@
-#!/usr/bin/env python3
-import sys, glob, os, subprocess
-import os.path
-import argparse
-import shutil
-from distutils.dir_util import copy_tree
-import re
-import threading
-import fnmatch
-from getmoduleinfo import *
-from getusedversions import *
-
-
-"""
-This is a python script for extracting module
-"""
-
-# check if help is needed
-epilog = '''
------------------------------------------------------------
-The script has to be called one level above module_dir.
-At least one of the subfolders (FOLDER_1 [FOLDER_2 ...]) has
-to contain a source file *.cc of an executable for which
-you would like to timber a table in dumux-pub.)
-'''
-parser = argparse.ArgumentParser(prog='extractmodulepart',
-                                 usage= "./extractmodulepart module_dir SUBFOLDER_1 [SUBFOLDER_2 ...]",
-                                 description='This script extracts a subfolder of a DUNE module',
-                                 formatter_class=argparse.RawDescriptionHelpFormatter,
-                                 epilog=epilog)
-parser.add_argument('module_dir', help='Dune module from which the subfolder is extracted')
-parser.add_argument('subfolder', nargs='+', help = 'subfolder(s) of module_dir which you want to extract')
-args = vars(parser.parse_args())
-
-# function to search the header file efficiently with parallel programming
-def search_headers(c_file):
-    with open(c_file, 'r') as f:
-        content = f.read()
-
-    header_in_bracket = re.findall(r'#include\s+<(.+?)>',content)
-    header_in_quotation = re.findall(r'#include\s+"(.+?)"',content)
-
-    # search for includes relative to the module path
-    for header in header_in_bracket + header_in_quotation:
-        header_with_path = os.path.join(module_full_path, header)
-        if not os.path.exists(header_with_path):
-            continue
-        if header_with_path not in all_headers:
-            all_headers.append(header_with_path)
-            thread_ = threading.Thread(target = search_headers, args = (header_with_path, ))
-            thread_.start()
-
-    # search for includes relative to the path of the including file
-    # only allow quoted includes for this
-    for header in header_in_quotation:
-        if header == "config.h":
-            continue
-        header_dir_name = os.path.dirname(c_file)
-        header_with_path = os.path.join(header_dir_name, header)
-        if not os.path.exists(header_with_path):
-            continue
-        if header_with_path not in all_headers:
-            all_headers.append(header_with_path)
-            thread_ = threading.Thread(target = search_headers, args = (header_with_path, ))
-            thread_.start()
-
-# functions to find the file with specific pattern or name in a direcotry
-def find_files_with_name(name, path):
-    result = []
-    for root, dirs, files in os.walk(path):
-        if name in files:
-            result.append(os.path.join(root, name))
-    return result
-
-def find_files_with_pattern(pattern, path):
-    result = []
-    for root, dirs, files in os.walk(path):
-        for name in files:
-            if fnmatch.fnmatch(name, pattern):
-                result.append(os.path.join(root, name))
-    return result
-
-# functions to configure CMakeLists.txt files
-def generate_new_content(lines, pattern, replace_str):
-    index = lines.find(pattern)
-    if index != -1:
-        content = lines[0: index] + "\n"
-        if replace_str != "":
-            content += replace_str + "\n"
-        flag = True
-        while flag:
-            index = lines.find(")", index)
-            if index != -1:
-                lines = lines[index + 1:]
-            else:
-                break
-
-            index = lines.find(pattern)
-            if index != -1:
-                content += lines[0: index] + "\n"
-            else:
-                content += lines + "\n"
-                flag = False
-    else:
-        if replace_str == "":
-            content = lines
-        else:
-            if pattern == "add_subdirectory(":
-                content = replace_str + "\n" + lines
-            else:
-                content = lines + "\n" + replace_str
-    return content
-
-def generate_subdirectory_content(dirs):
-    content = ""
-    for name in dirs:
-        content += "add_subdirectory(" + name + ")" + "\n"
-    return content
-
-def generate_install_content(header_files, destination):
-    if len(header_files) == 0:
-        return ""
-    content = "install(FILES" + "\n"
-    for header_file in header_files:
-        content += "    " + header_file + "\n"
-    content += "DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}" + destination + ")" + "\n"
-    return content
-
-def generate_content(cmake_list_txt_file, dirs, header_files, destination):
-    subdirectory_content = generate_subdirectory_content(dirs)
-    install_content = generate_install_content(header_files, destination)
-
-    if os.path.exists(cmake_list_txt_file):
-        with open(cmake_list_txt_file, "r", encoding="utf-8") as f:
-            content = "".join(f.readlines()).strip()
-            f.close()
-        content = generate_new_content(content, "add_subdirectory(", subdirectory_content)
-        return generate_new_content(content, "install(FILE", install_content)
-    else:
-        if subdirectory_content == "":
-            return install_content
-        else:
-            return subdirectory_content + "\n" + install_content
-
-def generate_cmake_lists_txt(cmake_list_txt_file, dirs, header_files, destination):
-    content = generate_content(cmake_list_txt_file, dirs, header_files, destination)
-
-    with open(cmake_list_txt_file, "w", encoding="utf-8") as f:
-        if content != "":
-            f.write(content)
-        f.close()
-
-def check_dir(root_dir):
-    if not os.path.exists(root_dir):
-        print("root path" + str(root_dir) + "is not exist!")
-        return False
-    if not os.path.isdir(root_dir):
-        print("root path" + str(root_dir) + "is not dir!")
-        return False
-    return True
-
-def check_str(root_dir):
-    if root_dir is None or root_dir.strip() == "":
-        return None
-    root_dir = root_dir.strip()
-    if root_dir.endswith(os.sep):
-        root_dir = root_dir[:-1]
-    return root_dir
-
-def generate_cmake_lists_txt_file(root_dir):
-    root_dir = check_str(root_dir)
-    if root_dir is None:
-        return
-    if not check_dir(root_dir):
-        return
-    drop_len = len(root_dir)
-    for parent, dirs, files in os.walk(root_dir):
-        destination = parent[drop_len:]
-        header_files = []
-        cmake_list_txt_file = os.path.join(parent, "CMakeLists.txt")
-        for name in files:
-            for suffix in [".h", ".hh"]:
-                if name.endswith(suffix):
-                    header_files.append(name)
-        generate_cmake_lists_txt(cmake_list_txt_file, dirs, header_files, destination)
-
-def query_yes_no(question, default="yes"):
-    valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
-    if default is None:
-        prompt = " [y/n] "
-    elif default == "yes":
-        prompt = " [y/N] "
-    elif default == "no":
-        prompt = " [Y/n] "
-    else:
-        raise ValueError("invalid default answer: '%s'" % default)
-    while True:
-        sys.stdout.write(question + prompt)
-        choice = input().lower()
-        if default is not None and choice == "":
-            return valid[default]
-        elif choice in valid:
-            return valid[choice]
-        else:
-            sys.stdout.write("Please respond with 'yes' or 'no' " "(or 'y' or 'n').\n")
-
-if __name__ == "__main__":
-
-    module_dir = args['module_dir']
-    subfolders = args['subfolder']
-
-    # if module_dir contains a slash as last character, delete it
-    if module_dir.endswith('/'):
-       module_dir = module_dir[:-1]
-
-    # check if we are above module_dir
-    if not (os.path.isdir(module_dir)):
-        print("ERROR: you need to run the script one level above the folder "+module_dir+".")
-        print("Run \""+os.path.basename(__file__)+" --help\" for details.")
-        exit(1)
-
-    # determine all source files in the paths passed as arguments
-    script_path = os.getcwd()
-    os.chdir(module_dir)
-    module_full_path=os.getcwd()
-    all_sources=[]
-    all_sources_with_path = []
-    all_directories=[]
-    for dir_path in subfolders:
-        if dir_path.startswith(module_dir):
-            # the "+1" is required to also remove the "/"
-            stripped_path = dir_path[len(module_dir)+1:]
-        else:
-            stripped_path = dir_path
-        full_path = os.path.join(module_full_path, stripped_path)
-        directories = " " + stripped_path
-        all_directories.append(stripped_path)
-        os.chdir(full_path)
-        for file in find_files_with_pattern("*.cc", full_path):
-            print(file)
-            sources = os.path.relpath(file, module_full_path)
-            print(sources)
-            all_sources.append(sources)
-            sourceswithpath = os.path.join(module_full_path, sources)
-            all_sources_with_path.append(sourceswithpath)
-    os.chdir(module_full_path)
-    os.chdir("..") # back to the script folder
-
-    # check if sources have been obtained
-    if (all_sources == []):
-        print("ERROR: no source files *.cc found in the directories "+ " ".join([str(x) for x in subfolders]) +".")
-        print("Be sure to provide a list of paths as arguments to this script.")
-        print("Run \""+os.path.basename(__file__)+" --help\" for details.")
-        exit(1)
-
-    # try to find the duneproject script
-    dune_project = shutil.which('duneproject', path = "dune-common/bin")
-    if (dune_project == None):
-        print("ERROR: Could not find duneproject.")
-        print("Be sure to either have duneproject in your search path")
-        print("or to run this script from a directory that contains duneproject.")
-        exit(1)
-    else:
-        print(dune_project)
-
-    # give explanations
-    print("\n""This script will\n"
-          "- extract the following sub-folders of "+ module_dir +":\n")
-    for dir_path in all_directories:
-        print("  "+ dir_path + ",")
-    print("\n""  and all headers in "+ module_dir + " that are required to build the\n"
-          "  executables from the sources\n")
-    for source in all_sources:
-        print("  "+ source +",")
-    print("""
-- copy the extracted files into a freshly created DUNE module, retaining the directory structure,
-
-- update/create all required files like CMakeLists.txt, - store the versions of all used Dune module
-
-- and extract their modifications as patches.
-
-Thus, you receive a fully-working DUNE module containing the subset of {0} that is required to run your application.
-duneproject will be run now. The new module should NOT depend on the module in {0}.\n\n""".format(module_dir))
-
-    input("Read the above and press [Enter] to proceed...")
-
-    # run duneproject
-    old_ls = os.listdir()
-    subprocess.call([dune_project])
-    new_ls  = os.listdir()
-
-    # determine the new module/directory name
-    module_name = (set(new_ls) - set(old_ls)).pop()
-    if (module_name  == ""):
-        print("ERROR: could not find new module. Aborting.")
-        exit(1)
-    else:
-        print()
-        print(os.path.basename(__file__) + ": Found new module " + module_name)
-    print("Determining required headers...")
-    os.chdir(module_name)
-    module_path=os.getcwd()
-
-    # extract all headers, provide some output and copy everything to the new module
-    all_headers = []
-    print("The following header files are extracted: ")
-    for source in all_sources_with_path:
-        search_headers(source)
-        for header in all_headers:
-            print(header)
-            dir_path = os.path.dirname(os.path.realpath(header)).replace(module_dir,module_name,1)
-            os.makedirs(dir_path, exist_ok=True)
-            shutil.copy(header,dir_path)
-        source_dir = os.path.dirname(source)
-        source_path = source_dir.replace(module_dir,module_name,1)
-        copy_tree(source_dir, source_path)
-
-    # delete unnecessary directories to keep the extracted module clean
-    shutil.rmtree('dune')
-    shutil.rmtree('src')
-
-    # set CMakeLists.txt for each directory
-    generate_cmake_lists_txt_file(module_path)
-    print("The required header files are extracted and CMakeLists are configured.")
-    print("=============================================================================")
-
-    # create README file
-    os.remove("README")
-    orig_stdout = sys.stdout
-    readme_file = open("README.md", "w+")
-
-    readme_dir_str_lists = []
-    for dir_path in all_directories:
-        readme_dir_str_lists.append("*   `" + dir_path + "`,\n")
-    readme_source_str_lists = []
-    for source in all_sources:
-        readme_source_str_lists.append("*   `" + source + "`,\n")
-    readme_str_args = [module_dir, ''.join(readme_dir_str_lists), ''.join(readme_source_str_lists)]
-
-    readme_file.write("""This file has been created automatically. Please adapt it to your needs. \n
-===============================\n
-## Content\n
-"The content of this DUNE module was extracted from the module `{0}`."
-In particular, the following subfolders of `{0}` have been extracted:
-{1}
-Additionally, all headers in `{0}` that are required to build the
-executables from the sources
-{2}
-have been extracted. You can configure the module just like any other DUNE
-module by using `dunecontrol`. For building and running the executables,
-please go to the build folders corresponding to the sources listed above.\n
-""".format(*readme_str_args))
-
-    version_script_path = os.path.join(script_path, "dumux/bin/util/getusedversions.py")
-    install_script_path = os.path.join(script_path, "dumux/bin/util/makeinstallscript.py")
-    os.chdir(script_path)
-    if query_yes_no("\nWrite detailed version information (folder/branch/commits/dates) into README.md?"):
-        print("Looking for the dune modules in path :" + str(script_path) + "...")
-        readme_file.write("===============================\n" + "## Version Information\n")
-        versions = getUsedVersions([dep['folder'] for dep in getDependencies(module_full_path)], True)
-        print("Writing version information into README.md ...")
-        sys.stdout = readme_file
-        printVersionTable(versions)
-        sys.stdout = orig_stdout
-    readme_file.close()
-    print("Automatic generation of README.md file is complete.")
-    print("=============================================================================")
-
-    install_script_name = 'install_' + module_name + '.sh'
-    if query_yes_no("\nGenerate install script " +  install_script_name + "in your new module " + module_name + "?"):
-        os.system("python3 " + install_script_path + " -p" + os.path.join(script_path,module_name) + " -i -s " + module_name)
-        shutil.move(install_script_name, os.path.join(module_path, install_script_name))
-
-    # output guidence for users
-    print("\n"+"*"*80+"\n"+
-          """The extracted module is contained in the subfolder \"{0}\".
-          You can build it using \"dunecontrol ... —only= {0} all\".
-          BEFORE building, you can add the module to dumux-pub by something like:
-          (Rename module name if it does not match the AuthorLastNameYearx scheme
-          and commit it to the Git repository dumux-pub)
-          git clone https://git.iws.uni-stuttgart.de/dumux-pub/AuthorLastNameYearx.git
-          sed -i '/Module:/c\Module: AuthorLastNameYearx' {0} /dune.module
-          mv {0} /* dumux-pub/AuthorLastNameYearx/.
-          cd AuthorLastNameYearx
-          git commit -a
-          git push""".format(module_name)+
-          "\n"+"*"*80)
-
diff --git a/bin/util/extractmodulepart.sh b/bin/util/extractmodulepart.sh
deleted file mode 100755
index 433a79fcd721b96ec76d9cf3d0dd31a3a0b63fdf..0000000000000000000000000000000000000000
--- a/bin/util/extractmodulepart.sh
+++ /dev/null
@@ -1,398 +0,0 @@
-#! /bin/bash
-
-# check if help is needed
-if test "$1" = "--help" || test "$1" = "-help" \
-   || test "$1" = "help" || test "$1" = ""; then
-  echo ""
-  echo "USAGE: $0 MODULE_DIR FOLDER_1 [FOLDER_2 ...]"
-  echo ""
-  echo "MODULE_DIR is the folder containing the DUNE module from which you"
-  echo "want to extract. The script has to be called one level above it."
-  echo ""
-  echo "The FOLDERs need to indicate subfolders of MODULE_DIR. At least one"
-  echo "of them has to contain a source file *.cc of an executable for which"
-  echo "you would like to timber a table in dumux-pub."
-  exit 0
-fi
-
-MODULE_DIR=$1
-# if MODULE_DIR contains a slash as last character, delete it
-MODULE_DIR=$(echo $MODULE_DIR | sed s'/\/$//')
-# check if we are above MODULE_DIR
-if (! [ -d $MODULE_DIR ]); then
-  echo "ERROR: you need to run the script one level above the folder $MODULE_DIR."
-  echo "Run \"$0 --help\" for details."
-  exit 1
-fi
-
-# determine all source files in the paths passed as arguments
-cd $MODULE_DIR
-MODULE_FULL_PATH=$(pwd)
-ALL_SOURCES=""
-ALL_DIRECTORIES=""
-for DIR_PATH in ${*:2}; do
-  STRIPPED_PATH=$(sed 's%.*'"$MODULE_DIR"'/%%g' <(echo "$DIR_PATH"))
-  ALL_DIRECTORIES="$ALL_DIRECTORIES $STRIPPED_PATH"
-  ALL_SOURCES="$ALL_SOURCES $(find $STRIPPED_PATH -name '*.cc' 2>/dev/null)"
-done
-cd ..
-
-# check if sources have been obtained
-CONTRACTED="$(echo "$ALL_SOURCES" | tr -d " \t\n\r")"
-if test "$CONTRACTED" = ""; then
-  echo "ERROR: no source files *.cc found in the directories ${*:2}."
-  echo "Be sure to provide a list of paths as arguments to this script."
-  echo "Run \"$0 --help\" for details."
-  exit 1;
-fi
-
-# try to find the duneproject script
-if hash duneproject 2>/dev/null; then
-  DUNE_PROJECT="duneproject"
-else
-  DUNE_PROJECT=$(find . -name 'duneproject')
-fi
-if test "$DUNE_PROJECT" = ""; then
-  echo "ERROR: Could not find duneproject."
-  echo "Be sure to either have duneproject in your search path"
-  echo "or to run this script from a directory that contains duneproject."
-  exit 1;
-fi
-
-# give explanations
-echo ""
-echo "This script will"
-echo "- extract the following sub-folders of $MODULE_DIR:"
-echo ""
-for DIR_PATH in $ALL_DIRECTORIES; do
-  echo "  $DIR_PATH,"
-done
-echo ""
-echo "  and all headers in $MODULE_DIR that are required to build the"
-echo "  executables from the sources"
-echo ""
-for SOURCE in $ALL_SOURCES; do
-  echo "  $SOURCE,"
-done
-echo ""
-echo "- copy the extracted files into a freshly created DUNE module, retaining the"
-echo "  directory structure,"
-echo ""
-echo "- update/create all required files like CMakeLists.txt,"
-echo ""
-echo "- store the versions of all used Dune module"
-echo ""
-echo "- and extract their modifications as patches."
-echo ""
-echo "Thus, you receive a fully-working DUNE module containing the subset of"
-echo "$MODULE_DIR that is required to run your application."
-echo ""
-echo "duneproject will be run now. The new module should NOT depend on the"
-echo "module in $MODULE_DIR."
-echo ""
-read -p "Read the above and press [Enter] to proceed..."
-
-# run duneproject
-OLD_LS="$(ls)"
-$DUNE_PROJECT
-NEW_LS="$(ls)"
-
-# determine the new module/directory name
-DIFF_OUTPUT=$(diff <(echo "$OLD_LS" ) <(echo "$NEW_LS"))
-FOUND="0"
-MODULE_NAME=""
-for WORD in $DIFF_OUTPUT; do
-  if test "$FOUND" = "1"; then
-    MODULE_NAME=$WORD
-  fi
-  if test "$WORD" = ">"; then
-    FOUND="1"
-  fi
-done
-if test "$MODULE_NAME" = ""; then
-  echo "ERROR: could not find new module. Aborting."
-  exit 1
-else
-  echo ""
-  echo "$(basename $0): Found new module $MODULE_NAME"
-fi
-
-echo -n "Determining required headers..."
-cd $MODULE_DIR
-
-# initialize lists to hold required headers
-LAST_REQUIRED_HEADERS=""
-REQUIRED_HEADERS="$ALL_SOURCES"
-
-while test "$LAST_REQUIRED_HEADERS" != "$REQUIRED_HEADERS"; do
-  echo -n "."
-  LAST_REQUIRED_HEADERS="$REQUIRED_HEADERS"
-
-  # remove the file that stores all required headers line by line
-  rm -f tmp_header_file
-
-  for HEADER in $REQUIRED_HEADERS; do
-    # if tmp_header_file does not exist, create it
-    if test "$(ls tmp_header_file 2>/dev/null)" = ""; then
-      touch tmp_header_file
-    fi
-
-    # append header name to tmp_header_file
-    echo "$HEADER" >> tmp_header_file
-
-    # obtain the base name and the directory name of the header that is considered
-    HEADER_BASE_NAME=$(basename $HEADER)
-    HEADER_DIR_NAME=$(dirname $HEADER)
-
-    # create a list of all files that are included from the header
-    sed -i 's/#include/#include /' $HEADER
-    INCLUDE_LIST=$(tr -s " " < $HEADER | tr -d '><"' | awk '/#include/{print $2}')
-    sed -i 's/#include /#include/' $HEADER
-
-    # look at each of the included files
-    for INCLUDED_HEADER in $INCLUDE_LIST; do
-      # don't include config.h
-      if test "$INCLUDED_HEADER" = "config.h"; then
-        continue
-      fi
-
-      INCLUDED_BASE_NAME=$(basename $INCLUDED_HEADER)
-      INCLUDED_DIR_NAME=$(dirname $INCLUDED_HEADER)
-
-      # if the header file exists, add it
-      if test "$(ls $INCLUDED_HEADER 2>/dev/null)" = "$INCLUDED_HEADER"; then
-        echo "$INCLUDED_HEADER" >> tmp_header_file
-        continue
-      fi
-
-      # try the header preceded by its path
-      INCLUDED_HEADER_WITH_PATH="${HEADER_DIR_NAME}/${INCLUDED_HEADER}"
-
-      # if the header file actually exists, add it
-      if test "$(ls $INCLUDED_HEADER_WITH_PATH 2>/dev/null)" = "$INCLUDED_HEADER_WITH_PATH"; then
-        # remove "../" from the path
-        cd $(dirname $INCLUDED_HEADER_WITH_PATH)
-        HEADER_FULL_PATH=$(pwd)
-        HEADER_RELATIVE_PATH=${HEADER_FULL_PATH#$MODULE_FULL_PATH}
-        HEADER_RELATIVE_PATH=$(echo $HEADER_RELATIVE_PATH | sed 's/^.//')
-        INCLUDED_HEADER_WITH_PATH="${HEADER_RELATIVE_PATH}/${INCLUDED_BASE_NAME}"
-        cd $MODULE_FULL_PATH
-        echo "$INCLUDED_HEADER_WITH_PATH" >> tmp_header_file
-      fi
-    done
-  done
-
-  # sort the required headers, eliminate copies
-  REQUIRED_HEADERS=$(sort -u tmp_header_file)
-
-done
-
-# remove the file that stores all required headers line by line
-rm -f tmp_header_file
-
-# provide some output and copy everything to the new module
-echo ""
-echo -n "Number of required headers: "
-echo "$REQUIRED_HEADERS" | wc -w
-for HEADER in $REQUIRED_HEADERS; do
-  echo $HEADER
-  rsync -R $HEADER ../$MODULE_NAME
-done
-for DIR_PATH in $ALL_DIRECTORIES; do
-  rsync -rR $DIR_PATH ../$MODULE_NAME
-done
-
-# delete all architecture-dependent files
-cd ../$MODULE_NAME
-find . -name Makefile.in -delete
-find . -name Makefile -delete
-find . -name '*.o' -delete
-find . -wholename '*.deps/*' -delete
-find . -path '*.deps' -delete
-echo "Removed architecture-dependent files."
-
-# remove directories that are not required
-sed -i '/(dune)/d' CMakeLists.txt
-sed -i '/(src)/d' CMakeLists.txt
-rm -rf dune/
-rm -rf src/
-
-# create a list of the subfolders
-EMPTY_DIR_NAME="."
-rm -f tmp_subdir_file
-for HEADER in $REQUIRED_HEADERS; do
-
-  # move through every header, cutting off the last folder
-  while test "$HEADER" != $EMPTY_DIR_NAME; do
-    if test "$(dirname $HEADER)" != $EMPTY_DIR_NAME; then
-      dirname $HEADER >> tmp_subdir_file
-    fi
-
-    HEADER=$(dirname $HEADER)
-  done
-
-  # finally add the topmost folder
-  if test "$(basename $HEADER)" != $EMPTY_DIR_NAME; then
-    basename $HEADER >> tmp_subdir_file
-  fi
-done
-SUBDIR_LIST=$(sort -u tmp_subdir_file)
-rm -f tmp_subdir_file
-
-# treat every subfolder
-START_DIR=$PWD
-INITIAL_SUBDIR_LIST=""
-for SUBDIR in $SUBDIR_LIST; do
-  if test "$SUBDIR" = "$(basename $SUBDIR)"; then
-    INITIAL_SUBDIR_LIST="$INITIAL_SUBDIR_LIST $SUBDIR"
-  fi
-
-  cd $SUBDIR
-
-  # create lists of files and folders
-  FILE_LIST=""
-  DIR_LIST=""
-  for FILE_OR_FOLDER in $( ls ); do
-    if ([ -f $FILE_OR_FOLDER ]); then
-      FILE_LIST="$FILE_LIST $FILE_OR_FOLDER"
-    fi
-
-    if ([ -d $FILE_OR_FOLDER ]); then
-      DIR_LIST="$DIR_LIST $FILE_OR_FOLDER"
-    fi
-  done
-
-  # create CMakeLists.txt if not present
-  if ([ ! -f CMakeLists.txt ]); then
-    # add subfolder information
-    for DIR in $DIR_LIST; do
-      echo "add_subdirectory($DIR)" >> CMakeLists.txt
-    done
-
-    # determine whether to add file information
-    ADD_FILE_INFO="0"
-    for FILE in $FILE_LIST; do
-      if ([ -f $FILE ]); then
-        ADD_FILE_INFO="1"
-      fi
-    done
-
-    # add file information
-    if test "$ADD_FILE_INFO" = "1"; then
-      echo "" >> CMakeLists.txt
-      echo "install(FILES"  >> CMakeLists.txt
-
-      for FILE in $FILE_LIST; do
-        echo "        $FILE" >> CMakeLists.txt
-      done
-
-      echo "        DESTINATION \${CMAKE_INSTALL_INCLUDEDIR}/$SUBDIR)" >> CMakeLists.txt
-    fi
-  fi
-
-  cd $START_DIR
-
-done
-
-# update top CMakeLists.txt
-for INITIAL_SUBDIR in $INITIAL_SUBDIR_LIST; do
-  sed -i '/add_subdirectory(doc)/a add_subdirectory('$INITIAL_SUBDIR')' CMakeLists.txt
-done
-echo "Updated and created CMakeLists.txt's."
-cd ..
-
-# add includes to the automatically generated file *Macros.cmake
-MACROS_FILE="$(find $MODULE_NAME -name '*Macros.cmake')"
-if test "$(find $MODULE_NAME -type f -name 'CMakeLists.txt' -exec grep add_csv_file_links {} \; 2>/dev/null)" != ""; then
-   cp dumux-devel/cmake/modules/AddCSVFileLinks.cmake $MODULE_NAME/cmake/modules/.
-   echo "include(AddCSVFileLinks)" >>$MACROS_FILE
-fi
-if test "$(find $MODULE_NAME -type f -name 'CMakeLists.txt' -exec grep add_executable_all {} \; 2>/dev/null)" != ""; then
-   cp dumux-devel/cmake/modules/AddExecutableAll.cmake $MODULE_NAME/cmake/modules/.
-   echo "include(AddExecutableAll)" >>$MACROS_FILE
-fi
-if test "$(find $MODULE_NAME -type f -name 'CMakeLists.txt' -exec grep add_file_link {} \; 2>/dev/null)" != ""; then
-   cp dumux-devel/cmake/modules/AddFileLink.cmake $MODULE_NAME/cmake/modules/.
-   echo "include(AddFileLink)" >>$MACROS_FILE
-fi
-if test "$(find $MODULE_NAME -type f -name 'CMakeLists.txt' -exec grep add_folder_link {} \; 2>/dev/null)" != ""; then
-   cp dumux-devel/cmake/modules/AddFolderLink.cmake $MODULE_NAME/cmake/modules/.
-   echo "include(AddFolderLink)" >>$MACROS_FILE
-fi
-if test "$(find $MODULE_NAME -type f -name 'CMakeLists.txt' -exec grep add_gnuplot_file_links {} \; 2>/dev/null)" != ""; then
-   cp dumux-devel/cmake/modules/AddGnuplotFileLinks.cmake $MODULE_NAME/cmake/modules/.
-   echo "include(AddGnuplotFileLinks)" >>$MACROS_FILE
-fi
-
-# create $README_FILE
-README_FILE="README.md"
-mv $MODULE_NAME/README $MODULE_NAME/$README_FILE
-echo "This file has been created automatically. Please adapt it to your needs." >$MODULE_NAME/$README_FILE
-echo "" >>$MODULE_NAME/$README_FILE
-echo "===============================" >>$MODULE_NAME/$README_FILE
-echo "" >>$MODULE_NAME/$README_FILE
-echo "## Content" >>$MODULE_NAME/$README_FILE
-echo "" >>$MODULE_NAME/$README_FILE
-echo "The content of this DUNE module was extracted from the module \`$MODULE_DIR\`." >>$MODULE_NAME/$README_FILE
-echo "In particular, the following subfolders of \`$MODULE_DIR\` have been extracted:" >>$MODULE_NAME/$README_FILE
-echo "" >>$MODULE_NAME/$README_FILE
-for DIR_PATH in $ALL_DIRECTORIES; do
-  echo "*  \`$DIR_PATH\`," >>$MODULE_NAME/$README_FILE
-done
-echo "" >>$MODULE_NAME/$README_FILE
-echo "Additionally, all headers in \`$MODULE_DIR\` that are required to build the" >>$MODULE_NAME/$README_FILE
-echo "executables from the sources" >>$MODULE_NAME/$README_FILE
-echo "" >>$MODULE_NAME/$README_FILE
-for SOURCE in $ALL_SOURCES; do
-  echo "*  \`$SOURCE\`," >>$MODULE_NAME/$README_FILE
-done
-echo "" >>$MODULE_NAME/$README_FILE
-echo "have been extracted. You can configure the module just like any other DUNE" >>$MODULE_NAME/$README_FILE
-echo "module by using \`dunecontrol\`. For building and running the executables," >>$MODULE_NAME/$README_FILE
-echo "please go to the build folders corresponding to the sources listed above." >>$MODULE_NAME/$README_FILE
-echo "" >>$MODULE_NAME/$README_FILE
-
-# get versions from modules we are depeding on
-# create patches for un-pushed commits and uncomitted changes
-
-# try to find the dunecontrol script
-if hash dunecontrol 2>/dev/null; then
-  DUNE_CONTROL="dunecontrol"
-else
-  DUNE_CONTROL=$(find . -type f -executable -name 'dunecontrol')
-fi
-if test "$DUNE_CONTROL" = ""; then
-  echo "ERROR: Could not find dunecontrol."
-  echo "Be sure to either have dunecontrol in your search path"
-  echo "or to run this script from a directory that contains dunecontrol."
-  exit
-fi
-# get names of all modules we are depeding on
-echo "Extracting dependencies to other Dune modules"
-DEPENDING_MODULE_NAMES=$($DUNE_CONTROL --module=$MODULE_NAME 2>/dev/null | head -n 1)
-DEPENDING_MODULE_NAMES=$(sed 's/--- going to build //' <<< "$DEPENDING_MODULE_NAMES")
-DEPENDING_MODULE_NAMES=$(sed 's/  ---//' <<< "$DEPENDING_MODULE_NAMES")
-# load script
-CALL_FROM_EXTERNAL_SCRIPT="yes"
-SCRIPT_DIR="${BASH_SOURCE%/*}"
-if [[ ! -d "$SCRIPT_DIR" ]]; then
-  SCRIPT_DIR="$PWD";
-fi
-
-# output guidence for users
-echo ""
-echo "*************************************************************************"
-echo "The extracted module is contained in the subfolder \"$MODULE_NAME\"."
-echo "You can build it using \"dunecontrol ... --only=$MODULE_NAME all\"."
-echo "*************************************************************************"
-echo "BEFORE building, you can add the module to dumux-pub by something like:"
-echo "(Rename module name if it does not match the AuthorLastNameYearx scheme"
-echo "and commit it to the Git repository dumux-pub)"
-echo "git clone https://git.iws.uni-stuttgart.de/dumux-pub/AuthorLastNameYearx.git"
-echo "sed -i '/Module:/c\Module: AuthorLastNameYearx' $MODULE_NAME/dune.module"
-echo "mv $MODULE_NAME/* dumux-pub/AuthorLastNameYearx/."
-echo "cd AuthorLastNameYearx"
-echo "git commit -a"
-echo "git push"
-
-exit 0
diff --git a/bin/util/getusedversions.py b/bin/util/getusedversions.py
deleted file mode 100755
index 18b407051a3695fbf3cd86b9059f1ce2dcdb5e1e..0000000000000000000000000000000000000000
--- a/bin/util/getusedversions.py
+++ /dev/null
@@ -1,115 +0,0 @@
-#!/usr/bin/env python3
-
-import os
-import argparse
-from common import runCommand
-from common import callFromPath
-
-# print warning message for scanned folders that are not git repositories
-def printNoGitRepoWarning(folderPath):
-    print("Folder " + folderPath + " does not seem to be the top level" \
-          "of a git repository and will be skipped. Make sure not to call " \
-          "this script from a sub-directory of a git repository.")
-
-# raise error due to untracked files present in the given module folder
-def raiseUntrackedFilesError(folderPath):
-    raise RuntimeError('Found untracked files in module folder: "' + folderPath + '". ' \
-                       'Please commit, stash, or remove them.')
-
-# returns true if the given folder is a git repository
-def isGitRepository(modFolderPath):
-    return os.path.exists(os.path.join(modFolderPath, '.git'))
-
-# returns true if a module contains untracked files
-def hasUntrackedFiles(modFolderPath):
-    run = callFromPath(modFolderPath)(runCommand)
-    return run('git ls-files --others --exclude-standard') != ''
-
-# function to extract git version information for modules
-# returns a dictionary containing module information for each given module folder
-def getUsedVersions(modFolderPaths, ignoreUntracked=False):
-
-    result = {}
-    for modFolderPath in modFolderPaths:
-
-        # make sure this is the top level of a git repository
-        if not isGitRepository(modFolderPath):
-            printNoGitRepoWarning(modFolderPath)
-        else:
-            if not ignoreUntracked and hasUntrackedFiles(modFolderPath):
-                raiseUntrackedFilesError(modFolderPath)
-
-            run = callFromPath(modFolderPath)(runCommand)
-            result[modFolderPath] = {}
-            result[modFolderPath]['remote'] = run('git ls-remote --get-url').strip('\n')
-            result[modFolderPath]['revision'] = run('git log -n 1 --format=%H @{upstream}').strip('\n')
-            result[modFolderPath]['date'] = run('git log -n 1 --format=%ai @{upstream}').strip('\n')
-            result[modFolderPath]['author'] = run('git log -n 1 --format=%an @{upstream}').strip('\n')
-            result[modFolderPath]['branch'] = run('git rev-parse --abbrev-ref HEAD').strip('\n')
-
-    return result
-
-# create patches for unpublished commits and uncommitted changes in modules
-def getPatches(modFolderPaths, ignoreUntracked=False):
-
-    result = {}
-    for modFolderPath in modFolderPaths:
-
-        # make sure this is the top level of a git repository
-        if not isGitRepository(modFolderPath):
-            printNoGitRepoWarning(modFolderPath)
-        else:
-            if not ignoreUntracked and hasUntrackedFiles(modFolderPath):
-                raiseUntrackedFilesError(modFolderPath)
-
-            run = callFromPath(modFolderPath)(runCommand)
-            unpubPatch = run('git format-patch --stdout @{upstream}')
-            unCommPatch = run('git diff')
-            if unpubPatch != '' or unCommPatch != '': result[modFolderPath] = {}
-            if unpubPatch != '': result[modFolderPath]['unpublished'] = unpubPatch
-            if unCommPatch != '': result[modFolderPath]['uncommitted'] = unCommPatch
-
-    return result
-
-# prints the detected versions as table
-def printVersionTable(versions):
-    print("\t| {:^50} | {:^50} | {:^50} | {:^30} |".format('module folder', 'branch', 'commit hash', 'commit date'))
-    print("\t" + 193*'-')
-    for folder, versionInfo in versions.items():
-        print("\t| {:^50} | {:^50} | {:^50} | {:^30} |".format(folder, versionInfo['branch'], versionInfo['revision'], versionInfo['date']))
-
-
-# For standalone execution
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description='This script extracts the used dune/dumux versions.')
-    parser.add_argument('-p', '--path', required=False, help='the path to the top folder containing your dune/dumux modules')
-    parser.add_argument('-i', '--ignoreuntracked', required=False, action='store_true', help='use this flag to ignore untracked files present in the modules')
-    parser.add_argument('-s', '--skipfolders', required=False, nargs='*', help='a list of module folders to be skipped')
-    cmdArgs = vars(parser.parse_args())
-
-    modulesPath = os.getcwd() if not cmdArgs['path'] else os.path.join(os.getcwd(), cmdArgs['path'])
-    print('\nDetermining the versions of all dune modules in the folder: ' + modulesPath)
-
-    def getPath(modFolder):
-        return os.path.join(modulesPath, modFolder)
-
-    modFolderPaths = [getPath(dir) for dir in os.listdir(modulesPath) if os.path.isdir(getPath(dir))]
-    if cmdArgs['skipfolders']:
-        cmdArgs['skipfolders'] = [f.strip('/') for f in cmdArgs['skipfolders']]
-        modFolderPaths = [d for d in modFolderPaths if os.path.basename(d.strip('/')) not in cmdArgs['skipfolders']]
-    versions = getUsedVersions(modFolderPaths, True)
-
-    print("\nDetected the following versions:")
-    printVersionTable(versions)
-
-    # maybe check untracked files
-    if not cmdArgs['ignoreuntracked']:
-        modsWithUntracked = [f for f in versions if hasUntrackedFiles(f)]
-        if modsWithUntracked:
-            print('\n')
-            print('#'*56)
-            print('WARNING: Found untracked files in the following modules:\n\n')
-            print('\n'.join(modsWithUntracked))
-            print('\nPlease make sure that these are not required for your purposes.')
-            print('If not, you can run this script with the option -i/--ignoreuntracked to suppress this warning.')
-            print('#'*56)
diff --git a/bin/util/installscript.py b/bin/util/installscript.py
new file mode 100644
index 0000000000000000000000000000000000000000..1d8669d75b0c71c9d77e6735db1865532e226e58
--- /dev/null
+++ b/bin/util/installscript.py
@@ -0,0 +1,203 @@
+""""
+Helper functions to generate an install script for a dune-module,
+accounting for non-published commits and local changes
+"""
+
+import os
+import sys
+import textwrap
+
+from util.common import getPersistentVersions, versionTable, getPatches
+from util.moduleinfo import getModuleInfo
+from util.installscript_writer import InstallScriptWriterBash
+from util.installscript_writer import InstallScriptWriterPython
+
+if sys.version_info[0] < 3:
+    sys.exit("\nError': Python3 required")
+
+
+def supportedLanguages():
+    return ['python', 'bash']
+
+
+def getScriptExtension(language):
+    assert language in supportedLanguages()
+    ext = {
+        'python': '.py',
+        'bash': '.sh'
+    }
+    return ext[language]
+
+
+def makeScriptWriter(language):
+    if language == 'bash':
+        return InstallScriptWriterBash()
+    elif language == 'python':
+        return InstallScriptWriterPython()
+    raise ValueError(f'Could not create writer for language {language}')
+
+
+def getDefaultScriptName(modName, language):
+    return 'install_{}{}'.format(
+        modName,
+        getScriptExtension(language)
+    )
+
+
+def printProgressInfo(infoLines, indLevel=0):
+    firstPrefix = '\n' + '--'*(indLevel+1)
+    emptyPrefix = firstPrefix.replace('-', ' ').strip('\n')
+    print(f"{firstPrefix} {infoLines[0]}")
+    for line in infoLines[1:]:
+        print(f"{emptyPrefix} {line}")
+
+
+def filterDependencies(dependencies, skipFolders=[]):
+    if not skipFolders:
+        return dependencies
+    else:
+        def skipFolder(folderName):
+            return any(folderName == os.path.basename(path) for path in skipFolders)
+        return [
+            dep for dep in dependencies if not skipFolder(dep['folder'])
+        ]
+
+
+def addDependencyVersions(dependencies, ignoreUntracked=False):
+    def getKey(dependency):
+        return dependency['path']
+
+    versions = getPersistentVersions(
+        [getKey(d) for d in dependencies], ignoreUntracked
+    )
+    if len(versions) != len(dependencies):
+        raise Exception("Not all versions of all modules could be found.")
+
+    mergedResult = []
+    for depInfo in dependencies:
+        versionInfo = versions[getKey(depInfo)]
+        mergedResult.append({**depInfo, **versionInfo})
+    return mergedResult
+
+
+def addDependencyPatches(dependenciesWithVersions):
+    def getKey(dependency):
+        return dependency['path']
+
+    patches = getPatches({
+        getKey(d): d for d in dependenciesWithVersions
+    })
+
+    mergedResult = []
+    for depInfo in dependenciesWithVersions:
+        patch = patches[getKey(depInfo)]
+        mergedResult.append({**depInfo, **patch})
+    return mergedResult
+
+
+def makeInstallScript(modPath,
+                      dependencies,
+                      scriptName,
+                      writer,
+                      topFolderName='DUMUX',
+                      optsFile=None):
+
+    modPath = os.path.abspath(modPath)
+    modName = getModuleInfo(modPath, 'Module')
+
+    modOptsFile = '{}/cmake.opts'.format(modPath)
+    if not optsFile:
+        if os.path.isfile(modOptsFile):
+            optsFile = '{}/cmake.opts'.format(os.path.relpath(modPath))
+        else:
+            optsFile = 'dumux/cmake.opts'
+    if os.path.isabs(optsFile):
+        raise ValueError("Opts file must be given as relative path")
+    if not any(optsFile.startswith(d['folder']) for d in dependencies):
+        print("Warning: opts file is not contained in any of the dependencies")
+
+    with open(scriptName, 'w') as script:
+
+        writer.setOutputStream(script)
+        writer.writeSheBang()
+
+        script.write('\n')
+        writer.writeComment(textwrap.dedent(f"""\
+
+            This installs the module {modName} and its dependencies.
+            The exact revisions used are listed in the table below.
+            However, note that this script may also apply further patches.
+            If so, all patches are required to be the current folder, or,
+            in the one that you specified as argument to this script.
+
+        """))
+
+        script.write('\n')
+        writer.writeComment(versionTable(dependencies))
+
+        script.write('\n')
+        writer.writePreamble(topFolderName)
+
+        for dep in dependencies:
+            script.write('\n')
+            writer.writeMessageOutput('Installing {}'.format(dep['name']))
+            writer.writeInstallation(dep)
+
+        for dep in dependencies:
+            def writePatch(patch, moduleName, description):
+                script.write('\n')
+                writer.writeMessageOutput(
+                    f'Applying patch for {description} in {moduleName}'
+                )
+                writer.writePatchApplication(dep['folder'], patch)
+
+            if dep['untracked'] is not None:
+                description = 'untracked files'
+                writePatch(dep['untracked'], description, dep['name'])
+            if dep['unpublished'] is not None:
+                description = 'unpublished commits'
+                writePatch(dep['unpublished'], description, dep['name'])
+            if dep['uncommitted'] is not None:
+                description = 'uncommitted changes'
+                writePatch(dep['uncommitted'], description, dep['name'])
+
+        script.write('\n')
+        writer.writeMessageOutput('Configuring project')
+        writer.writeConfiguration(optsFile)
+
+
+def printFoundDependencies(deps):
+    if len(deps) > 0:
+        infoText = ["Found the following dependencies"]
+        infoText.extend(
+            versionTable(
+                deps, {'name': 'module name', 'path': 'folder'}
+            ).split('\n')
+        )
+        printProgressInfo(infoText)
+
+
+def printFoundVersionInfo(dependenciesWithVersions):
+    table = versionTable(dependenciesWithVersions)
+    printProgressInfo(
+        ["The following (remotely available) versions are used as a basis",
+         "on top of which the required patches will be automatically created:",
+         "\n{}".format(table)]
+    )
+
+
+def printFinalMessage(scriptName,
+                      topFolderName=None):
+
+    if topFolderName:
+        description = textwrap.dedent(f"""\
+            Running this script will create a folder `{topFolderName}`, clone all modules
+            into it, configure the entire project and build the contained applications
+        """)
+    else:
+        description = textwrap.dedent(f"""\
+            Running this script will clone all modules into the folder from which it is
+            called, configure the entire project and build the contained applications
+        """)
+
+    printProgressInfo(['Info:', description])
diff --git a/bin/util/installscript_writer.py b/bin/util/installscript_writer.py
new file mode 100644
index 0000000000000000000000000000000000000000..0eb133b344eda1eab734e393a49bfbad913b5271
--- /dev/null
+++ b/bin/util/installscript_writer.py
@@ -0,0 +1,222 @@
+"""Language-specific backends for install script generation"""
+
+import os
+import sys
+import textwrap
+from abc import ABC, abstractmethod
+from util.common import addPrefixToLines, escapeCharacters
+
+
+def getRawString(text):
+    def makeRaw(text): return repr(text)
+    def removeEnclosingQuotes(text): return text[1:-1]
+    return removeEnclosingQuotes(makeRaw(text))
+
+
+class InstallScriptWriterInterface(ABC):
+    def __init__(self):
+        self.ostream = None
+
+    def setOutputStream(self, stream):
+        self.ostream = stream
+
+    @abstractmethod
+    def writeSheBang(self):
+        pass
+
+    @abstractmethod
+    def writeComment(self, comment):
+        pass
+
+    @abstractmethod
+    def writeMessageOutput(self, message):
+        pass
+
+    @abstractmethod
+    def writePreamble(self, topFolderName=None):
+        pass
+
+    @abstractmethod
+    def writeInstallation(self, dependency):
+        pass
+
+    @abstractmethod
+    def writePatchApplication(self, folder, patchName):
+        pass
+
+    @abstractmethod
+    def writeConfiguration(self, optsFile):
+        pass
+
+
+class InstallScriptWriterBash(InstallScriptWriterInterface):
+    def __init__(self):
+        super().__init__()
+
+    def writeSheBang(self):
+        self.ostream.write('#!/bin/bash\n')
+
+    def writeComment(self, comment):
+        comment = addPrefixToLines('#', comment)
+        self.ostream.write(comment)
+
+    def writeMessageOutput(self, message):
+        self.ostream.write(f'echo "{message}"\n')
+
+    def writePreamble(self, topFolderName=None):
+        self.ostream.write(textwrap.dedent("""\
+
+            exitWithError()
+            {
+                MSG=$1
+                echo "$MSG"
+                exit 1
+            }
+
+            installModule()
+            {
+                FOLDER=$1
+                URL=$2
+                BRANCH=$3
+                REVISION=$4
+
+                if [ ! -d "$FOLDER" ]; then
+                    if ! git clone $URL; then exitWithError "clone failed"; fi
+                    pushd $FOLDER
+                        if ! git checkout $BRANCH; then exitWithError "checkout failed"; fi
+                        if ! git reset --hard $REVISION; then exitWithError "reset failed"; fi
+                    popd
+                else
+                    echo "Skip cloning $URL since target folder "$FOLDER" already exists."
+                fi
+            }
+
+            applyPatch()
+            {
+                FOLDER=$1
+                PATCH=$2
+
+                pushd $FOLDER
+                    echo "$PATCH" > tmp.patch
+                    if ! git apply tmp.patch; then exitWithError "patch failed"; fi
+                    rm tmp.patch
+                popd
+            }
+
+        """))
+        top = topFolderName if topFolderName else "."
+        self.ostream.write('TOP="{}"\n'.format(top))
+        self.ostream.write('mkdir -p $TOP\n')
+        self.ostream.write('cd $TOP\n')
+
+    def writeInstallation(self, dependency):
+        self.ostream.write('installModule {} {} {} {}'
+                           .format(dependency['folder'],
+                                   dependency['remote'],
+                                   dependency['branch'],
+                                   dependency['revision']))
+
+    def writePatchApplication(self, folder, patchContent):
+        def removeEscapedSingleQuotes(line):
+            return line.replace(r"\'", "'")
+
+        self.ostream.write('PATCH="\n')
+        for line in patchContent.rstrip('\n').split('\n'):
+            line = getRawString(line)
+            line = removeEscapedSingleQuotes(line)
+            line = escapeCharacters(line, ['"', "$", "`"])
+            self.ostream.write(line)
+            self.ostream.write('\n')
+        self.ostream.write('"\n')
+        self.ostream.write(f'applyPatch {folder} "$PATCH"')
+
+    def writeConfiguration(self, opts):
+        self.ostream.write(
+            f'if ! ./dune-common/bin/dunecontrol --opts={opts} all; then\n'
+            '    echo "Configuration of the project failed"\n'
+            '    exit 1\n'
+            'fi\n'
+        )
+
+
+class InstallScriptWriterPython(InstallScriptWriterInterface):
+    def __init__(self):
+        super().__init__()
+
+    def writeSheBang(self):
+        self.ostream.write('#!/usr/bin/env python3\n')
+
+    def writeComment(self, comment):
+        comment = addPrefixToLines('#', comment)
+        self.ostream.write(comment)
+
+    def writeMessageOutput(self, message):
+        self.ostream.write(f'print("{message}")\n')
+
+    def writePreamble(self, topFolderName=None):
+        top = topFolderName if topFolderName else "."
+        self.ostream.write(textwrap.dedent(f"""\
+
+            import os
+            import sys
+            import subprocess
+
+            top = "{top}"
+            os.makedirs(top, exist_ok=True)
+
+
+            def runFromSubFolder(cmd, subFolder):
+                folder = os.path.join(top, subFolder)
+                try:
+                    subprocess.run(cmd, cwd=folder, check=True)
+                except Exception as e:
+                    cmdString = ' '.join(cmd)
+                    sys.exit(
+                        "Error when calling:\\n{{}}\\n-> folder: {{}}\\n-> error: {{}}"
+                        .format(cmdString, folder, str(e))
+                    )
+
+
+            def installModule(subFolder, url, branch, revision):
+                targetFolder = url.rstrip(".git").split("/")[-1]
+                if not os.path.exists(targetFolder):
+                    runFromSubFolder(['git', 'clone', url, targetFolder], '.')
+                    runFromSubFolder(['git', 'checkout', branch], subFolder)
+                    runFromSubFolder(['git', 'reset', '--hard', revision], subFolder)
+                else:
+                    print(f'Skip cloning {{url}} since target folder "{{targetFolder}}" already exists.')
+
+
+            def applyPatch(subFolder, patch):
+                sfPath = os.path.join(top, subFolder)
+                patchPath = os.path.join(sfPath, 'tmp.patch')
+                with open(patchPath, 'w') as patchFile:
+                    patchFile.write(patch)
+                runFromSubFolder(['git', 'apply', 'tmp.patch'], subFolder)
+                os.remove(patchPath)
+        """))
+
+    def writeInstallation(self, dependency):
+        self.ostream.write('installModule("{}", "{}", "{}", "{}")\n'
+                           .format(dependency['folder'],
+                                   dependency['remote'],
+                                   dependency['branch'],
+                                   dependency['revision']))
+
+    def writePatchApplication(self, folder, patchContent):
+        self.ostream.write('patch = """\n')
+        for line in patchContent.rstrip('\n').split('\n'):
+            line = getRawString(line)
+            self.ostream.write(escapeCharacters(line, ['"']))
+            self.ostream.write('\n')
+        self.ostream.write('"""\n')
+
+        self.ostream.write(f'applyPatch("{folder}", patch)\n')
+
+    def writeConfiguration(self, opts):
+        self.ostream.write(
+            "runFromSubFolder(\n"
+            f"    ['./dune-common/bin/dunecontrol', '--opts={opts}', 'all'],\n"
+            "    '.'\n"
+            ")\n"
+        )
diff --git a/bin/util/makeinstallscript.py b/bin/util/makeinstallscript.py
deleted file mode 100755
index 83248590051d82e68b4c45fc7270fec341ea9f13..0000000000000000000000000000000000000000
--- a/bin/util/makeinstallscript.py
+++ /dev/null
@@ -1,250 +0,0 @@
-#!/usr/bin/env python3
-
-# script to generate an install script for a dune-module,
-# accounting for non-published commits and local changes
-
-import os
-import sys
-import argparse
-import subprocess
-from getmoduleinfo import *
-from getusedversions import *
-
-if sys.version_info[0] < 3:
-    sys.exit("\nError': Python3 required")
-
-
-###################
-# parse arguments
-parser = argparse.ArgumentParser(
-    description='This script generates an install script for your dune module,' \
-                'taking into account non-published commits and local changes.\n' \
-                'This expects that your module is a git repository and that a ' \
-                'remote origin exists and has been set already.'
-)
-parser.add_argument('-p', '--path', required=True, help='The path to your dune module')
-parser.add_argument('-f', '--filename', required=False, help='Name of the file in which to write the install script')
-parser.add_argument('-i', '--ignoreuntracked', required=False, action='store_true', help='Use this flag to ignore untracked files present in the modules')
-parser.add_argument('-t', '--topfoldername', required=False, default='DUMUX',
-                    help='Name of the folder that the install script creates upon execution to install the modules in. '\
-                         'If you pass an empty string, no folder will be created and installation happens in place.')
-parser.add_argument('-o', '--optsfile', required=False,
-                    help='Provide custom opts file to be used for the call to dunecontrol. '\
-                         'Note that this file is required to be contained and committed within the module or its dependencies.')
-parser.add_argument('-s', '--skipfolders', required=False, nargs='*', help='a list of module folders to be skipped')
-cmdArgs = vars(parser.parse_args())
-
-
-####################
-# Welcome message
-modPath = os.path.abspath( os.path.join(os.getcwd(), cmdArgs['path']) )
-modParentPath = os.path.abspath( os.path.join(modPath, '../') )
-modFolder = os.path.relpath(modPath, modParentPath)
-
-try: modName = getModuleInfo(modPath, 'Module')
-except:
-    sys.exit("\Error: Could not determine module name. Make sure the path to\n" \
-             "        your module is correct and that a dune.module file is present.")
-
-instFileName = 'install_' + modName + '.sh' if not cmdArgs['filename'] else cmdArgs['filename']
-print("\n-- Creating install script '{}' for the module '{}' in the folder '{}'".format(instFileName, modName, modPath))
-
-
-###################
-# get dependencies
-print("\n-- Determining the dependencies")
-deps = getDependencies(modPath)
-if len(deps) > 0:
-    print("-> Found the following dependencies")
-    print("\t| {:^50} | {:^50} |".format('module name', 'module folder'))
-    print("\t" + 107*'-')
-    for dep in deps: print('\t| {:^50} | {:^50} |'.format(dep['name'], dep['folder']))
-else:
-    sys.exit("Error: Could not determine dependencies. At least the module itself should appear.")
-
-depNames = [dep['name'] for dep in deps]
-depFolders = [dep['folder'] for dep in deps]
-depFolderPaths = [os.path.abspath(os.path.join(modParentPath, d)) for d in depFolders]
-if cmdArgs['skipfolders']:
-    cmdArgs['skipfolders'] = [f.strip('/') for f in cmdArgs['skipfolders']]
-    depFolders = [d for d in depFolders if d not in cmdArgs['skipfolders']]
-    depNames = [getModuleInfo(d, 'Module') for d in depFolders]
-    depFolderPaths = [d for d in depFolderPaths if os.path.basename(d.strip('/')) not in cmdArgs['skipfolders']]
-
-#################################
-# determine specific commits of all modules
-print("\n-- Determining the module versions")
-try: versions = getUsedVersions(depFolderPaths, cmdArgs['ignoreuntracked'])
-except Exception as err:
-    print('\nCaught exception: ' + str(err))
-    if 'untracked' in str(err):
-        print('If you are certain that the untracked files are not needed for the ' \
-              'installation of the modules, run this script with the -i flag.')
-    sys.exit(1)
-
-if len(versions) != len(depNames):
-    sys.exit("Error': Could not determine versions of all modules")
-
-print("-> Detected the following versions")
-printVersionTable(versions)
-
-#################################
-# create patches if necessary
-print("\n-- Creating patches for unpublished commits and uncommitted changes")
-patches = getPatches(depFolderPaths, cmdArgs['ignoreuntracked'])
-
-# create patch files
-if len(patches) > 0:
-    patchesPath = os.path.join(modPath, 'patches')
-    if not os.path.exists(patchesPath):
-        os.mkdir(patchesPath)
-        print("-> Created a folder 'patches' in your module. You should commit this to your\n"\
-              "   repository in order for the installation script to work on other machines.")
-    else:
-        print("-> Adding patches to the 'patches' folder in your module. Make sure to commit\n"\
-              "   the newly added patches in order for the install script to work on other machines.")
-
-    # get a non-used patch file name (makes sure not to overwrite anything)
-    def getPatchFileName(depModPath, targetName):
-        i = 1
-        fileName = os.path.join(patchesPath, targetName + '.patch')
-        while os.path.exists(fileName):
-            fileName = os.path.join(patchesPath, targetName + '_' + str(i) + '.patch')
-            i += 1
-
-        return fileName, os.path.relpath(fileName, depModPath)
-
-    # function to write a new patch file (returns the path to the new file)
-    def writeDepModPatchFile(depModPath, depModName, type):
-        patchPath, patchRelPath = getPatchFileName(depModPath, depModName + '_' + type)
-        patches[depModPath][type + '_relpath'] = patchRelPath
-        open(patchPath, 'w').write(patches[depModPath][type])
-        return patchPath
-
-    print("-> Created patch files:")
-    for depModPath in patches.keys():
-        depModName = getModuleInfo(depModPath, "Module")
-        if 'unpublished' in patches[depModPath]:
-            print(' '*3 + writeDepModPatchFile(depModPath, depModName, 'unpublished'))
-        if 'uncommitted' in patches[depModPath]:
-            print(' '*3 + writeDepModPatchFile(depModPath, depModName, 'uncommitted'))
-
-else:
-    print("-> No Patches required")
-
-
-##################################
-# write installation shell script
-
-# in the install script, we work with relative paths from the parent to module folder
-def getModRelPath(path): return os.path.relpath(path, modParentPath)
-versions = {getModRelPath(modPath): val for modPath, val in versions.items()}
-patches = {getModRelPath(modPath): val for modPath, val in patches.items()}
-
-topFolderName = cmdArgs['topfoldername']
-optsRelPath = 'dumux/cmake.opts'
-if cmdArgs['optsfile']:
-    optsPath = os.path.abspath( os.path.join(os.getcwd(), cmdArgs['optsfile']) )
-    optsRelPath = os.path.relpath(optsPath, modParentPath)
-
-# TODO: add support for different install script languages (e.g. Python).
-with open(instFileName, 'w') as installFile:
-    installFile.write('#!/bin/bash\n\n')
-    installFile.write('#'*80 + '\n')
-    installFile.write('# This script installs the module "' + modName + '" together with all dependencies.\n')
-    installFile.write('\n')
-
-    exitFunc = 'exitWith'
-    installFile.write('# defines a function to exit with error message\n')
-    installFile.write(exitFunc + ' ()\n')
-    installFile.write('{\n')
-    installFile.write('    echo ' + r'"\n$1"' +'\n')
-    installFile.write('    exit 1\n')
-    installFile.write('}\n\n')
-
-    # function to write a command with error check into the script file
-    def writeCommand(command, errorMessage, indentationLevel = 0):
-        installFile.write(' '*indentationLevel + 'if ! ' + command + ';')
-        installFile.write(' then ' + exitFunc + ' "' + errorMessage + '"; fi\n')
-
-    # write section on creating the top folder
-    if topFolderName:
-        installFile.write('# Everything will be installed into a newly created sub-folder named "' + topFolderName + '".\n')
-        installFile.write('echo "Creating the folder ' + topFolderName + ' to install the modules in"\n')
-        writeCommand('mkdir -p ' + topFolderName , '--Error: could not create top folder ' + topFolderName)
-        writeCommand('cd ' + topFolderName, '--Error: could not enter top folder ' + topFolderName)
-        installFile.write('\n')
-    else:
-        installFile.write('# Everything will be installed inside the folder from which the script is executed.\n\n')
-
-    # function to write installation procedure for a module
-    def writeCloneModule(depModName, depModFolder):
-        installFile.write('# ' + depModName + '\n')
-        installFile.write('# ' + versions[depModFolder]['branch'] + ' # '
-                               + versions[depModFolder]['revision'] + ' # '
-                               + versions[depModFolder]['date'] + ' # '
-                               + versions[depModFolder]['author'] + '\n')
-
-        writeCommand('git clone ' + versions[depModFolder]['remote'],
-                     '-- Error: failed to clone ' + depModName + '.')
-        writeCommand('cd ' + depModFolder,
-                     '-- Error: could not enter folder ' + depModFolder + '.')
-        writeCommand('git checkout ' + versions[depModFolder]['branch'],
-                     '-- Error: failed to check out branch ' + versions[depModFolder]['branch'] + ' in module ' + depModName + '.')
-        writeCommand('git reset --hard ' + versions[depModFolder]['revision'],
-                     '-- Error: failed to check out commit ' + versions[depModFolder]['revision'] + ' in module ' + depModName + '.')
-
-        # write section on application of patches
-        def writeApplyPatch(patchRelPath):
-            installFile.write('if [ -f ' + patchRelPath + ' ]; then\n')
-            writeCommand('git apply ' + patchRelPath,'--Error: failed to apply patch ' + patchRelPath + ' in module ' + depModName + '.', 4)
-            installFile.write('else\n')
-            installFile.write(' '*4 + exitFunc + ' "--Error: patch ' + patchRelPath + ' was not found."\n')
-            installFile.write('fi\n')
-
-        if depModFolder in patches and 'unpublished_relpath' in patches[depModFolder]:
-            writeApplyPatch(patches[depModFolder]['unpublished_relpath'])
-        if depModFolder in patches and 'uncommitted_relpath' in patches[depModFolder]:
-            writeApplyPatch(patches[depModFolder]['uncommitted_relpath'])
-
-        installFile.write('echo "-- Successfully set up the module ' + depModName + r'\n"' + '\n')
-        installFile.write('cd ..\n\n')
-
-    # write the module clone first in order for the patches to be present
-    if modName in depNames:
-        writeCloneModule(modName, modFolder)
-    for depModName, depModFolder in zip(depNames, depFolders):
-        if depModName != modName:
-            writeCloneModule(depModName, depModFolder)
-
-    # write configure command
-    installFile.write('echo "-- All modules haven been cloned successfully. Configuring project..."\n')
-    writeCommand('./dune-common/bin/dunecontrol --opts=' + optsRelPath + ' all', '--Error: could not configure project')
-
-    # build the tests of the module
-    installFile.write('\n')
-    installFile.write('echo "-- Configuring successful. Compiling applications..."\n')
-    writeCommand('cd ' + modFolder + "/build-cmake", '--Error: could not enter build directory at ' + modFolder + '/build-cmake')
-    writeCommand('make build_tests', '--Error: applications could not be compiled. Please try to compile them manually.')
-
-print("\n-- Successfully created install script file " + instFileName)
-
-if len(patches) > 0:
-    print("-> It is recommended that you now commit and publish the 'patches' folder and this install script in your module such that others can use it.")
-    print("   IMPORTANT: After you committed the patches, you have to adjust the line of the install script in which your module is checked out to a specific commit.")
-    print("              That is, in the line 'git reset --hard COMMIT_SHA' for your module, replace COMMIT_SHA by the commit in which you added the patches.")
-    print("              If patches had to be created for your own module, please think about comitting and pushing your local changes and rerunning this script again.")
-
-if modName in depNames: # print gudience to installation if the module is not skipped
-    print("\n-- You might want to put installation instructions into the README.md file of your module, for instance:\n")
-    print("   ## Installation\n")
-    print("   The easiest way of installation is to use the script `" + instFileName + "` provided in this repository.")
-    print("   Using `wget`, you can simply install all dependent modules by typing:\n")
-    print("   ```sh")
-    print("   wget " + versions[modFolder]['remote'] + "/" + instFileName)
-    print("   chmod u+x " + instFileName)
-    print("   ./" + instFileName)
-    print("   ```\n")
-
-    if topFolderName: print("   This will create a sub-folder `" + topFolderName + "`, clone all modules into it, configure the entire project and build the applications contained in this module.")
-    else: print("   This will clone all modules into the folder from which the script is called, configure the entire project and build the applications contained in this module.")
diff --git a/bin/util/getmoduleinfo.py b/bin/util/moduleinfo.py
similarity index 50%
rename from bin/util/getmoduleinfo.py
rename to bin/util/moduleinfo.py
index 61cb155fe0a19a85ebcf287c1fe42ca8843d7657..ad7a0600eb5265c09ab8f9554f4d83a6cfcb28bd 100644
--- a/bin/util/getmoduleinfo.py
+++ b/bin/util/moduleinfo.py
@@ -1,8 +1,8 @@
 import os
-from common import runCommand
-from common import callFromPath
+from util.common import runCommand
+from util.common import callFromPath
+
 
-# extract information (for the given keys) from a module file
 def extractModuleInfos(moduleFile, keys):
     results = {}
     with open(moduleFile, 'r') as modFile:
@@ -21,48 +21,70 @@ def extractModuleInfos(moduleFile, keys):
 
     return results
 
-# get info file of a module
+
 def getModuleFile(modulePath):
     modFile = os.path.join(modulePath, 'dune.module')
     if not os.path.exists(modFile):
         raise RuntimeError("Could not find module file")
     return modFile
 
-# retrieve single information from a module
+
 def getModuleInfo(modulePath, key):
     return extractModuleInfos(getModuleFile(modulePath), [key])[key]
 
-# get the dependencies of a dune module located in the given directory
-def getDependencies(modulePath, verbose=False):
+
+def getDependencies(modulePath, verbose=False, includeSelf=False):
     modName = getModuleInfo(modulePath, 'Module')
     parentPath = os.path.join(modulePath, '../')
     duneControlPath = os.path.join(parentPath, 'dune-common/bin/dunecontrol')
     if not os.path.exists(duneControlPath):
-        raise RuntimeError('Could not find dunecontrol, expected it to be in {}'.format(duneControlPath))
+        raise RuntimeError(
+            'Could not find dunecontrol, expected it to be in {}'
+            .format(duneControlPath)
+        )
 
-    run = callFromPath(parentPath)(runCommand)
-    for line in run('./dune-common/bin/dunecontrol --module={}'.format(modName)).split('\n'):
+    dcOutput = callFromPath(parentPath)(runCommand)(
+        './dune-common/bin/dunecontrol --module={}'.format(modName)
+    )
+
+    if not dcOutput:
+        raise RuntimeError("Error: call to dunecontrol failed.")
+
+    for line in dcOutput.split('\n'):
         if "going to build" in line:
-            line = line.replace('going to build', '').replace('---', '').replace('done', '')
+            line = line.replace('going to build', '').strip('-')
             line = line.strip('\n').strip()
             line = line.split(' ')
             deps = line
 
-    # Now we look for the folders with the modules
+    if not includeSelf:
+        deps.remove(modName)
+
     if verbose:
-        print("Determined the following dependencies: " + ", ".join(deps))
+        print(" -- Determined the following dependencies: " + ", ".join(deps))
+        print(" -- Searching the respective directories...")
 
     result = []
-    for dir in [d for d in os.listdir(parentPath) if os.path.isdir(os.path.join(parentPath, d))]:
-        try: depModName = getModuleInfo(os.path.join(parentPath, dir), 'Module')
-        except: print('--- Note: skipping folder "' + dir + '" as it could not be identifed as dune module')
+    parentFiles = [os.path.join(parentPath, d) for d in os.listdir(parentPath)]
+    for path in filter(os.path.isdir, parentFiles):
+        try:
+            depModName = getModuleInfo(path, 'Module')
+        except Exception:
+            if verbose:
+                print(f" --- skipping folder '{path}' "
+                      "as it could not be identifed as dune module")
         else:
+            if verbose:
+                print(" --- visited module '{}'".format(depModName))
             if depModName in deps:
-                result.append({'name': depModName, 'folder': dir})
+                result.append({
+                    'name': depModName,
+                    'folder': os.path.basename(path),
+                    'path': path
+                })
 
     if len(result) != len(deps):
         raise RuntimeError("Could not find the folders of all dependencies")
     elif verbose:
-        print("Found all module folders of the dependencies.")
-
+        print(" -- Found all module folders of the dependencies.")
     return result