├── .gitignore ├── README.md └── beautify_bash.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | # Vim swap files 30 | *.swp 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | beautify_bash 2 | ============= 3 | 4 | Code formatter / beautifier for bash written in python by 5 | Paul Lutus (a remake of previous version in Ruby). 6 | 7 | For further details please see the following blog record 8 | http://arachnoid.com/python/beautify_bash_program.html 9 | 10 | 11 | -------------------------------------------------------------------------------- /beautify_bash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | #************************************************************************** 5 | # Copyright (C) 2011, Paul Lutus * 6 | # * 7 | # This program is free software; you can redistribute it and/or modify * 8 | # it under the terms of the GNU General Public License as published by * 9 | # the Free Software Foundation; either version 2 of the License, or * 10 | # (at your option) any later version. * 11 | # * 12 | # This program is distributed in the hope that it will be useful, * 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of * 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 15 | # GNU General Public License for more details. * 16 | # * 17 | # You should have received a copy of the GNU General Public License * 18 | # along with this program; if not, write to the * 19 | # Free Software Foundation, Inc., * 20 | # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * 21 | #************************************************************************** 22 | 23 | import re 24 | import sys 25 | 26 | PVERSION = '1.0' 27 | 28 | 29 | class BeautifyBash: 30 | 31 | def __init__(self): 32 | self.tab_str = ' ' 33 | self.tab_size = 2 34 | 35 | def read_file(self, fp): 36 | with open(fp) as f: 37 | return f.read() 38 | 39 | def write_file(self, fp, data): 40 | with open(fp, 'w') as f: 41 | f.write(data) 42 | 43 | def beautify_string(self, data, path=''): 44 | tab = 0 45 | case_stack = [] 46 | in_here_doc = False 47 | defer_ext_quote = False 48 | in_ext_quote = False 49 | ext_quote_string = '' 50 | here_string = '' 51 | output = [] 52 | line = 1 53 | for record in re.split('\n', data): 54 | record = record.rstrip() 55 | stripped_record = record.strip() 56 | 57 | # collapse multiple quotes between ' ... ' 58 | test_record = re.sub(r'\'.*?\'', '', stripped_record) 59 | # collapse multiple quotes between " ... " 60 | test_record = re.sub(r'".*?"', '', test_record) 61 | # collapse multiple quotes between ` ... ` 62 | test_record = re.sub(r'`.*?`', '', test_record) 63 | # collapse multiple quotes between \` ... ' (weird case) 64 | test_record = re.sub(r'\\`.*?\'', '', test_record) 65 | # strip out any escaped single characters 66 | test_record = re.sub(r'\\.', '', test_record) 67 | # remove '#' comments 68 | test_record = re.sub(r'(\A|\s)(#.*)', '', test_record, 1) 69 | if(not in_here_doc): 70 | if(re.search('<<-?', test_record)): 71 | here_string = re.sub( 72 | '.*<<-?\s*[\'|"]?([_|\w]+)[\'|"]?.*', '\\1', stripped_record, 1) 73 | in_here_doc = (len(here_string) > 0) 74 | if(in_here_doc): # pass on with no changes 75 | output.append(record) 76 | # now test for here-doc termination string 77 | if(re.search(here_string, test_record) and not re.search('<<', test_record)): 78 | in_here_doc = False 79 | else: # not in here doc 80 | if(in_ext_quote): 81 | if(re.search(ext_quote_string, test_record)): 82 | # provide line after quotes 83 | test_record = re.sub( 84 | '.*%s(.*)' % ext_quote_string, '\\1', test_record, 1) 85 | in_ext_quote = False 86 | else: # not in ext quote 87 | if(re.search(r'(\A|\s)(\'|")', test_record)): 88 | # apply only after this line has been processed 89 | defer_ext_quote = True 90 | ext_quote_string = re.sub( 91 | '.*([\'"]).*', '\\1', test_record, 1) 92 | # provide line before quote 93 | test_record = re.sub( 94 | '(.*)%s.*' % ext_quote_string, '\\1', test_record, 1) 95 | if(in_ext_quote): 96 | # pass on unchanged 97 | output.append(record) 98 | else: # not in ext quote 99 | inc = len(re.findall( 100 | '(\s|\A|;)(case|then|do)(;|\Z|\s)', test_record)) 101 | inc += len(re.findall('(\{|\(|\[)', test_record)) 102 | outc = len(re.findall( 103 | '(\s|\A|;)(esac|fi|done|elif)(;|\)|\||\Z|\s)', test_record)) 104 | outc += len(re.findall('(\}|\)|\])', test_record)) 105 | if(re.search(r'\besac\b', test_record)): 106 | if(len(case_stack) == 0): 107 | sys.stderr.write( 108 | 'File %s: error: "esac" before "case" in line %d.\n' % ( 109 | path, line) 110 | ) 111 | else: 112 | outc += case_stack.pop() 113 | # sepcial handling for bad syntax within case ... esac 114 | if(len(case_stack) > 0): 115 | if(re.search('\A[^(]*\)', test_record)): 116 | # avoid overcount 117 | outc -= 2 118 | case_stack[-1] += 1 119 | if(re.search(';;', test_record)): 120 | outc += 1 121 | case_stack[-1] -= 1 122 | # an ad-hoc solution for the "else" keyword 123 | else_case = ( 124 | 0, -1)[re.search('^(else)', test_record) != None] 125 | net = inc - outc 126 | tab += min(net, 0) 127 | extab = tab + else_case 128 | extab = max(0, extab) 129 | output.append( 130 | (self.tab_str * self.tab_size * extab) + stripped_record) 131 | tab += max(net, 0) 132 | if(defer_ext_quote): 133 | in_ext_quote = True 134 | defer_ext_quote = False 135 | if(re.search(r'\bcase\b', test_record)): 136 | case_stack.append(0) 137 | line += 1 138 | error = (tab != 0) 139 | if(error): 140 | sys.stderr.write( 141 | 'File %s: error: indent/outdent mismatch: %d.\n' % (path, tab)) 142 | return '\n'.join(output), error 143 | 144 | def beautify_file(self, path): 145 | error = False 146 | if(path == '-'): 147 | data = sys.stdin.read() 148 | result, error = self.beautify_string(data, '(stdin)') 149 | sys.stdout.write(result) 150 | else: # named file 151 | data = self.read_file(path) 152 | result, error = self.beautify_string(data, path) 153 | if(data != result): 154 | # make a backup copy 155 | self.write_file(path + '~', data) 156 | self.write_file(path, result) 157 | return error 158 | 159 | def main(self): 160 | error = False 161 | sys.argv.pop(0) 162 | if(len(sys.argv) < 1): 163 | sys.stderr.write( 164 | 'usage: shell script filenames or \"-\" for stdin.\n') 165 | else: 166 | for path in sys.argv: 167 | error |= self.beautify_file(path) 168 | sys.exit((0, 1)[error]) 169 | 170 | # if not called as a module 171 | if(__name__ == '__main__'): 172 | BeautifyBash().main() 173 | --------------------------------------------------------------------------------