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