summaryrefslogtreecommitdiffstats
path: root/debian/uncrustify-trinity/uncrustify-trinity-0.76.0/scripts/option_reducer.py
blob: 403ff92b8a06c6d3fb1a95947b838e0cc20d05aa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
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())