]> https://gitweb.dealii.org/ - dealii.git/commitdiff
Add prm tools script.
authorRene Gassmoeller <rene.gassmoeller@mailbox.org>
Wed, 21 Aug 2024 14:33:16 +0000 (16:33 +0200)
committerRene Gassmoeller <rene.gassmoeller@mailbox.org>
Tue, 27 Aug 2024 13:06:23 +0000 (15:06 +0200)
contrib/utilities/prm_tools.py [new file with mode: 0644]

diff --git a/contrib/utilities/prm_tools.py b/contrib/utilities/prm_tools.py
new file mode 100644 (file)
index 0000000..46ac387
--- /dev/null
@@ -0,0 +1,315 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+## ------------------------------------------------------------------------
+##
+## SPDX-License-Identifier: LGPL-2.1-or-later
+## Copyright (C) 2016 by the deal.II authors
+##
+## This file is part of the deal.II library.
+##
+## Part of the source code is dual licensed under Apache-2.0 WITH
+## LLVM-exception OR LGPL-2.1-or-later. Detailed license information
+## governing the source code and code contributions can be found in
+## LICENSE.md and CONTRIBUTING.md at the top level directory of deal.II.
+##
+## ------------------------------------------------------------------------
+
+""" Routines to read, format and write deal.II parameter input files (.prm files) """
+
+import sys
+import os
+import re
+import argparse
+
+__author__ = 'The authors of deal.II'
+__copyright__ = 'Copyright 2024, deal.II'
+__license__ = 'GNU GPL 2 or later'
+
+
+def get_parameter_value(parameters, name):
+    """ Given a dictionary of parameters with a structure as
+    the one created by read_parameter_file(), return the value
+    of the parameter with the given name. Returns None if the
+    parameter is not found.
+    """
+
+    if name in parameters:
+        return parameters[name]["value"]
+    else:
+        for entry in parameters:
+            if parameters[entry]["type"] == "subsection":
+                value = get_parameter_value(parameters[entry]["value"], name)
+                if value != None:
+                    return value
+
+    return None
+
+
+
+def set_parameter_value(parameters, name, value):
+    """ Given a dictionary of parameters with a structure as
+    the one created by read_parameter_file(), recursively search
+    through the subsections and set the value
+    of the first parameter with the given name to the given value.
+    Returns 0 if the parameter was found and set, 1 otherwise.
+    """
+
+    if name in parameters:
+        parameters[name]["value"] = value
+        return 0
+    else:
+        for entry in parameters:
+            if parameters[entry]["type"] == "subsection":
+                if set_parameter_value(parameters[entry]["value"], name, value) == 0:
+                    return 0
+
+    return 1
+
+
+def split_parameter_line(line):
+    """ Read a 'set parameter' line and extract the name, value, and format. """
+
+    equal_index = line.find('=')
+    # Determine number of additional spaces left of equal sign.
+    # We need to store these separately, because there might be two
+    # lines setting the same parameter, but different number of spaces. They
+    # should still map to the same parameter.
+    alignment_spaces = len(line[:equal_index]) - len(line[:equal_index].rstrip())
+
+    # skip initial word "set" in parameter name
+    words_left_of_equal_sign = line[:equal_index].replace("\\\n","").split()
+    param_name = words_left_of_equal_sign[1:]
+    param_name = ' '.join(param_name)
+
+    # strip spaces at end of value string, keep all inline comments
+    param_value = line[equal_index+1:].rstrip()
+    # if there is one or more spaces at the start of the string, remove one space.
+    # keep additional spaces, because they are part of the formatting.
+    # this way we can add one space when writing the file back and keep the formatting
+    if param_value.startswith(' ') and len(param_value) > 1:
+        param_value = param_value[1:]
+
+    return param_name, param_value, alignment_spaces
+
+
+
+def read_value_or_subsection(input_file, parameters):
+    """ Read a value or a subsection from a parameter file into a parameter dictionary.
+    """
+
+    # Keep track of the comment lines to
+    # add them to the next parameter or subsection
+    accumulated_comment = ""
+
+    for line in input_file:
+        line = line.strip()
+
+        # If line ends with \, read next line and append including \n
+        if line != "":
+            while line[-1] == "\\":
+                line += "\n" + next(input_file).rstrip()
+
+        words = line.replace("\\\n","").split()
+
+        # Attach empty lines if we are inside a comment
+        if line == "" or len(words) == 0:
+            if accumulated_comment != "":
+                accumulated_comment += "\n"
+
+        # Store comments in accumulated_comment
+        elif line[0] == "#":
+            if accumulated_comment != "":
+                accumulated_comment += "\n"
+            accumulated_comment += line.replace("\\\n","")
+
+        # If we encounter a 'subsection' line, store the subsection name
+        # and recursively call this function to read the subsection
+        elif words[0] == "subsection":
+            subsection_name = ' '.join(words[1:]).replace("\\","")
+
+            if subsection_name not in parameters:
+                parameters[subsection_name] = {"comment": "", "value" : dict({}), "type": "subsection"}
+
+            if parameters[subsection_name]["comment"] != "":
+                parameters[subsection_name]["comment"] += "\n"
+
+            parameters[subsection_name]["comment"] += accumulated_comment
+            accumulated_comment = ""
+
+            parameters[subsection_name]["value"].update(read_value_or_subsection(input_file, parameters[subsection_name]["value"]))
+
+        # If we encounter a 'set' line, store the parameter name and value
+        elif words[0] == 'set':
+            name, value, alignment_spaces = split_parameter_line(line)
+
+            parameters[name] = {"comment": accumulated_comment, "value": value, "alignment spaces": alignment_spaces, "type": "parameter"}
+            accumulated_comment = ""
+
+        elif words[0] == "include":
+            name = ' '.join(words[1:])
+            value = ''
+            alignment_spaces = 0
+            parameters[name] = {"comment": accumulated_comment, "value": value, "alignment spaces": alignment_spaces, "type": "include"}
+            accumulated_comment = ""
+
+        elif words[0] == "end":
+            # sometimes there are comments at the end of files or subsection. store them in their own parameter
+            if accumulated_comment != "":
+                parameters["postcomment"] = {"comment": accumulated_comment, "value": "", "alignment spaces": 0, "type": "comment"}
+                accumulated_comment = ""
+
+            return parameters
+
+        else:
+            raise RuntimeError("Unrecognized first keyword in line: " + line)
+
+    # sometimes there are comments at the end of files or subsection. store them in their own parameter
+    if accumulated_comment != "":
+        parameters["postcomment"] = {"comment": accumulated_comment, "value": "", "alignment spaces": 0, "type": "comment"}
+        accumulated_comment = ""
+
+    return parameters
+
+
+
+def read_parameter_file(file):
+    """ Read parameter file, and return a dictionary with all values.
+
+    The returned dictionary contains as keys the name of the subsection,
+     include statement or parameter, and as value a dictionary with
+     the following keys:
+    - 'comment': the comment lines before the parameter or subsection
+    - 'value': the value of the parameter as a string, or a dictionary
+      with the same structure as the one returned by this function
+      for a subsection. Empty for an include statement (the content
+      of an include statement is its name).
+    - 'alignment spaces': the number of spaces between name and equal sign
+      in a parameter line. This is used to keep the formatting
+      of the parameter file when writing it back.
+    - 'type': either 'parameter', 'include', 'subsection', or 'comment'
+      depending on the type of the entry.
+    """
+
+    parameters = dict({})
+
+    with open(file, 'r') as f:
+        read_value_or_subsection(f, parameters)
+
+    return parameters
+
+
+
+def write_comment_if_existent(comment, prepend_spaces, file):
+    """ Print the comment lines, take care to use the correct indentation.
+    """
+    if comment != "":
+        for line in comment.split("\n"):
+            if line != "":
+                file.write(" " * prepend_spaces)
+            file.write(line + "\n")
+
+
+
+def write_value_or_subsection(parameters, prepend_spaces, file):
+    """ Write a parameters dictionary recursively into a given file.
+    prepend_spaces tracks the current indentation level.
+    """
+
+    # keep track of whether this is the first entry
+    # this is important for some formatting
+    first_entry = True
+
+    # Print all parameter/include entries first
+    for entry in parameters:
+        if parameters[entry]["type"] == "parameter":
+            # Add empty line before comments
+            if first_entry == False and parameters[entry]["comment"] != "":
+                file.write("\n")
+            write_comment_if_existent(parameters[entry]["comment"], prepend_spaces, file)
+
+            # Only add a space after equal sign if the value is not empty
+            space_if_value = " " if parameters[entry]["value"] != "" else ""
+            file.write(" " * prepend_spaces + "set " + entry + " " * parameters[entry]["alignment spaces"] + "=" \
+                        + space_if_value + parameters[entry]["value"] + "\n")
+
+        elif parameters[entry]["type"] == "include":
+            # Add empty line before include
+            if first_entry == False:
+                file.write("\n")
+            write_comment_if_existent(parameters[entry]["comment"], prepend_spaces, file)
+
+            # Write the include
+            file.write(" " * prepend_spaces + "include " + entry + "\n")
+            # Add an empty line after include, except for the end of the file
+            if entry != list(parameters)[-1]:
+                file.write("\n")
+
+        elif parameters[entry]["type"] == "subsection":
+            if first_entry == False:
+                file.write("\n")
+            write_comment_if_existent(parameters[entry]["comment"], prepend_spaces, file)
+
+            file.write(" " * prepend_spaces + "subsection " + entry + "\n")
+            prepend_spaces += 2
+            # Recursively write the next subsection level
+            write_value_or_subsection(parameters[entry]["value"], prepend_spaces, file)
+            prepend_spaces -= 2
+            file.write(" " * prepend_spaces + "end\n")
+
+        elif parameters[entry]["type"] == "comment":
+            if first_entry == False and parameters[entry]["comment"] != "":
+                file.write("\n")
+            write_comment_if_existent(parameters[entry]["comment"], prepend_spaces, file)
+
+        first_entry = False
+
+    return 0
+
+
+
+def write_parameter_file(parameters, file):
+    """ Given a dictionary of parameters with a structure as
+    the one created by read_parameter_file(), write the dictionary
+    to a file.
+    """
+
+    prepend_spaces = 0
+    with open(file,'w') as f:
+        write_value_or_subsection(parameters, prepend_spaces, f)
+    return 0
+
+
+
+def main(input_file, output_file):
+    """ This function reformats the given input .prm files to follow our
+    general formatting guidelines. These are:
+
+    - indent by two spaces for each subsection level, and indent comments as well
+    as content
+    - ensure an empty line before a subsection or a comment, unless it is a
+    subsection/comment following directly another subsection, or it is at the
+    start of the file
+    - combine content of subsections that are duplicated
+    - if values are duplicated, merge them and use the last one in the file (like
+    deal.II ParameterHandler when it reads the file)
+    - retain as much user formatting in comments and parameter values as possible,
+    e.g. spaces for padding values to align between adjacent lines. This is not
+    always perfectly possible.
+    - retain broken lines (`\\`) in values of parameters and comments, remove them
+    from subsection or parameter names.
+    """
+    parameters = read_parameter_file(input_file)
+    write_parameter_file(parameters, output_file)
+
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(
+                    prog='deal.II .prm file reformatter',
+                    description='Reformats deal.II .prm files to follow our general formatting guidelines. See the documentation of this script for details.')
+    parser.add_argument('input_file', type=str, help='The .prm file to reformat.')
+    parser.add_argument('output_file', type=str, help='The .prm file to write the reformatted file to.')
+    args = parser.parse_args()
+
+    sys.exit(main(args.input_file, args.output_file))

In the beginning the Universe was created. This has made a lot of people very angry and has been widely regarded as a bad move.

Douglas Adams


Typeset in Trocchi and Trocchi Bold Sans Serif.