diff options
Diffstat (limited to 'debian/uncrustify-trinity/uncrustify-trinity-0.78.1/scripts/option_reducer.py')
| -rwxr-xr-x | debian/uncrustify-trinity/uncrustify-trinity-0.78.1/scripts/option_reducer.py | 1125 | 
1 files changed, 1125 insertions, 0 deletions
| diff --git a/debian/uncrustify-trinity/uncrustify-trinity-0.78.1/scripts/option_reducer.py b/debian/uncrustify-trinity/uncrustify-trinity-0.78.1/scripts/option_reducer.py new file mode 100755 index 00000000..403ff92b --- /dev/null +++ b/debian/uncrustify-trinity/uncrustify-trinity-0.78.1/scripts/option_reducer.py @@ -0,0 +1,1125 @@ +#!/usr/bin/python +""" +option_reducer.py + +reduces options in a given config file to the minimum while still maintaining +desired formatting + +:author:  Daniel Chumak +:license: GPL v2+ +""" + +# Possible improvements: +# - parallelize add_back() +# - (maybe) reduce amount of written config file, see Uncrustify --set + +from __future__ import print_function  # python >= 2.6 +import argparse + +from os import name as os_name, sep as os_path_sep, fdopen as os_fdopen, \ +    remove as os_remove +from os.path import exists, join as path_join +from subprocess import Popen, PIPE +from sys import exit as sys_exit, stderr, stdout +from shutil import rmtree +from multiprocessing import cpu_count +from tempfile import mkdtemp, mkstemp +from contextlib import contextmanager +from collections import OrderedDict +from threading import Timer +from multiprocessing.pool import Pool +from itertools import combinations + +FLAGS = None +NULL_DEV = "/dev/null" if os_name != "nt" else "nul" + + +def enum(**enums): +    return type('Enum', (), enums) + + +RESTULTSFLAG = enum(NONE=0, REMOVE=1, KEEP=2) +ERROR_CODE = enum(NONE=0, FLAGS=200, SANITY0=201, SANITY1=202) +MODES = ("reduce", "no-default") + + +@contextmanager +def make_temp_directory(): +    """ +    Wraps tempfile.mkdtemp to use it inside a with statement that auto deletes +    the temporary directory with its content after the with block closes + + +    :return: str +    ---------------------------------------------------------------------------- +        path to the generated directory +    """ +    temp_dir = mkdtemp() +    try: +        yield temp_dir +    finally: +        rmtree(temp_dir) + + +@contextmanager +def make_raw_temp_file(*args, **kwargs): +    """ +    Wraps tempfile.mkstemp to use it inside a with statement that auto deletes +    the file after the with block closes + + +    Parameters +    ---------------------------------------------------------------------------- +    :param args, kwargs: +        arguments passed to mkstemp + + +    :return: int, str +    ---------------------------------------------------------------------------- +        the file descriptor and the file path of the created temporary file +    """ +    fd, tmp_file_name = mkstemp(*args, **kwargs) +    try: +        yield (fd, tmp_file_name) +    finally: +        os_remove(tmp_file_name) + + +@contextmanager +def open_fd(*args, **kwargs): +    """ +    Wraps os.fdopen to use it inside a with statement that auto closes the +    generated file descriptor after the with block closes + + +    Parameters +    ---------------------------------------------------------------------------- +    :param args, kwargs: +        arguments passed to os.fdopen + + +    :return: TextIOWrapper +    ---------------------------------------------------------------------------- +        open file object connected to the file descriptor +    """ +    fp = os_fdopen(*args, **kwargs) +    try: +        yield fp +    finally: +        fp.close() + + +def term_proc(proc, timeout): +    """ +    helper function to terminate a process + + +    Parameters +    ---------------------------------------------------------------------------- +    :param proc: process object +        the process object that is going to be terminated + +    :param timeout: dictionary +        a dictionary (used as object reference) to set a flag that indicates +        that the process is going to be terminated +    """ +    timeout["value"] = True +    proc.terminate() + + +def uncrustify(unc_bin_path, cfg_file_path, unformatted_file_path, +               lang=None, debug_file=None, check=False): +    """ +    executes Uncrustify and captures its stdout + + +    Parameters +    ---------------------------------------------------------------------------- +    :param unc_bin_path: str +        path to the Uncrustify binary + +    :param cfg_file_path: str +        path to a config file for Uncrustify + +    :param unformatted_file_path: str +        path to a file that is going to be formatted + +    :param lang: str / None +        Uncrustifys -l argument + +    :param debug_file: str / None +        Uncrustifys -p argument + +    :param check: bool +        Used to control whether Uncrustifys --check is going to be used + + +    :return: str / None +    ---------------------------------------------------------------------------- +        returns the stdout from Uncrustify or None if the process takes to much +        time (set to 5 sec) +    """ + +    args = [unc_bin_path, "-q", "-c", cfg_file_path, '-f', +            unformatted_file_path] +    if lang: +        args.extend(("-l", lang)) +    if debug_file: +        args.extend(('-p', debug_file)) +    if check: +        args.append('--check') + +    proc = Popen(args, stdout=PIPE, stderr=PIPE) + +    timeout = {"value": False} +    timer = Timer(5, term_proc, [proc, timeout]) +    timer.start() + +    output_b, error_txt_b = proc.communicate() + +    timer.cancel() + +    if timeout["value"]: +        print("uncrustify proc timeout: %s" % ' '.join(args), file=stderr) +        return None + +    error = error_txt_b.decode("UTF-8") +    if error: +        print("Uncrustify %s stderr:\n %s" % (unformatted_file_path, error), +              file=stderr) + +    return output_b + + +def same_expected_generated(formatted_path, unc_bin_path, cfg_file_path, +                            input_path, lang=None): +    """ +    Calls uncrustify and compares its generated output with the content of a +    file + + +    Parameters +    ---------------------------------------------------------------------------- +    :param formatted_path: str +        path to a file containing the expected content + +    :params unc_bin_path, cfg_file_path, input_path, lang: str, str, str, +                                                           str / None +        see uncrustify() + + +    :return: bool +    ---------------------------------------------------------------------------- +        True if the strings match, False otherwise +    """ + +    expected_string = '' +    with open(formatted_path, 'rb') as f: +        expected_string = f.read() + +    formatted_string = uncrustify(unc_bin_path, cfg_file_path, input_path, lang) + +    return True if formatted_string == expected_string else False + + +def process_uncrustify(args): +    """ +    special wrapper for same_expected_generated() + +    accesses global var(s): RESTULTSFLAG + + +    Parameters +    ---------------------------------------------------------------------------- +    :param args: list / tuple< int, ... > +        this function is intended to be called by multiprocessing.pool.map() +        therefore all arguments are inside a list / tuple: +            id: int +                an index number needed by the caller to differentiate runs + +            other parameters: +                see same_expected_generated() + + +    :return: tuple< int, RESTULTSFLAG > +    ---------------------------------------------------------------------------- +        returns a tuple containing the id and a RESTULTSFLAG, REMOVE if both +        strings are equal, KEEP if not +    """ + +    id = args[0] +    res = same_expected_generated(*args[1:]) + +    return id, RESTULTSFLAG.REMOVE if res else RESTULTSFLAG.KEEP + + +def write_config_file(args): +    """ +    Writes all but one excluded option into a config file + + +    Parameters +    ---------------------------------------------------------------------------- +    :param args: list / tuple< list< tuple< str, str > >, str, int > +        this function is intended to be called by multiprocessing.pool.map() +        therefore all arguments are inside a list / tuple: + +            config_list: list< tuple< str, str > > +                a list of tuples containing option names and values + +            tmp_dir: str +                path to a directory in which the config file is going to be +                written + +            exclude_idx: int +                index for an option that is not going to be written into the +                config file +    """ + +    config_list, tmp_dir, exclude_idx = args + +    with open("%s%suncr-%d.cfg" % (tmp_dir, os_path_sep, exclude_idx), +              'w') as f: +        print_config(config_list, target_file_obj=f, exclude_idx=exclude_idx) + + +def write_config_file2(args): +    """ +    Writes two option lists into a config file + + +    Parameters +    ---------------------------------------------------------------------------- +    :param args: list< tuple< str, str > >, +                 list< tuple< str, str > >, str, int +        this function is intended to be called by multiprocessing.pool.map() +        therefore all arguments are inside a list / tuple: + +            config_list: list< tuple< str, str > > +                the first list of tuples containing option names and values + +            test_list: list< tuple< str, str > > +                the second list of tuples containing option names and values + +            tmp_dir: str +                path to a directory in which the config file is going to be +                written + +            idx: int +                index that is going to be used for the filename +    """ + +    config_list0, config_list1, tmp_dir, idx = args + +    with open("%s%suncr-r-%d.cfg" % (tmp_dir, os_path_sep, idx), 'w') as f: +        print_config(config_list0, target_file_obj=f) +        print("", end='\n', file=f) +        print_config(config_list1, target_file_obj=f) + + +def gen_multi_combinations(elements, N): +    """ +    generator function that generates, based on a set of elements, all +    combinations of 1..N elements + + +    Parameters +    ---------------------------------------------------------------------------- +    :param elements: list / tuple +        a list of elements from which the combinations will be generated + +    :param N: +        the max number of element in a combination + + +    :return: list +    ---------------------------------------------------------------------------- +        yields a single combination of the elements + +    >>> gen_multi_combinations(["a", "b", "c"], 3) +    (a); (b); (c); (a,b); (a,c); (b,c); (a,b,c) +    """ + +    fields = len(elements) +    if N > fields: +        raise Exception("Error: N > len(options)") +    if N <= 0: +        raise Exception("Error: N <= 0") + +    for n in range(1, N + 1): +        yield combinations(elements, n) + + +def add_back(unc_bin_path, input_files, formatted_files, langs, options_r, +             options_k, tmp_dir): +    """ +    lets Uncrustify format files with generated configs files until all +    formatted files match their according expected files. + +    Multiple config files are generated based on a (base) list of Uncrustify +    options combined with additional (new) options derived from combinations of +    another list of options. + + +    accesses global var(s): RESTULTSFLAG + + +    Parameters +    ---------------------------------------------------------------------------- +    :param unc_bin_path: str +        path to the Uncrustify binary + +    :param input_files: list / tuple< str > +        a list containing paths to a files that are going to be formatted + +    :param formatted_files: list / tuple< str > +        a list containing paths to files containing the expected contents + +    :param langs: list / tuple< str > / None +        a list of languages the files, used as Uncrustifys -l argument +        can be None or shorter than the amount of provided files + +    :param options_r: list< tuple< str, str > > +        the list of options from which combinations will be derived + +    :param options_k: list< tuple< str, str > > +        the (base) list of Uncrustify options + +    :param tmp_dir: str +        the directory in which the config files will be written to + + +    :return: list< tuple< str, str > > / None +    ---------------------------------------------------------------------------- +        list of additional option that were needed to generate matching file +        contents +    """ + +    lang_max_idx = -1 if langs is None else len(langs) - 1 +    file_len = len(input_files) + +    if len(formatted_files) != file_len: +        raise Exception("len(input_files) != len(formatted_files)") + +    for m_combination in gen_multi_combinations(options_r, len(options_r)): +        for idx, (r_combination) in enumerate(m_combination): +            write_config_file2((options_k, r_combination, tmp_dir, idx)) + +            cfg_file_path = "%s%suncr-r-%d.cfg" % (tmp_dir, os_path_sep, idx) +            res = [] + +            for file_idx in range(file_len): +                lang = None if idx > lang_max_idx else langs[file_idx] + +                r = process_uncrustify( +                    (0, formatted_files[file_idx], unc_bin_path, cfg_file_path, +                     input_files[file_idx], lang)) +                res.append(r[1]) + +            # all files, flag = remove -> option can be removed -> equal output +            if res.count(RESTULTSFLAG.REMOVE) == len(res): +                return r_combination +    return None + + +def sanity_raw_run(args): +    """ +    wrapper for same_expected_generated(), prints error message if the config +    file does not generate the expected result + +    Parameters +    ---------------------------------------------------------------------------- +    :param args: +        see same_expected_generated + + +    :return: +    ---------------------------------------------------------------------------- +        see same_expected_generated +    """ +    res = same_expected_generated(*args) + +    if not res: +        formatted_file_path = args[0] +        config_file_path = args[2] +        input_file_path = args[3] + +        print("\nprovided config does not create formatted source file:\n" +              "    %s\n    %s\n->| %s" +              % (input_file_path, config_file_path, formatted_file_path), +              file=stderr) +    return res + + +def sanity_run(args): +    """ +    wrapper for same_expected_generated(), prints error message if the config +    file does not generate the expected result + + +    Parameters +    ---------------------------------------------------------------------------- +    :param args: +        see same_expected_generated + + +    :return: +    ---------------------------------------------------------------------------- +        see same_expected_generated +    """ +    res = same_expected_generated(*args) + +    if not res: +        formatted_file_path = args[0] +        input_file_path = args[3] + +        print("\ngenerated config does not create formatted source file:\n" +              "    %s\n    %s" +              % (input_file_path, formatted_file_path), file=stderr) +    return res + + +def sanity_run_splitter(uncr_bin, config_list, input_files, formatted_files, +                        langs, tmp_dir, jobs): +    """ +    writes config option into a file and tests if every input file is formatted +    so that is matches the content of the according expected file + + +    Parameters +    ---------------------------------------------------------------------------- +    :param uncr_bin: str +        path to the Uncrustify binary + +    :param config_list: list< tuple< str, str > > +        a list of tuples containing option names and values + +    :param input_files: list / tuple< str > +        a list containing paths to a files that are going to be formatted + +    :param formatted_files: list / tuple< str > +        a list containing paths to files containing the expected contents + +    :param langs: list / tuple< str > / None +        a list of languages the files, used as Uncrustifys -l argument +        can be None or shorter than the amount of provided files + +    :param tmp_dir: str +        the directory in which the config files will be written to + +    :param jobs: int +        number of processes to use + + +    :return: bool +    ---------------------------------------------------------------------------- +        True if all files generate correct results, False oterhwise +    """ + +    file_len = len(input_files) +    if len(formatted_files) != file_len: +        raise Exception("len(input_files) != len(formatted_files)") + +    gen_cfg_path = path_join(tmp_dir, "gen.cfg") +    with open(gen_cfg_path, 'w') as f: +        print_config(config_list, target_file_obj=f) + +    lang_max_idx = -1 if langs is None else len(langs) - 1 +    args = [] + +    for idx in range(file_len): +        lang = None if idx > lang_max_idx else langs[idx] + +        args.append((formatted_files[idx], uncr_bin, gen_cfg_path, +                     input_files[idx], lang)) + +    pool = Pool(processes=jobs) +    sr = pool.map(sanity_run, args) + +    return False not in sr + + +def print_config(config_list, target_file_obj=stdout, exclude_idx=()): +    """ +    prints config options into a config file + + +    Parameters +    ---------------------------------------------------------------------------- +    :param config_list: list< tuple< str, str > > +        a list containing pairs of option names and option values + +    :param target_file_obj: file object +        see file param of print() + +    :param exclude_idx: int / list< int > +        index of option(s) that are not going to be printed +    """ + +    if not config_list: +        return +    config_list_len = len(config_list) + +    # check if exclude_idx list is empty -> assign len +    if type(exclude_idx) in (list, tuple) and not exclude_idx: +        exclude_idx = [config_list_len] +    else: +        # sort it, unless it is an int -> transform into a list +        try: +            exclude_idx = sorted(exclude_idx) +        except TypeError: +            exclude_idx = [exclude_idx] + +    # extracted first loop round: +    # do not print '\n' for the ( here non-existing) previous line +    if exclude_idx[0] != 0: +        print("%s = %s" % (config_list[0][0].ljust(31, ' '), config_list[0][1]), +              end='', file=target_file_obj) +    # also print space if a single option was provided and it is going to be +    # excluded. This is done in order to be able to differentiate between +    # --empty-nochange and the case where all options can be removed +    elif config_list_len == 1: +        print(' ', end='', file=target_file_obj) +        return + +    start_idx = 1 +    for end in exclude_idx: +        end = min(end, config_list_len) + +        for idx in range(start_idx, end): +            print("\n%s = %s" +                  % (config_list[idx][0].ljust(31, ' '), config_list[idx][1]), +                  end='', file=target_file_obj) + +        start_idx = min(end + 1, config_list_len) + +    # after +    for idx in range(start_idx, config_list_len): +        print("\n%s = %s" +              % (config_list[idx][0].ljust(31, ' '), config_list[idx][1]), +              end='', file=target_file_obj) + + +def get_non_default_options(unc_bin_path, cfg_file_path): +    """ +    calls Uncrustify to generate a debug file from which a config only with +    non default valued options are extracted + +    accesses global var(s): NULL_DEV + + +    Parameters +    ---------------------------------------------------------------------------- +    :param unc_bin_path: str +        path to the Uncrustify binary + +    :param cfg_file_path: str +        path to a config file for Uncrustify + + +    :return: list< str > +    ---------------------------------------------------------------------------- +        amount of lines in the provided and shortened config +    """ +    lines = [] + +    with make_raw_temp_file(suffix='.unc') as (fd, file_path): +        # make debug file +        uncrustify(unc_bin_path, cfg_file_path, NULL_DEV, debug_file=file_path, +                   check=True) + +        # extract non comment lines -> non default config lines +        with open_fd(fd, 'r') as fp: +            lines = fp.read().splitlines() +            lines = [line for line in lines if not line[:1] == '#'] + +    return lines + + +def parse_config_file(file_obj): +    """ +    Reads in a Uncrustify config file + + +    Parameters +    ---------------------------------------------------------------------------- +    :param file_obj: +        the file object of an opened config file + + +    :return: list< tuple< str, str > > +    ---------------------------------------------------------------------------- +        a list containing pairs of option names and option values +    """ +    # dict used to only save the last option setting if the same option occurs +    # multiple times, without this: +    #   optionA0 can be removed because optionA1 = s0, and +    #   optionA1 can be removed because optionA0 = s0 +    # -> optionA0, optionA1 are both removed +    config_map = OrderedDict() + +    # special keys may not have this limitation, as for example +    # 'set x y' and 'set x z' do not overwrite each other +    special_keys = {'macro-open', 'macro-else', 'macro-close', 'set', 'type', +                    'file_ext', 'define'} +    special_list = [] + +    for line in file_obj: +        # cut comments +        pound_pos = line.find('#') +        if pound_pos != -1: +            line = line[:pound_pos] + +        split_pos = line.find('=') +        if split_pos == -1: +            split_pos = line.find(' ') +        if split_pos == -1: +            continue + +        key = line[:split_pos].strip() +        value = line[split_pos + 1:].strip() + +        if key in special_keys: +            special_list.append((key, value)) +        else: +            config_map[key] = value + +    config_list = list(config_map.items()) +    config_list += special_list + +    return config_list + + +def count_lines(file_path): +    """ +    returns the count of lines in a file by counting '\n' chars + +    Parameters +    ---------------------------------------------------------------------------- +    :param file_path: str +        file in which the lines will be counted + + +    :return: int +    ---------------------------------------------------------------------------- +        number a lines +    """ +    in_count = 0 +    with open(file_path, 'r') as f: +        in_count = f.read().count('\n') + 1 +    return in_count + + +def reduce(options_list): +    """ +    Reduces the given options to a minimum + +    accesses global var(s): FLAGS, RESTULTSFLAG, ERROR_CODE + +    Parameters +    ---------------------------------------------------------------------------- +    :param options_list: list< tuple< str, str > > +        the list of options that are going to be reduced + +    :return: int, list< tuple< str, str > > +        status return code, reduced options +    """ +    config_list_len = len(options_list) +    ret_flag = ERROR_CODE.NONE + +    file_count = len(FLAGS.input_file_path) +    lang_max_idx = -1 if FLAGS.lang is None else len(FLAGS.lang) - 1 + +    pool = Pool(processes=FLAGS.jobs) +    with make_temp_directory() as tmp_dir: +        # region sanity run ---------------------------------------------------- +        args = [] +        for idx in range(file_count): +            lang = None if idx > lang_max_idx else FLAGS.lang[idx] + +            args.append((FLAGS.formatted_file_path[idx], +                         FLAGS.uncrustify_binary_path, FLAGS.config_file_path, +                         FLAGS.input_file_path[idx], lang)) +        sr = pool.map(sanity_raw_run, args) +        del args[:] + +        if False in sr: +            return ERROR_CODE.SANITY0, [] +        del sr[:] + +        # endregion +        # region config generator loop ----------------------------------------- +        args = [] + +        for e_idx in range(config_list_len): +            args.append((options_list, tmp_dir, e_idx)) +        pool.map(write_config_file, args) + +        del args[:] + +        # endregion +        # region main loop ----------------------------------------------------- +        args = [] +        jobs = config_list_len * file_count + +        for idx in range(jobs): +            file_idx = idx // config_list_len +            option_idx = idx % config_list_len + +            cfg_file_path = "%s%suncr-%d.cfg" \ +                            % (tmp_dir, os_path_sep, option_idx) +            lang = None if idx > lang_max_idx else FLAGS.lang[file_idx] + +            args.append((idx, FLAGS.formatted_file_path[file_idx], +                         FLAGS.uncrustify_binary_path, cfg_file_path, +                         FLAGS.input_file_path[file_idx], lang)) + +        results = pool.map(process_uncrustify, args) +        del args[:] +        # endregion +        # region clean results ------------------------------------------------- +        option_flags = [RESTULTSFLAG.NONE] * config_list_len + +        for r in results: +            idx = r[0] +            flag = r[1] + +            option_idx = idx % config_list_len + +            if option_flags[option_idx] == RESTULTSFLAG.KEEP: +                continue + +            option_flags[option_idx] = flag +        del results[:] +        # endregion + +        options_r = [options_list[idx] for idx, x in enumerate(option_flags) +                     if x == RESTULTSFLAG.REMOVE] +        options_list = [options_list[idx] for idx, x in enumerate(option_flags) +                        if x == RESTULTSFLAG.KEEP] + +        del option_flags[:] + +        # region sanity run ---------------------------------------------------- +        # options can be removed one at a time generating appropriate results, +        # oddly enough sometimes a config generated this way can fail when a +        # combination of multiple options is missing +        s_flag = True +        if options_r: +            s_flag = sanity_run_splitter( +                FLAGS.uncrustify_binary_path, options_list, +                FLAGS.input_file_path, FLAGS.formatted_file_path, FLAGS.lang, +                tmp_dir, FLAGS.jobs) + +        if not s_flag: +            ret_flag = ERROR_CODE.SANITY1 +            print("\n\nstumbled upon complex option dependencies in \n" +                  "    %s\n" +                  "trying to add back minimal amount of removed options\n" +                  % FLAGS.config_file_path, file=stderr) + +            ret_options = add_back( +                FLAGS.uncrustify_binary_path, FLAGS.input_file_path, +                FLAGS.formatted_file_path, FLAGS.lang, options_r, +                options_list, tmp_dir) + +            if ret_options: +                options_list.extend(ret_options) + +                s_flag = sanity_run_splitter( +                    FLAGS.uncrustify_binary_path, options_list, +                    FLAGS.input_file_path, FLAGS.formatted_file_path, +                    FLAGS.lang, tmp_dir, FLAGS.jobs) + +                if s_flag: +                    print("Success!", file=stderr) +                    ret_flag = ERROR_CODE.NONE +                    # endregion +    return ret_flag, options_list if ret_flag == ERROR_CODE.NONE else [] + + +def reduce_mode(): +    """ +    the mode that minimizes a config file as much as possible + +    accesses global var(s): FLAGS, ERROR_CODE +    """ +    ret_flag = ERROR_CODE.NONE +    option_list = {} + +    # gen & parse non default config +    lines = get_non_default_options(FLAGS.uncrustify_binary_path, +                                    FLAGS.config_file_path) +    option_list = parse_config_file(lines) +    config_list_len = len(option_list) + +    config_lines_init = count_lines(FLAGS.config_file_path) +    config_lines_ndef = len(lines) +    del lines[:] + +    # early return if all options are already removed at this point +    if config_list_len == 0: +        if not FLAGS.empty_nochange \ +                or (config_lines_init - config_lines_ndef) > 0: +            if not FLAGS.quiet: +                print("\n%s" % '# '.ljust(78, '-')) + +            print(" ") + +            if not FLAGS.quiet: +                print("%s" % '# '.ljust(78, '-')) +                print("# initial config lines: %d,\n" +                      "# default options and unneeded lines: %d,\n" +                      "# unneeded options: 0,\n" +                      "# kept options: 0" +                      % (config_lines_init, config_lines_init)) +        print("ret_flag: 0", file=stderr) +        return ERROR_CODE.NONE + +    # gen reduced options +    config_lines_redu = -1 +    for i in range(FLAGS.passes): +        old_config_lines_redu = config_lines_redu + +        ret_flag, option_list = reduce(option_list) +        config_lines_redu = len(option_list) + +        if ret_flag != ERROR_CODE.NONE \ +                or config_lines_redu == old_config_lines_redu: +            break + +    if ret_flag == ERROR_CODE.NONE: +        # use the debug file trick again to get correctly sorted options +        with make_raw_temp_file(suffix='.unc') as (fd, file_path): +            with open_fd(fd, 'w') as f: +                print_config(option_list, target_file_obj=f) + +            lines = get_non_default_options(FLAGS.uncrustify_binary_path, +                                            file_path) +            option_list = parse_config_file(lines) + +        # print output + stats +        if not FLAGS.empty_nochange or config_lines_ndef != config_lines_redu: +            if not FLAGS.quiet: +                print("\n%s" % '# '.ljust(78, '-')) + +            print_config(option_list) + +            if not FLAGS.quiet: +                print("\n%s" % '# '.ljust(78, '-')) +                print("# initial config lines: %d,\n" +                      "# default options and unneeded lines: %d,\n" +                      "# unneeded options: %d,\n" +                      "# kept options: %d" +                      % (config_lines_init, +                         config_lines_init - config_lines_ndef, +                         config_lines_ndef - config_lines_redu, +                         config_lines_redu)) + +    print("ret_flag: %d" % ret_flag, file=stderr) +    return ret_flag + + +def no_default_mode(): +    """ +    the mode removes all unnecessary lines and options with default values + +    accesses global var(s): FLAGS, ERROR_CODE +    """ + +    lines = get_non_default_options(FLAGS.uncrustify_binary_path, +                                    FLAGS.config_file_path, ) +    config_lines_ndef = len(lines) +    config_lines_init = count_lines(FLAGS.config_file_path) + +    if not FLAGS.empty_nochange or (config_lines_ndef != config_lines_init): +        if not FLAGS.quiet: +            print("%s" % '# '.ljust(78, '-')) + +        options_str = '\n'.join(lines) +        if not options_str: +            print(" ") +        else: +            print(options_str, file=stdout) + +        if not FLAGS.quiet: +            print("%s" % '# '.ljust(78, '-')) +            print("# initial config lines: %d,\n" +                  "# default options and unneeded lines: %d,\n" +                  % (config_lines_init, config_lines_init - config_lines_ndef)) + +    return ERROR_CODE.NONE + + +def main(): +    """ +    calls the mode that was specified by the -m script argument, +    defaults to reduce_mode if not provided or unknown mode + +    accesses global var(s): MODES, FLAGS + + +    :return: int +    ---------------------------------------------------------------------------- +        return code +    """ +    if FLAGS.mode == MODES[1]: +        return no_default_mode() + +    return reduce_mode() + + +def valid_file(arg_parser, *args): +    """ +    checks if on of the provided paths is a file + + +    Parameters +    ---------------------------------------------------------------------------- +    :param arg_parser: +        argument parser object that is called if no file is found + +    :param args: list< str > +        a list of file path that is going to be checked + + +    :return: str +    ---------------------------------------------------------------------------- +        path to an existing file +    """ +    arg = None +    found_flag = False +    for arg in args: +        if exists(arg): +            found_flag = True +            break +    if not found_flag: +        arg_parser.error("file(s) do not exist: %s" % args) + +    return arg + + +if __name__ == "__main__": +    """ +    parses all script arguments and calls main() + +    accesses global var(s): FLAGS, ERROR_CODE, MODES +    """ +    arg_parser = argparse.ArgumentParser() + +    group_general = arg_parser.add_argument_group( +        'general options', 'Options used by both modes') + +    group_general.add_argument( +        '-q', '--quiet', +        default=False, +        action='store_true', +        help='Whether or not messages, other than the actual config output, ' +             'should be printed to stdout.' +    ) +    group_general.add_argument( +        '--empty-nochange', +        default=False, +        action='store_true', +        help='Do not print anything to stdout if no options could be removed' +    ) +    group_general.add_argument( +        '-m', '--mode', +        type=str, +        choices=MODES, +        default=MODES[0], +        help="The script operation mode. Defaults to '%s'" % MODES[0] +    ) +    group_general.add_argument( +        '-b', '--uncrustify_binary_path', +        metavar='<path>', +        type=lambda x: valid_file( +            arg_parser, x, +            "../build/uncrustify.exe", +            "../build/Debug/uncrustify", +            "../build/Debug/uncrustify.exe", +            "../build/Release/uncrustify", +            "../build/Release/uncrustify.exe"), +        default="../build/uncrustify", +        help="The Uncrustify binary file path. Is searched in known locations " +             "in the 'Uncrustify/build/' directory if no <path> is provided." +    ) +    group_general.add_argument( +        '-c', '--config_file_path', +        metavar='<path>', +        type=lambda x: valid_file(arg_parser, x), +        required=True, +        help='Path to the config file.' +    ) + +    group_reduce = arg_parser.add_argument_group( +        'reduce mode', 'Options to reduce configuration file options') + +    group_reduce.add_argument( +        '-i', '--input_file_path', +        metavar='<path>', +        type=lambda x: valid_file(arg_parser, x), +        nargs='+', +        action='append', +        help="Path to the unformatted source file. " +             "Required if mode '%s' is used" % MODES[0] +    ) +    group_reduce.add_argument( +        '-f', '--formatted_file_path', +        metavar='<path>', +        type=lambda x: valid_file(arg_parser, x), +        nargs='+', +        action='append', +        help="Path to the formatted source file. " +             "Required if mode '%s' is used" % MODES[0] +    ) +    group_reduce.add_argument( +        '-l', '--lang', +        metavar='<str>', +        nargs='+', +        required=False, +        action='append', +        help='Uncrustify processing language for each input file' +    ) +    group_reduce.add_argument( +        '-j', '--jobs', +        metavar='<nr>', +        type=int, +        default=cpu_count(), +        help='Number of concurrent jobs.' +    ) +    group_reduce.add_argument( +        '-p', '--passes', +        metavar='<nr>', +        type=int, +        default=5, +        help='Max. number of cleaning passes.' +    ) + +    group_no_default = arg_parser.add_argument_group( +        'no-default mode', 'Options to remove configuration file option with ' +                           'default values: ~~_Currently only the general' +                           ' options are used for this mode_~~') +    FLAGS, unparsed = arg_parser.parse_known_args() + +    if FLAGS.lang is not None: +        FLAGS.lang = [j for i in FLAGS.lang for j in i] + +    if FLAGS.mode == MODES[0]: +        if not FLAGS.input_file_path or not FLAGS.formatted_file_path: +            arg_parser.error("Flags -f and -i are required in Mode '%s'!" +                             % MODES[0]) +            sys_exit(ERROR_CODE.FLAGS) + +        # flatten 2 dimensional args: -f p -f p -f p -f p0 p1 p2 -> [[],[], ...] +        FLAGS.input_file_path = [j for i in FLAGS.input_file_path for j in i] + +        FLAGS.formatted_file_path = [j for i in +                                     FLAGS.formatted_file_path for j in i] + +        if len(FLAGS.input_file_path) != len(FLAGS.formatted_file_path): +            print("Unequal amount of input and formatted file paths.", +                  file=stderr) +            sys_exit(ERROR_CODE.FLAGS) + +    sys_exit(main()) | 
