Browse Source

Script for tidying bash scripts

Bob Mottram 8 years ago
parent
commit
ac44c0695b
1 changed files with 159 additions and 0 deletions
  1. 159
    0
      tidy

+ 159
- 0
tidy View File

@@ -0,0 +1,159 @@
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, sys
24
+
25
+PVERSION = '1.0'
26
+
27
+class BeautifyBash:
28
+
29
+  def __init__(self):
30
+    self.tab_str = ' '
31
+    self.tab_size = 4
32
+
33
+  def read_file(self,fp):
34
+    with open(fp) as f:
35
+      return f.read()
36
+
37
+  def write_file(self,fp,data):
38
+    with open(fp,'w') as f:
39
+      f.write(data)
40
+
41
+  def beautify_string(self,data,path = ''):
42
+    tab = 0
43
+    case_stack = []
44
+    in_here_doc = False
45
+    defer_ext_quote = False
46
+    in_ext_quote = False
47
+    ext_quote_string = ''
48
+    here_string = ''
49
+    output = []
50
+    line = 1
51
+    for record in re.split('\n',data):
52
+      record = record.rstrip()
53
+      stripped_record = record.strip()
54
+      
55
+      # collapse multiple quotes between ' ... '
56
+      test_record = re.sub(r'\'.*?\'','',stripped_record)
57
+      # collapse multiple quotes between " ... "
58
+      test_record = re.sub(r'".*?"','',test_record)
59
+      # collapse multiple quotes between ` ... `
60
+      test_record = re.sub(r'`.*?`','',test_record)
61
+      # collapse multiple quotes between \` ... ' (weird case)
62
+      test_record = re.sub(r'\\`.*?\'','',test_record)
63
+      # strip out any escaped single characters
64
+      test_record = re.sub(r'\\.','',test_record)
65
+      # remove '#' comments
66
+      test_record = re.sub(r'(\A|\s)(#.*)','',test_record,1)
67
+      if(not in_here_doc):
68
+        if(re.search('<<-?',test_record)):
69
+          here_string = re.sub('.*<<-?\s*[\'|"]?([_|\w]+)[\'|"]?.*','\\1',stripped_record,1)
70
+          in_here_doc = (len(here_string) > 0)
71
+      if(in_here_doc): # pass on with no changes
72
+        output.append(record)
73
+        # now test for here-doc termination string
74
+        if(re.search(here_string,test_record) and not re.search('<<',test_record)):
75
+          in_here_doc = False
76
+      else: # not in here doc
77
+        if(in_ext_quote):
78
+          if(re.search(ext_quote_string,test_record)):
79
+            # provide line after quotes
80
+            test_record = re.sub('.*%s(.*)' % ext_quote_string,'\\1',test_record,1)
81
+            in_ext_quote = False
82
+        else: # not in ext quote
83
+          if(re.search(r'(\A|\s)(\'|")',test_record)):
84
+            # apply only after this line has been processed
85
+            defer_ext_quote = True
86
+            ext_quote_string = re.sub('.*([\'"]).*','\\1',test_record,1)
87
+            # provide line before quote
88
+            test_record = re.sub('(.*)%s.*' % ext_quote_string,'\\1',test_record,1)
89
+        if(in_ext_quote):
90
+          # pass on unchanged
91
+          output.append(record)
92
+        else: # not in ext quote
93
+          inc = len(re.findall('(\s|\A|;)(case|then|do)(;|\Z|\s)',test_record))
94
+          inc += len(re.findall('(\{|\(|\[)',test_record))
95
+          outc = len(re.findall('(\s|\A|;)(esac|fi|done|elif)(;|\)|\||\Z|\s)',test_record))
96
+          outc += len(re.findall('(\}|\)|\])',test_record))
97
+          if(re.search(r'\besac\b',test_record)):
98
+            if(len(case_stack) == 0):
99
+              sys.stderr.write(
100
+                'File %s: error: "esac" before "case" in line %d.\n' % (path,line)
101
+              )
102
+            else:
103
+              outc += case_stack.pop()
104
+          # sepcial handling for bad syntax within case ... esac
105
+          if(len(case_stack) > 0):
106
+            if(re.search('\A[^(]*\)',test_record)):
107
+              # avoid overcount
108
+              outc -= 2
109
+              case_stack[-1] += 1
110
+            if(re.search(';;',test_record)):
111
+              outc += 1
112
+              case_stack[-1] -= 1
113
+          # an ad-hoc solution for the "else" keyword
114
+          else_case = (0,-1)[re.search('^(else)',test_record) != None]
115
+          net = inc - outc
116
+          tab += min(net,0)
117
+          extab = tab + else_case
118
+          extab = max(0,extab)
119
+          output.append((self.tab_str * self.tab_size * extab) + stripped_record)
120
+          tab += max(net,0)
121
+        if(defer_ext_quote):
122
+          in_ext_quote = True
123
+          defer_ext_quote = False
124
+        if(re.search(r'\bcase\b',test_record)):
125
+          case_stack.append(0)
126
+      line += 1
127
+    error = (tab != 0)
128
+    if(error):
129
+      sys.stderr.write('File %s: error: indent/outdent mismatch: %d.\n' % (path,tab))
130
+    return '\n'.join(output), error
131
+
132
+  def beautify_file(self,path):
133
+    error = False
134
+    if(path == '-'):
135
+      data = sys.stdin.read()
136
+      result,error = self.beautify_string(data,'(stdin)')
137
+      sys.stdout.write(result)
138
+    else: # named file
139
+      data = self.read_file(path)
140
+      result,error = self.beautify_string(data,path)
141
+      if(data != result):
142
+        # make a backup copy
143
+        self.write_file(path + '~',data)
144
+        self.write_file(path,result)
145
+    return error
146
+
147
+  def main(self):
148
+    error = False
149
+    sys.argv.pop(0)
150
+    if(len(sys.argv) < 1):
151
+      sys.stderr.write('usage: shell script filenames or \"-\" for stdin.\n')
152
+    else:
153
+      for path in sys.argv:
154
+        error |= self.beautify_file(path)
155
+    sys.exit((0,1)[error])
156
+
157
+# if not called as a module
158
+if(__name__ == '__main__'):
159
+  BeautifyBash().main()