"Fossies" - the Fresh Open Source Software Archive

Member "beautysh-6.0.1/beautysh/beautysh.py" (10 Mar 2020, 16659 Bytes) of package /linux/privat/beautysh-6.0.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "beautysh.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 5.0.2_vs_6.0.1.

    1 #!/usr/bin/env python3
    2 # -*- coding: utf-8 -*-
    3 """A beautifier for Bash shell scripts written in Python."""
    4 import argparse
    5 import re
    6 import sys
    7 import pkg_resources  # part of setuptools
    8 
    9 # correct function style detection is obtained only if following regex are tested in sequence.
   10 # styles are listed as follows:
   11 # 0) function keyword, open/closed parentheses, e.g.      function foo()
   12 # 1) function keyword, NO open/closed parentheses, e.g.   function foo
   13 # 2) NO function keyword, open/closed parentheses, e.g.   foo()
   14 FUNCTION_STYLE_REGEX = [
   15     r'\bfunction\s+(\w*)\s*\(\s*\)\s*',
   16     r'\bfunction\s+(\w*)\s*',
   17     r'\b\s*(\w*)\s*\(\s*\)\s*'
   18 ]
   19 
   20 FUNCTION_STYLE_REPLACEMENT = [
   21     r'function \g<1>() ',
   22     r'function \g<1> ',
   23     r'\g<1>() '
   24 ]
   25 
   26 def main():
   27     """Call the main function."""
   28     Beautify().main()
   29 
   30 
   31 class Beautify:
   32     """Class to handle both module and non-module calls."""
   33 
   34     def __init__(self):
   35         """Set tab as space and it's value to 4."""
   36         self.tab_str = ' '
   37         self.tab_size = 4
   38         self.backup = False
   39         self.check_only = False
   40         self.apply_function_style = None # default is no change based on function style
   41 
   42     def read_file(self, fp):
   43         """Read input file."""
   44         with open(fp) as f:
   45             return f.read()
   46 
   47     def write_file(self, fp, data):
   48         """Write output to a file."""
   49         with open(fp, 'w', newline='\n') as f:
   50             f.write(data)
   51 
   52     def detect_function_style(self, test_record):
   53         """Returns the index for the function declaration style detected in the given string
   54            or None if no function declarations are detected."""
   55         index = 0
   56         # IMPORTANT: apply regex sequentially and stop on the first match:
   57         for regex in FUNCTION_STYLE_REGEX:
   58             if re.search(regex, test_record):
   59                 return index
   60             index+=1
   61         return None
   62 
   63     def change_function_style(self, stripped_record, func_decl_style):
   64         """Converts a function definition syntax from the 'func_decl_style' to the one that has been
   65            set in self.apply_function_style and returns the string with the converted syntax."""
   66         if func_decl_style is None:
   67             return stripped_record
   68         if self.apply_function_style is None:
   69             # user does not want to enforce any specific function style
   70             return stripped_record
   71         regex = FUNCTION_STYLE_REGEX[func_decl_style]
   72         replacement = FUNCTION_STYLE_REPLACEMENT[self.apply_function_style]
   73         changed_record = re.sub(regex, replacement, stripped_record)
   74         return changed_record.strip()
   75 
   76     def get_test_record(self, source_line):
   77         """Takes the given Bash source code line and simplifies it by removing stuff that is not
   78            useful for the purpose of indentation level calculation"""
   79         # first of all, get rid of escaped special characters like single/double quotes
   80         # that may impact later "collapse" attempts
   81         test_record = source_line.replace("\\'", "")
   82         test_record = test_record.replace("\\\"", "")
   83 
   84         # collapse multiple quotes between ' ... '
   85         test_record = re.sub(r'\'.*?\'', '', test_record)
   86         # collapse multiple quotes between " ... "
   87         test_record = re.sub(r'".*?"', '', test_record)
   88         # collapse multiple quotes between ` ... `
   89         test_record = re.sub(r'`.*?`', '', test_record)
   90         # collapse multiple quotes between \` ... ' (weird case)
   91         test_record = re.sub(r'\\`.*?\'', '', test_record)
   92         # strip out any escaped single characters
   93         test_record = re.sub(r'\\.', '', test_record)
   94         # remove '#' comments
   95         test_record = re.sub(r'(\A|\s)(#.*)', '', test_record, 1)
   96         return test_record
   97 
   98     def beautify_string(self, data, path=''):
   99         """Beautify string (file part)."""
  100         tab = 0
  101         case_level = 0
  102         prev_line_had_continue = False
  103         continue_line = False
  104         started_multiline_quoted_string = False
  105         ended_multiline_quoted_string = False
  106         open_brackets = 0
  107         in_here_doc = False
  108         defer_ext_quote = False
  109         in_ext_quote = False
  110         ext_quote_string = ''
  111         here_string = ''
  112         output = []
  113         line = 1
  114         formatter = True
  115         for record in re.split('\n', data):
  116             record = record.rstrip()
  117             stripped_record = record.strip()
  118 
  119             # preserve blank lines
  120             if not stripped_record:
  121                 output.append(stripped_record)
  122                 continue
  123 
  124             # ensure space before ;; terminators in case statements
  125             if case_level:
  126                 stripped_record = re.sub(r'(\S);;', r'\1 ;;', stripped_record)
  127 
  128             test_record = self.get_test_record(stripped_record)
  129 
  130             # detect whether this line ends with line continuation character:
  131             prev_line_had_continue = continue_line
  132             continue_line = True if (re.search(r'\\$', stripped_record)!=None) else False
  133             inside_multiline_quoted_string = prev_line_had_continue and continue_line and started_multiline_quoted_string
  134 
  135             if not continue_line and prev_line_had_continue and started_multiline_quoted_string:
  136                 # remove contents of strings initiated on previous lines and that are ending on this line:
  137                 [test_record, num_subs] = re.subn(r'^[^"]*"', '', test_record)
  138                 ended_multiline_quoted_string = True if num_subs>0 else False
  139             else:
  140                 ended_multiline_quoted_string = False
  141 
  142             if(in_here_doc) or (inside_multiline_quoted_string) or (ended_multiline_quoted_string):  # pass on with no changes
  143                 output.append(record)
  144                 # now test for here-doc termination string
  145                 if(re.search(here_string, test_record) and not
  146                    re.search(r'<<', test_record)):
  147                     in_here_doc = False
  148             else:  # not in here doc or inside multiline-quoted
  149 
  150                 if continue_line:
  151                     if prev_line_had_continue:
  152                         # this line is not STARTING a multiline-quoted string... we may be in the middle
  153                         # of such a multiline string though
  154                         started_multiline_quoted_string = False
  155                     else:
  156                         # remove contents of strings initiated on current line but that continue on next line
  157                         # (in particular we need to ignore brackets they may contain!)
  158                         [test_record, num_subs] = re.subn(r'"[^"]*?\\$', '', test_record)
  159                         started_multiline_quoted_string = True if num_subs>0 else False
  160                 else:
  161                     # this line is not STARTING a multiline-quoted string
  162                     started_multiline_quoted_string = False
  163 
  164                 if(re.search(r'<<-?', test_record)) and not (re.search(r'.*<<<', test_record)):
  165                     here_string = re.sub(
  166                         r'.*<<-?\s*[\'|"]?([_|\w]+)[\'|"]?.*', r'\1',
  167                         stripped_record, 1)
  168                     in_here_doc = (len(here_string) > 0)
  169 
  170                 if(in_ext_quote):
  171                     if(re.search(ext_quote_string, test_record)):
  172                         # provide line after quotes
  173                         test_record = re.sub(
  174                             r'.*%s(.*)' % ext_quote_string, r'\1',
  175                             test_record, 1)
  176                         in_ext_quote = False
  177                 else:  # not in ext quote
  178                     if(re.search(r'(\A|\s)(\'|")', test_record)):
  179                         # apply only after this line has been processed
  180                         defer_ext_quote = True
  181                         ext_quote_string = re.sub(
  182                             r'.*([\'"]).*', r'\1', test_record, 1)
  183                         # provide line before quote
  184                         test_record = re.sub(
  185                             r'(.*)%s.*' % ext_quote_string, r'\1',
  186                             test_record, 1)
  187                 if(in_ext_quote or not formatter):
  188                     # pass on unchanged
  189                     output.append(record)
  190                     if(re.search(r'#\s*@formatter:on', stripped_record)):
  191                         formatter = True
  192                         continue
  193                 else:  # not in ext quote
  194                     if(re.search(r'#\s*@formatter:off', stripped_record)):
  195                         formatter = False
  196                         output.append(record)
  197                         continue
  198 
  199                     # multi-line conditions are often meticulously formatted
  200                     if open_brackets:
  201                         output.append(record)
  202                     else:
  203                         inc = len(re.findall(
  204                             r'(\s|\A|;)(case|then|do)(;|\Z|\s)', test_record))
  205                         inc += len(re.findall(r'(\{|\(|\[)', test_record))
  206                         outc = len(re.findall(
  207                             r'(\s|\A|;)(esac|fi|done|elif)(;|\)|\||\Z|\s)',
  208                             test_record))
  209                         outc += len(re.findall(r'(\}|\)|\])', test_record))
  210                         if(re.search(r'\besac\b', test_record)):
  211                             if(case_level == 0):
  212                                 sys.stderr.write(
  213                                     'File %s: error: "esac" before "case" in '
  214                                     'line %d.\n' % (path, line))
  215                             else:
  216                                 outc += 1
  217                                 case_level -= 1
  218 
  219                         # special handling for bad syntax within case ... esac
  220                         if re.search(r'\bcase\b', test_record):
  221                             inc += 1
  222                             case_level += 1
  223 
  224                         choice_case = 0
  225                         if case_level:
  226                             if(re.search(r'\A[^(]*\)', test_record)):
  227                                 inc += 1
  228                                 choice_case = -1
  229 
  230                         # detect functions
  231                         func_decl_style = self.detect_function_style(test_record)
  232                         if func_decl_style != None:
  233                              stripped_record = self.change_function_style(stripped_record, func_decl_style)
  234 
  235                         # an ad-hoc solution for the "else" or "elif" keyword
  236                         else_case = (0, -1)[re.search(r'^(else|elif)',
  237                                             test_record) is not None]
  238                         net = inc - outc
  239                         tab += min(net, 0)
  240 
  241                         # while 'tab' is preserved across multiple lines, 'extab' is not and is used for
  242                         # some adjustments:
  243                         extab = tab + else_case + choice_case
  244                         if prev_line_had_continue and not open_brackets and not ended_multiline_quoted_string:
  245                             extab+=1
  246                         extab = max(0, extab)
  247                         output.append((self.tab_str * self.tab_size * extab) +
  248                                       stripped_record)
  249                         tab += max(net, 0)
  250                 if(defer_ext_quote):
  251                     in_ext_quote = True
  252                     defer_ext_quote = False
  253 
  254                 # count open brackets for line continuation
  255                 open_brackets += len(re.findall(r'\[', test_record))
  256                 open_brackets -= len(re.findall(r'\]', test_record))
  257             line += 1
  258         error = (tab != 0)
  259         if(error):
  260             sys.stderr.write(
  261                 'File %s: error: indent/outdent mismatch: %d.\n' % (path, tab))
  262         return '\n'.join(output), error
  263 
  264     def beautify_file(self, path):
  265         """Beautify bash script file."""
  266         error = False
  267         if(path == '-'):
  268             data = sys.stdin.read()
  269             result, error = self.beautify_string(data, '(stdin)')
  270             sys.stdout.write(result)
  271         else:  # named file
  272             data = self.read_file(path)
  273             result, error = self.beautify_string(data, path)
  274             if(data != result):
  275                 if(self.check_only):
  276                     if not error:
  277                         # we want to return 0 (success) only if the given file is already
  278                         # well formatted:
  279                         error = (result != data)
  280                 else:
  281                     if(self.backup):
  282                         self.write_file(path+'.bak', data)
  283                     self.write_file(path, result)
  284         return error
  285 
  286     def print_help(self, parser):
  287         parser.print_help()
  288         sys.stdout.write("\nBash function styles that can be specified via --force-function-style are:\n")
  289         sys.stdout.write("  fnpar: function keyword, open/closed parentheses, e.g.      function foo()\n")
  290         sys.stdout.write("  fnonly: function keyword, no open/closed parentheses, e.g.  function foo\n")
  291         sys.stdout.write("  paronly: no function keyword, open/closed parentheses, e.g. foo()\n")
  292         sys.stdout.write("\n")
  293 
  294     def parse_function_style(self, style_name):
  295         # map the user-provided function style to our range 0-2
  296         if style_name == "fnpar":
  297             return 0
  298         elif style_name == "fnonly":
  299             return 1
  300         elif style_name == "paronly":
  301             return 2
  302         return None
  303 
  304     def get_version(self):
  305         try:
  306             return pkg_resources.require("beautysh")[0].version
  307         except pkg_resources.DistributionNotFound:
  308             return "Not Available"
  309 
  310     def main(self):
  311         """Main beautifying function."""
  312         error = False
  313         parser = argparse.ArgumentParser(
  314             description="A Bash beautifier for the masses, version {}".format(self.get_version()), add_help=False)
  315         parser.add_argument('--indent-size', '-i', nargs=1, type=int, default=4,
  316                             help="Sets the number of spaces to be used in "
  317                                  "indentation.")
  318         parser.add_argument('--backup', '-b', action='store_true',
  319                             help="Beautysh will create a backup file in the "
  320                                  "same path as the original.")
  321         parser.add_argument('--check', '-c', action='store_true',
  322                             help="Beautysh will just check the files without doing "
  323                                  "any in-place beautify.")
  324         parser.add_argument('--tab', '-t', action='store_true',
  325                             help="Sets indentation to tabs instead of spaces.")
  326         parser.add_argument('--force-function-style', '-s', nargs=1,
  327                             help="Force a specific Bash function formatting. See below for more info.")
  328         parser.add_argument('--version', '-v', action='store_true',
  329                             help="Prints the version and exits.")
  330         parser.add_argument('--help', '-h', action='store_true',
  331                             help="Print this help message.")
  332         parser.add_argument('files', metavar='FILE', nargs='*',
  333                             help="Files to be beautified. This is mandatory. "
  334                             "If - is provided as filename, then beautysh reads "
  335                             "from stdin and writes on stdout.")
  336         args = parser.parse_args()
  337         if (len(sys.argv) < 2) or args.help:
  338             self.print_help(parser)
  339             exit()
  340         if args.version:
  341             sys.stdout.write("%s\n" % self.get_version())
  342             exit()
  343         if(type(args.indent_size) is list):
  344             args.indent_size = args.indent_size[0]
  345         if not args.files:
  346             sys.stdout.write("Please provide at least one input file\n")
  347             exit()
  348         self.tab_size = args.indent_size
  349         self.backup = args.backup
  350         self.check_only = args.check
  351         if (args.tab):
  352             self.tab_size = 1
  353             self.tab_str = '\t'
  354         if (type(args.force_function_style) is list):
  355             provided_style = self.parse_function_style(args.force_function_style[0])
  356             if provided_style is None:
  357                 sys.stdout.write("Invalid value for the function style. See --help for details.\n")
  358                 exit()
  359             self.apply_function_style = provided_style
  360         for path in args.files:
  361             error |= self.beautify_file(path)
  362         sys.exit((0, 1)[error])
  363 
  364 
  365 # if not called as a module
  366 if(__name__ == '__main__'):
  367     Beautify().main()