#!/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='', 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 is provided." ) group_general.add_argument( '-c', '--config_file_path', metavar='', 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='', 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='', 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='', nargs='+', required=False, action='append', help='Uncrustify processing language for each input file' ) group_reduce.add_argument( '-j', '--jobs', metavar='', type=int, default=cpu_count(), help='Number of concurrent jobs.' ) group_reduce.add_argument( '-p', '--passes', metavar='', 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())