1#!/usr/bin/python
2"""
3option_reducer.py
4
5reduces options in a given config file to the minimum while still maintaining
6desired formatting
7
8:author:  Daniel Chumak
9:license: GPL v2+
10"""
11
12# Possible improvements:
13# - parallelize add_back()
14# - (maybe) reduce amount of written config file, see Uncrustify --set
15
16from __future__ import print_function  # python >= 2.6
17import argparse
18
19from os import name as os_name, sep as os_path_sep, fdopen as os_fdopen, \
20    remove as os_remove
21from os.path import exists, join as path_join
22from subprocess import Popen, PIPE
23from sys import exit as sys_exit, stderr, stdout
24from shutil import rmtree
25from multiprocessing import cpu_count
26from tempfile import mkdtemp, mkstemp
27from contextlib import contextmanager
28from collections import OrderedDict
29from threading import Timer
30from multiprocessing.pool import Pool
31from itertools import combinations
32
33FLAGS = None
34NULL_DEV = "/dev/null" if os_name != "nt" else "nul"
35
36
37def enum(**enums):
38    return type('Enum', (), enums)
39
40
41RESTULTSFLAG = enum(NONE=0, REMOVE=1, KEEP=2)
42ERROR_CODE = enum(NONE=0, FLAGS=200, SANITY0=201, SANITY1=202)
43MODES = ("reduce", "no-default")
44
45
46@contextmanager
47def make_temp_directory():
48    """
49    Wraps tempfile.mkdtemp to use it inside a with statement that auto deletes
50    the temporary directory with its content after the with block closes
51
52
53    :return: str
54    ----------------------------------------------------------------------------
55        path to the generated directory
56    """
57    temp_dir = mkdtemp()
58    try:
59        yield temp_dir
60    finally:
61        rmtree(temp_dir)
62
63
64@contextmanager
65def make_raw_temp_file(*args, **kwargs):
66    """
67    Wraps tempfile.mkstemp to use it inside a with statement that auto deletes
68    the file after the with block closes
69
70
71    Parameters
72    ----------------------------------------------------------------------------
73    :param args, kwargs:
74        arguments passed to mkstemp
75
76
77    :return: int, str
78    ----------------------------------------------------------------------------
79        the file descriptor and the file path of the created temporary file
80    """
81    fd, tmp_file_name = mkstemp(*args, **kwargs)
82    try:
83        yield (fd, tmp_file_name)
84    finally:
85        os_remove(tmp_file_name)
86
87
88@contextmanager
89def open_fd(*args, **kwargs):
90    """
91    Wraps os.fdopen to use it inside a with statement that auto closes the
92    generated file descriptor after the with block closes
93
94
95    Parameters
96    ----------------------------------------------------------------------------
97    :param args, kwargs:
98        arguments passed to os.fdopen
99
100
101    :return: TextIOWrapper
102    ----------------------------------------------------------------------------
103        open file object connected to the file descriptor
104    """
105    fp = os_fdopen(*args, **kwargs)
106    try:
107        yield fp
108    finally:
109        fp.close()
110
111
112def term_proc(proc, timeout):
113    """
114    helper function to terminate a process
115
116
117    Parameters
118    ----------------------------------------------------------------------------
119    :param proc: process object
120        the process object that is going to be terminated
121
122    :param timeout: dictionary
123        a dictionary (used as object reference) to set a flag that indicates
124        that the process is going to be terminated
125    """
126    timeout["value"] = True
127    proc.terminate()
128
129
130def uncrustify(unc_bin_path, cfg_file_path, unformatted_file_path,
131               lang=None, debug_file=None, check=False):
132    """
133    executes Uncrustify and captures its stdout
134
135
136    Parameters
137    ----------------------------------------------------------------------------
138    :param unc_bin_path: str
139        path to the Uncrustify binary
140
141    :param cfg_file_path: str
142        path to a config file for Uncrustify
143
144    :param unformatted_file_path: str
145        path to a file that is going to be formatted
146
147    :param lang: str / None
148        Uncrustifys -l argument
149
150    :param debug_file: str / None
151        Uncrustifys -p argument
152
153    :param check: bool
154        Used to control whether Uncrustifys --check is going to be used
155
156
157    :return: str / None
158    ----------------------------------------------------------------------------
159        returns the stdout from Uncrustify or None if the process takes to much
160        time (set to 5 sec)
161    """
162
163    args = [unc_bin_path, "-q", "-c", cfg_file_path, '-f',
164            unformatted_file_path]
165    if lang:
166        args.extend(("-l", lang))
167    if debug_file:
168        args.extend(('-p', debug_file))
169    if check:
170        args.append('--check')
171
172    proc = Popen(args, stdout=PIPE, stderr=PIPE)
173
174    timeout = {"value": False}
175    timer = Timer(5, term_proc, [proc, timeout])
176    timer.start()
177
178    output_b, error_txt_b = proc.communicate()
179
180    timer.cancel()
181
182    if timeout["value"]:
183        print("uncrustify proc timeout: %s" % ' '.join(args), file=stderr)
184        return None
185
186    error = error_txt_b.decode("UTF-8")
187    if error:
188        print("Uncrustify %s stderr:\n %s" % (unformatted_file_path, error),
189              file=stderr)
190
191    return output_b
192
193
194def same_expected_generated(formatted_path, unc_bin_path, cfg_file_path,
195                            input_path, lang=None):
196    """
197    Calls uncrustify and compares its generated output with the content of a
198    file
199
200
201    Parameters
202    ----------------------------------------------------------------------------
203    :param formatted_path: str
204        path to a file containing the expected content
205
206    :params unc_bin_path, cfg_file_path, input_path, lang: str, str, str,
207                                                           str / None
208        see uncrustify()
209
210
211    :return: bool
212    ----------------------------------------------------------------------------
213        True if the strings match, False otherwise
214    """
215
216    expected_string = ''
217    with open(formatted_path, 'rb') as f:
218        expected_string = f.read()
219
220    formatted_string = uncrustify(unc_bin_path, cfg_file_path, input_path, lang)
221
222    return True if formatted_string == expected_string else False
223
224
225def process_uncrustify(args):
226    """
227    special wrapper for same_expected_generated()
228
229    accesses global var(s): RESTULTSFLAG
230
231
232    Parameters
233    ----------------------------------------------------------------------------
234    :param args: list / tuple< int, ... >
235        this function is intended to be called by multiprocessing.pool.map()
236        therefore all arguments are inside a list / tuple:
237            id: int
238                an index number needed by the caller to differentiate runs
239
240            other parameters:
241                see same_expected_generated()
242
243
244    :return: tuple< int, RESTULTSFLAG >
245    ----------------------------------------------------------------------------
246        returns a tuple containing the id and a RESTULTSFLAG, REMOVE if both
247        strings are equal, KEEP if not
248    """
249
250    id = args[0]
251    res = same_expected_generated(*args[1:])
252
253    return id, RESTULTSFLAG.REMOVE if res else RESTULTSFLAG.KEEP
254
255
256def write_config_file(args):
257    """
258    Writes all but one excluded option into a config file
259
260
261    Parameters
262    ----------------------------------------------------------------------------
263    :param args: list / tuple< list< tuple< str, str > >, str, int >
264        this function is intended to be called by multiprocessing.pool.map()
265        therefore all arguments are inside a list / tuple:
266
267            config_list: list< tuple< str, str > >
268                a list of tuples containing option names and values
269
270            tmp_dir: str
271                path to a directory in which the config file is going to be
272                written
273
274            exclude_idx: int
275                index for an option that is not going to be written into the
276                config file
277    """
278
279    config_list, tmp_dir, exclude_idx = args
280
281    with open("%s%suncr-%d.cfg" % (tmp_dir, os_path_sep, exclude_idx),
282              'w') as f:
283        print_config(config_list, target_file_obj=f, exclude_idx=exclude_idx)
284
285
286def write_config_file2(args):
287    """
288    Writes two option lists into a config file
289
290
291    Parameters
292    ----------------------------------------------------------------------------
293    :param args: list< tuple< str, str > >,
294                 list< tuple< str, str > >, str, int
295        this function is intended to be called by multiprocessing.pool.map()
296        therefore all arguments are inside a list / tuple:
297
298            config_list: list< tuple< str, str > >
299                the first list of tuples containing option names and values
300
301            test_list: list< tuple< str, str > >
302                the second list of tuples containing option names and values
303
304            tmp_dir: str
305                path to a directory in which the config file is going to be
306                written
307
308            idx: int
309                index that is going to be used for the filename
310    """
311
312    config_list0, config_list1, tmp_dir, idx = args
313
314    with open("%s%suncr-r-%d.cfg" % (tmp_dir, os_path_sep, idx), 'w') as f:
315        print_config(config_list0, target_file_obj=f)
316        print("", end='\n', file=f)
317        print_config(config_list1, target_file_obj=f)
318
319
320def gen_multi_combinations(elements, N):
321    """
322    generator function that generates, based on a set of elements, all
323    combinations of 1..N elements
324
325
326    Parameters
327    ----------------------------------------------------------------------------
328    :param elements: list / tuple
329        a list of elements from which the combinations will be generated
330
331    :param N:
332        the max number of element in a combination
333
334
335    :return: list
336    ----------------------------------------------------------------------------
337        yields a single combination of the elements
338
339    >>> gen_multi_combinations(["a", "b", "c"], 3)
340    (a); (b); (c); (a,b); (a,c); (b,c); (a,b,c)
341    """
342
343    fields = len(elements)
344    if N > fields:
345        raise Exception("Error: N > len(options)")
346    if N <= 0:
347        raise Exception("Error: N <= 0")
348
349    for n in range(1, N + 1):
350        yield combinations(elements, n)
351
352
353def add_back(unc_bin_path, input_files, formatted_files, langs, options_r,
354             options_k, tmp_dir):
355    """
356    lets Uncrustify format files with generated configs files until all
357    formatted files match their according expected files.
358
359    Multiple config files are generated based on a (base) list of Uncrustify
360    options combined with additional (new) options derived from combinations of
361    another list of options.
362
363
364    accesses global var(s): RESTULTSFLAG
365
366
367    Parameters
368    ----------------------------------------------------------------------------
369    :param unc_bin_path: str
370        path to the Uncrustify binary
371
372    :param input_files: list / tuple< str >
373        a list containing paths to a files that are going to be formatted
374
375    :param formatted_files: list / tuple< str >
376        a list containing paths to files containing the expected contents
377
378    :param langs: list / tuple< str > / None
379        a list of languages the files, used as Uncrustifys -l argument
380        can be None or shorter than the amount of provided files
381
382    :param options_r: list< tuple< str, str > >
383        the list of options from which combinations will be derived
384
385    :param options_k: list< tuple< str, str > >
386        the (base) list of Uncrustify options
387
388    :param tmp_dir: str
389        the directory in which the config files will be written to
390
391
392    :return: list< tuple< str, str > > / None
393    ----------------------------------------------------------------------------
394        list of additional option that were needed to generate matching file
395        contents
396    """
397
398    lang_max_idx = -1 if langs is None else len(langs) - 1
399    file_len = len(input_files)
400
401    if len(formatted_files) != file_len:
402        raise Exception("len(input_files) != len(formatted_files)")
403
404    for m_combination in gen_multi_combinations(options_r, len(options_r)):
405        for idx, (r_combination) in enumerate(m_combination):
406            write_config_file2((options_k, r_combination, tmp_dir, idx))
407
408            cfg_file_path = "%s%suncr-r-%d.cfg" % (tmp_dir, os_path_sep, idx)
409            res = []
410
411            for file_idx in range(file_len):
412                lang = None if idx > lang_max_idx else langs[file_idx]
413
414                r = process_uncrustify(
415                    (0, formatted_files[file_idx], unc_bin_path, cfg_file_path,
416                     input_files[file_idx], lang))
417                res.append(r[1])
418
419            # all files, flag = remove -> option can be removed -> equal output
420            if res.count(RESTULTSFLAG.REMOVE) == len(res):
421                return r_combination
422    return None
423
424
425def sanity_raw_run(args):
426    """
427    wrapper for same_expected_generated(), prints error message if the config
428    file does not generate the expected result
429
430    Parameters
431    ----------------------------------------------------------------------------
432    :param args:
433        see same_expected_generated
434
435
436    :return:
437    ----------------------------------------------------------------------------
438        see same_expected_generated
439    """
440    res = same_expected_generated(*args)
441
442    if not res:
443        formatted_file_path = args[0]
444        config_file_path = args[2]
445        input_file_path = args[3]
446
447        print("\nprovided config does not create formatted source file:\n"
448              "    %s\n    %s\n->| %s"
449              % (input_file_path, config_file_path, formatted_file_path),
450              file=stderr)
451    return res
452
453
454def sanity_run(args):
455    """
456    wrapper for same_expected_generated(), prints error message if the config
457    file does not generate the expected result
458
459
460    Parameters
461    ----------------------------------------------------------------------------
462    :param args:
463        see same_expected_generated
464
465
466    :return:
467    ----------------------------------------------------------------------------
468        see same_expected_generated
469    """
470    res = same_expected_generated(*args)
471
472    if not res:
473        formatted_file_path = args[0]
474        input_file_path = args[3]
475
476        print("\ngenerated config does not create formatted source file:\n"
477              "    %s\n    %s"
478              % (input_file_path, formatted_file_path), file=stderr)
479    return res
480
481
482def sanity_run_splitter(uncr_bin, config_list, input_files, formatted_files,
483                        langs, tmp_dir, jobs):
484    """
485    writes config option into a file and tests if every input file is formatted
486    so that is matches the content of the according expected file
487
488
489    Parameters
490    ----------------------------------------------------------------------------
491    :param uncr_bin: str
492        path to the Uncrustify binary
493
494    :param config_list: list< tuple< str, str > >
495        a list of tuples containing option names and values
496
497    :param input_files: list / tuple< str >
498        a list containing paths to a files that are going to be formatted
499
500    :param formatted_files: list / tuple< str >
501        a list containing paths to files containing the expected contents
502
503    :param langs: list / tuple< str > / None
504        a list of languages the files, used as Uncrustifys -l argument
505        can be None or shorter than the amount of provided files
506
507    :param tmp_dir: str
508        the directory in which the config files will be written to
509
510    :param jobs: int
511        number of processes to use
512
513
514    :return: bool
515    ----------------------------------------------------------------------------
516        True if all files generate correct results, False oterhwise
517    """
518
519    file_len = len(input_files)
520    if len(formatted_files) != file_len:
521        raise Exception("len(input_files) != len(formatted_files)")
522
523    gen_cfg_path = path_join(tmp_dir, "gen.cfg")
524    with open(gen_cfg_path, 'w') as f:
525        print_config(config_list, target_file_obj=f)
526
527    lang_max_idx = -1 if langs is None else len(langs) - 1
528    args = []
529
530    for idx in range(file_len):
531        lang = None if idx > lang_max_idx else langs[idx]
532
533        args.append((formatted_files[idx], uncr_bin, gen_cfg_path,
534                     input_files[idx], lang))
535
536    pool = Pool(processes=jobs)
537    sr = pool.map(sanity_run, args)
538
539    return False not in sr
540
541
542def print_config(config_list, target_file_obj=stdout, exclude_idx=()):
543    """
544    prints config options into a config file
545
546
547    Parameters
548    ----------------------------------------------------------------------------
549    :param config_list: list< tuple< str, str > >
550        a list containing pairs of option names and option values
551
552    :param target_file_obj: file object
553        see file param of print()
554
555    :param exclude_idx: int / list< int >
556        index of option(s) that are not going to be printed
557    """
558
559    if not config_list:
560        return
561    config_list_len = len(config_list)
562
563    # check if exclude_idx list is empty -> assign len
564    if type(exclude_idx) in (list, tuple) and not exclude_idx:
565        exclude_idx = [config_list_len]
566    else:
567        # sort it, unless it is an int -> transform into a list
568        try:
569            exclude_idx = sorted(exclude_idx)
570        except TypeError:
571            exclude_idx = [exclude_idx]
572
573    # extracted first loop round:
574    # do not print '\n' for the ( here non-existing) previous line
575    if exclude_idx[0] != 0:
576        print("%s = %s" % (config_list[0][0].ljust(31, ' '), config_list[0][1]),
577              end='', file=target_file_obj)
578    # also print space if a single option was provided and it is going to be
579    # excluded. This is done in order to be able to differentiate between
580    # --empty-nochange and the case where all options can be removed
581    elif config_list_len == 1:
582        print(' ', end='', file=target_file_obj)
583        return
584
585    start_idx = 1
586    for end in exclude_idx:
587        end = min(end, config_list_len)
588
589        for idx in range(start_idx, end):
590            print("\n%s = %s"
591                  % (config_list[idx][0].ljust(31, ' '), config_list[idx][1]),
592                  end='', file=target_file_obj)
593
594        start_idx = min(end + 1, config_list_len)
595
596    # after
597    for idx in range(start_idx, config_list_len):
598        print("\n%s = %s"
599              % (config_list[idx][0].ljust(31, ' '), config_list[idx][1]),
600              end='', file=target_file_obj)
601
602
603def get_non_default_options(unc_bin_path, cfg_file_path):
604    """
605    calls Uncrustify to generate a debug file from which a config only with
606    non default valued options are extracted
607
608    accesses global var(s): NULL_DEV
609
610
611    Parameters
612    ----------------------------------------------------------------------------
613    :param unc_bin_path: str
614        path to the Uncrustify binary
615
616    :param cfg_file_path: str
617        path to a config file for Uncrustify
618
619
620    :return: list< str >
621    ----------------------------------------------------------------------------
622        amount of lines in the provided and shortened config
623    """
624    lines = []
625
626    with make_raw_temp_file(suffix='.unc') as (fd, file_path):
627        # make debug file
628        uncrustify(unc_bin_path, cfg_file_path, NULL_DEV, debug_file=file_path,
629                   check=True)
630
631        # extract non comment lines -> non default config lines
632        with open_fd(fd, 'r') as fp:
633            lines = fp.read().splitlines()
634            lines = [line for line in lines if not line[:1] == '#']
635
636    return lines
637
638
639def parse_config_file(file_obj):
640    """
641    Reads in a Uncrustify config file
642
643
644    Parameters
645    ----------------------------------------------------------------------------
646    :param file_obj:
647        the file object of an opened config file
648
649
650    :return: list< tuple< str, str > >
651    ----------------------------------------------------------------------------
652        a list containing pairs of option names and option values
653    """
654    # dict used to only save the last option setting if the same option occurs
655    # multiple times, without this:
656    #   optionA0 can be removed because optionA1 = s0, and
657    #   optionA1 can be removed because optionA0 = s0
658    # -> optionA0, optionA1 are both removed
659    config_map = OrderedDict()
660
661    # special keys may not have this limitation, as for example
662    # 'set x y' and 'set x z' do not overwrite each other
663    special_keys = {'macro-open', 'macro-else', 'macro-close', 'set', 'type',
664                    'file_ext', 'define'}
665    special_list = []
666
667    for line in file_obj:
668        # cut comments
669        pound_pos = line.find('#')
670        if pound_pos != -1:
671            line = line[:pound_pos]
672
673        split_pos = line.find('=')
674        if split_pos == -1:
675            split_pos = line.find(' ')
676        if split_pos == -1:
677            continue
678
679        key = line[:split_pos].strip()
680        value = line[split_pos + 1:].strip()
681
682        if key in special_keys:
683            special_list.append((key, value))
684        else:
685            config_map[key] = value
686
687    config_list = list(config_map.items())
688    config_list += special_list
689
690    return config_list
691
692
693def count_lines(file_path):
694    """
695    returns the count of lines in a file by counting '\n' chars
696
697    Parameters
698    ----------------------------------------------------------------------------
699    :param file_path: str
700        file in which the lines will be counted
701
702
703    :return: int
704    ----------------------------------------------------------------------------
705        number a lines
706    """
707    in_count = 0
708    with open(file_path, 'r') as f:
709        in_count = f.read().count('\n') + 1
710    return in_count
711
712
713def reduce(options_list):
714    """
715    Reduces the given options to a minimum
716
717    accesses global var(s): FLAGS, RESTULTSFLAG, ERROR_CODE
718
719    Parameters
720    ----------------------------------------------------------------------------
721    :param options_list: list< tuple< str, str > >
722        the list of options that are going to be reduced
723
724    :return: int, list< tuple< str, str > >
725        status return code, reduced options
726    """
727    config_list_len = len(options_list)
728    ret_flag = ERROR_CODE.NONE
729
730    file_count = len(FLAGS.input_file_path)
731    lang_max_idx = -1 if FLAGS.lang is None else len(FLAGS.lang) - 1
732
733    pool = Pool(processes=FLAGS.jobs)
734    with make_temp_directory() as tmp_dir:
735        # region sanity run ----------------------------------------------------
736        args = []
737        for idx in range(file_count):
738            lang = None if idx > lang_max_idx else FLAGS.lang[idx]
739
740            args.append((FLAGS.formatted_file_path[idx],
741                         FLAGS.uncrustify_binary_path, FLAGS.config_file_path,
742                         FLAGS.input_file_path[idx], lang))
743        sr = pool.map(sanity_raw_run, args)
744        del args[:]
745
746        if False in sr:
747            return ERROR_CODE.SANITY0, []
748        del sr[:]
749
750        # endregion
751        # region config generator loop -----------------------------------------
752        args = []
753
754        for e_idx in range(config_list_len):
755            args.append((options_list, tmp_dir, e_idx))
756        pool.map(write_config_file, args)
757
758        del args[:]
759
760        # endregion
761        # region main loop -----------------------------------------------------
762        args = []
763        jobs = config_list_len * file_count
764
765        for idx in range(jobs):
766            file_idx = idx // config_list_len
767            option_idx = idx % config_list_len
768
769            cfg_file_path = "%s%suncr-%d.cfg" \
770                            % (tmp_dir, os_path_sep, option_idx)
771            lang = None if idx > lang_max_idx else FLAGS.lang[file_idx]
772
773            args.append((idx, FLAGS.formatted_file_path[file_idx],
774                         FLAGS.uncrustify_binary_path, cfg_file_path,
775                         FLAGS.input_file_path[file_idx], lang))
776
777        results = pool.map(process_uncrustify, args)
778        del args[:]
779        # endregion
780        # region clean results -------------------------------------------------
781        option_flags = [RESTULTSFLAG.NONE] * config_list_len
782
783        for r in results:
784            idx = r[0]
785            flag = r[1]
786
787            option_idx = idx % config_list_len
788
789            if option_flags[option_idx] == RESTULTSFLAG.KEEP:
790                continue
791
792            option_flags[option_idx] = flag
793        del results[:]
794        # endregion
795
796        options_r = [options_list[idx] for idx, x in enumerate(option_flags)
797                     if x == RESTULTSFLAG.REMOVE]
798        options_list = [options_list[idx] for idx, x in enumerate(option_flags)
799                        if x == RESTULTSFLAG.KEEP]
800
801        del option_flags[:]
802
803        # region sanity run ----------------------------------------------------
804        # options can be removed one at a time generating appropriate results,
805        # oddly enough sometimes a config generated this way can fail when a
806        # combination of multiple options is missing
807        s_flag = True
808        if options_r:
809            s_flag = sanity_run_splitter(
810                FLAGS.uncrustify_binary_path, options_list,
811                FLAGS.input_file_path, FLAGS.formatted_file_path, FLAGS.lang,
812                tmp_dir, FLAGS.jobs)
813
814        if not s_flag:
815            ret_flag = ERROR_CODE.SANITY1
816            print("\n\nstumbled upon complex option dependencies in \n"
817                  "    %s\n"
818                  "trying to add back minimal amount of removed options\n"
819                  % FLAGS.config_file_path, file=stderr)
820
821            ret_options = add_back(
822                FLAGS.uncrustify_binary_path, FLAGS.input_file_path,
823                FLAGS.formatted_file_path, FLAGS.lang, options_r,
824                options_list, tmp_dir)
825
826            if ret_options:
827                options_list.extend(ret_options)
828
829                s_flag = sanity_run_splitter(
830                    FLAGS.uncrustify_binary_path, options_list,
831                    FLAGS.input_file_path, FLAGS.formatted_file_path,
832                    FLAGS.lang, tmp_dir, FLAGS.jobs)
833
834                if s_flag:
835                    print("Success!", file=stderr)
836                    ret_flag = ERROR_CODE.NONE
837                    # endregion
838    return ret_flag, options_list if ret_flag == ERROR_CODE.NONE else []
839
840
841def reduce_mode():
842    """
843    the mode that minimizes a config file as much as possible
844
845    accesses global var(s): FLAGS, ERROR_CODE
846    """
847    ret_flag = ERROR_CODE.NONE
848    option_list = {}
849
850    # gen & parse non default config
851    lines = get_non_default_options(FLAGS.uncrustify_binary_path,
852                                    FLAGS.config_file_path)
853    option_list = parse_config_file(lines)
854    config_list_len = len(option_list)
855
856    config_lines_init = count_lines(FLAGS.config_file_path)
857    config_lines_ndef = len(lines)
858    del lines[:]
859
860    # early return if all options are already removed at this point
861    if config_list_len == 0:
862        if not FLAGS.empty_nochange \
863                or (config_lines_init - config_lines_ndef) > 0:
864            if not FLAGS.quiet:
865                print("\n%s" % '# '.ljust(78, '-'))
866
867            print(" ")
868
869            if not FLAGS.quiet:
870                print("%s" % '# '.ljust(78, '-'))
871                print("# initial config lines: %d,\n"
872                      "# default options and unneeded lines: %d,\n"
873                      "# unneeded options: 0,\n"
874                      "# kept options: 0"
875                      % (config_lines_init, config_lines_init))
876        print("ret_flag: 0", file=stderr)
877        return ERROR_CODE.NONE
878
879    # gen reduced options
880    config_lines_redu = -1
881    for i in range(FLAGS.passes):
882        old_config_lines_redu = config_lines_redu
883
884        ret_flag, option_list = reduce(option_list)
885        config_lines_redu = len(option_list)
886
887        if ret_flag != ERROR_CODE.NONE \
888                or config_lines_redu == old_config_lines_redu:
889            break
890
891    if ret_flag == ERROR_CODE.NONE:
892        # use the debug file trick again to get correctly sorted options
893        with make_raw_temp_file(suffix='.unc') as (fd, file_path):
894            with open_fd(fd, 'w') as f:
895                print_config(option_list, target_file_obj=f)
896
897            lines = get_non_default_options(FLAGS.uncrustify_binary_path,
898                                            file_path)
899            option_list = parse_config_file(lines)
900
901        # print output + stats
902        if not FLAGS.empty_nochange or config_lines_ndef != config_lines_redu:
903            if not FLAGS.quiet:
904                print("\n%s" % '# '.ljust(78, '-'))
905
906            print_config(option_list)
907
908            if not FLAGS.quiet:
909                print("\n%s" % '# '.ljust(78, '-'))
910                print("# initial config lines: %d,\n"
911                      "# default options and unneeded lines: %d,\n"
912                      "# unneeded options: %d,\n"
913                      "# kept options: %d"
914                      % (config_lines_init,
915                         config_lines_init - config_lines_ndef,
916                         config_lines_ndef - config_lines_redu,
917                         config_lines_redu))
918
919    print("ret_flag: %d" % ret_flag, file=stderr)
920    return ret_flag
921
922
923def no_default_mode():
924    """
925    the mode removes all unnecessary lines and options with default values
926
927    accesses global var(s): FLAGS, ERROR_CODE
928    """
929
930    lines = get_non_default_options(FLAGS.uncrustify_binary_path,
931                                    FLAGS.config_file_path, )
932    config_lines_ndef = len(lines)
933    config_lines_init = count_lines(FLAGS.config_file_path)
934
935    if not FLAGS.empty_nochange or (config_lines_ndef != config_lines_init):
936        if not FLAGS.quiet:
937            print("%s" % '# '.ljust(78, '-'))
938
939        options_str = '\n'.join(lines)
940        if not options_str:
941            print(" ")
942        else:
943            print(options_str, file=stdout)
944
945        if not FLAGS.quiet:
946            print("%s" % '# '.ljust(78, '-'))
947            print("# initial config lines: %d,\n"
948                  "# default options and unneeded lines: %d,\n"
949                  % (config_lines_init, config_lines_init - config_lines_ndef))
950
951    return ERROR_CODE.NONE
952
953
954def main():
955    """
956    calls the mode that was specified by the -m script argument,
957    defaults to reduce_mode if not provided or unknown mode
958
959    accesses global var(s): MODES, FLAGS
960
961
962    :return: int
963    ----------------------------------------------------------------------------
964        return code
965    """
966    if FLAGS.mode == MODES[1]:
967        return no_default_mode()
968
969    return reduce_mode()
970
971
972def valid_file(arg_parser, *args):
973    """
974    checks if on of the provided paths is a file
975
976
977    Parameters
978    ----------------------------------------------------------------------------
979    :param arg_parser:
980        argument parser object that is called if no file is found
981
982    :param args: list< str >
983        a list of file path that is going to be checked
984
985
986    :return: str
987    ----------------------------------------------------------------------------
988        path to an existing file
989    """
990    arg = None
991    found_flag = False
992    for arg in args:
993        if exists(arg):
994            found_flag = True
995            break
996    if not found_flag:
997        arg_parser.error("file(s) do not exist: %s" % args)
998
999    return arg
1000
1001
1002if __name__ == "__main__":
1003    """
1004    parses all script arguments and calls main()
1005
1006    accesses global var(s): FLAGS, ERROR_CODE, MODES
1007    """
1008    arg_parser = argparse.ArgumentParser()
1009
1010    group_general = arg_parser.add_argument_group(
1011        'general options', 'Options used by both modes')
1012
1013    group_general.add_argument(
1014        '-q', '--quiet',
1015        default=False,
1016        action='store_true',
1017        help='Whether or not messages, other than the actual config output, '
1018             'should be printed to stdout.'
1019    )
1020    group_general.add_argument(
1021        '--empty-nochange',
1022        default=False,
1023        action='store_true',
1024        help='Do not print anything to stdout if no options could be removed'
1025    )
1026    group_general.add_argument(
1027        '-m', '--mode',
1028        type=str,
1029        choices=MODES,
1030        default=MODES[0],
1031        help="The script operation mode. Defaults to '%s'" % MODES[0]
1032    )
1033    group_general.add_argument(
1034        '-b', '--uncrustify_binary_path',
1035        metavar='<path>',
1036        type=lambda x: valid_file(
1037            arg_parser, x,
1038            "../build/uncrustify.exe",
1039            "../build/Debug/uncrustify",
1040            "../build/Debug/uncrustify.exe",
1041            "../build/Release/uncrustify",
1042            "../build/Release/uncrustify.exe"),
1043        default="../build/uncrustify",
1044        help="The Uncrustify binary file path. Is searched in known locations "
1045             "in the 'Uncrustify/build/' directory if no <path> is provided."
1046    )
1047    group_general.add_argument(
1048        '-c', '--config_file_path',
1049        metavar='<path>',
1050        type=lambda x: valid_file(arg_parser, x),
1051        required=True,
1052        help='Path to the config file.'
1053    )
1054
1055    group_reduce = arg_parser.add_argument_group(
1056        'reduce mode', 'Options to reduce configuration file options')
1057
1058    group_reduce.add_argument(
1059        '-i', '--input_file_path',
1060        metavar='<path>',
1061        type=lambda x: valid_file(arg_parser, x),
1062        nargs='+',
1063        action='append',
1064        help="Path to the unformatted source file. "
1065             "Required if mode '%s' is used" % MODES[0]
1066    )
1067    group_reduce.add_argument(
1068        '-f', '--formatted_file_path',
1069        metavar='<path>',
1070        type=lambda x: valid_file(arg_parser, x),
1071        nargs='+',
1072        action='append',
1073        help="Path to the formatted source file. "
1074             "Required if mode '%s' is used" % MODES[0]
1075    )
1076    group_reduce.add_argument(
1077        '-l', '--lang',
1078        metavar='<str>',
1079        nargs='+',
1080        required=False,
1081        action='append',
1082        help='Uncrustify processing language for each input file'
1083    )
1084    group_reduce.add_argument(
1085        '-j', '--jobs',
1086        metavar='<nr>',
1087        type=int,
1088        default=cpu_count(),
1089        help='Number of concurrent jobs.'
1090    )
1091    group_reduce.add_argument(
1092        '-p', '--passes',
1093        metavar='<nr>',
1094        type=int,
1095        default=5,
1096        help='Max. number of cleaning passes.'
1097    )
1098
1099    group_no_default = arg_parser.add_argument_group(
1100        'no-default mode', 'Options to remove configuration file option with '
1101                           'default values: ~~_Currently only the general'
1102                           ' options are used for this mode_~~')
1103    FLAGS, unparsed = arg_parser.parse_known_args()
1104
1105    if FLAGS.lang is not None:
1106        FLAGS.lang = [j for i in FLAGS.lang for j in i]
1107
1108    if FLAGS.mode == MODES[0]:
1109        if not FLAGS.input_file_path or not FLAGS.formatted_file_path:
1110            arg_parser.error("Flags -f and -i are required in Mode '%s'!"
1111                             % MODES[0])
1112            sys_exit(ERROR_CODE.FLAGS)
1113
1114        # flatten 2 dimensional args: -f p -f p -f p -f p0 p1 p2 -> [[],[], ...]
1115        FLAGS.input_file_path = [j for i in FLAGS.input_file_path for j in i]
1116
1117        FLAGS.formatted_file_path = [j for i in
1118                                     FLAGS.formatted_file_path for j in i]
1119
1120        if len(FLAGS.input_file_path) != len(FLAGS.formatted_file_path):
1121            print("Unequal amount of input and formatted file paths.",
1122                  file=stderr)
1123            sys_exit(ERROR_CODE.FLAGS)
1124
1125    sys_exit(main())
1126