From: Rene Gassmoeller Date: Wed, 21 Aug 2024 14:33:16 +0000 (+0200) Subject: Add prm tools script. X-Git-Url: https://gitweb.dealii.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=52a40d9108138ef13f08c28a2f27d330e684ac82;p=dealii.git Add prm tools script. --- diff --git a/contrib/utilities/prm_tools.py b/contrib/utilities/prm_tools.py new file mode 100644 index 0000000000..46ac387b78 --- /dev/null +++ b/contrib/utilities/prm_tools.py @@ -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))