blob: 2e700e2ab856942067b04ab13be69d5031a50a3b [file] [log] [blame]
Steve Kondik95027ea2017-06-14 17:22:58 -07001#!/usr/bin/env python3
2#
3# update-headers - Updates the header comment in source files
4# Copyright (C) 2013 Lorenzo Villani
5# Updated for Python3 by Steve Kondik (part of UChroma)
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 3 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, see <http://www.gnu.org/licenses/>.
19#
20
21
22from argparse import ArgumentParser
23from functools import partial
24from os import chmod, stat, walk
25from os.path import isdir, isfile, join, splitext
26from shutil import copyfile
27from tempfile import NamedTemporaryFile
28
29
30# File extension -> comment string
31COMMENT_STRING = {
32 ".c": "//",
33 ".cc": "//",
34 ".cpp": "//",
35 ".el": ";;;;",
36 ".h": "//",
37 ".hh": "//",
38 ".hpp": "//",
39 ".hs": "--",
40 ".java": "//",
41 ".js": "//",
42 ".li": ";;;;",
43 ".m": "//",
44 ".mm": "//",
45 ".py": "#",
46 ".swift": "//",
47 ".yml": "#",
48}
49
50
51def main():
52 # Parse command line
53 arg_parser = ArgumentParser()
54 arg_parser.add_argument("-c", "--comment-string", default="#")
55 arg_parser.add_argument("header", nargs=1)
56 arg_parser.add_argument("path", nargs="+")
57
58 args = arg_parser.parse_args()
59
60 # Input stream for the header boilerplate
61 header = open(args.header[0], "r")
62
63 # Process files and directories
64 for path in args.path:
65 if isfile(path):
66 update_file(path, header, args.comment_string)
67 elif isdir(path):
68 update_directory(path, header, args.comment_string)
69
70
71def update_file(path, header, comment_string):
72 """
73 Updates the header boilerplate for a single file.
74
75 :param path: A file path.
76 """
77 original_stat = stat(path)
78 input_stream = open(path, "r")
79 tempfile = NamedTemporaryFile(mode="w")
80
81 update_header(input_stream, header, tempfile, comment_string)
82 tempfile.flush()
83
84 copyfile(tempfile.name, path)
85 chmod(path, original_stat.st_mode)
86
87
88def update_directory(path, header, comment_string):
89 """
90 Recursively updates the header boilerplate for all recognized files below
91 the specified directory.
92
93 :param path: A directory path.
94 """
95 def is_recognized(entry):
96 return isfile(entry) and splitext(entry)[1] in COMMENT_STRING
97
98 def visit(_, directory, files):
99 entries = list(map(partial(join, directory), files))
100 applicable = list(filter(is_recognized, entries))
101
102 for path in applicable:
103 ext = splitext(path)[1]
104
105 update_file(path, header, COMMENT_STRING[ext])
106
107 for dirpath, dirnames, filenames in walk(path):
108 visit(None, dirpath, dirnames + filenames)
109
110
111def update_header(input_stream, new_header, output_stream, comment_string="#"):
112 """
113 Replaces or inserts a new header in a file.
114
115 @param input_stream: The input file with the old (or missing) header.
116 @type input_stream: file
117
118 @param new_header: Input stream of the new header.
119 @type new_header: file
120
121 @param output_stream: Output stream for the new file with the header
122 replaced.
123 @type output_stream: file
124
125 @param comment_string: The string used to start a comment which spans until
126 the end of the line.
127 @type comment_string: str
128 """
129 class State:
130 Start, FoundHeaderStart, Done = list(range(3))
131
132 state = State.Start
133
134 for line in input_stream:
135 if state == State.Start:
136 # At the beginning of the file.
137 if line.startswith("#!"):
138 # Ignore the shebang and copy this line as-is. No state change.
139 output_stream.write(line)
140 elif line.startswith(comment_string):
141 # Start -> FoundHeaderStart.
142 state = State.FoundHeaderStart
143 else:
144 # Inject header then state transition: Start -> Done.
145 inject_header(new_header, output_stream, comment_string)
146 output_stream.write(line)
147
148 state = State.Done
149 elif state == State.FoundHeaderStart:
150 # We have found the beginning of the header comment, now we have to
151 # look for the first non comment line, then inject our header,
152 # then transition from FoundHeaderStart -> Done.
153 if not line.startswith(comment_string):
154 inject_header(new_header, output_stream, comment_string)
155 output_stream.write(line)
156 state = State.Done
157 elif state == State.Done:
158 # Copy input to output verbatim
159 output_stream.write(line)
160 else:
161 if state == State.Start or state == State.FoundHeaderStart:
162 # Input is empty, inject the header and be done with it
163 inject_header(new_header, output_stream, comment_string)
164
165 state = State.Done
166
167 output_stream.flush()
168
169
170def inject_header(header_stream, output_stream, comment_string):
171 """
172 Writes the header onto the output stream.
173
174 @param header_stream: Input stream for the header. A seek to the beginning
175 of the file is performed with every call.
176 @type header_stream: file
177
178 @param output_stream: The stream where the header will be written.
179 @type output_stream: file
180
181 @param comment_string: The string used to start a comment which spans until
182 the end of the line.
183 @type comment_string: str
184 """
185 header_stream.seek(0)
186
187 for line in header_stream:
188 if line.strip() == "":
189 output_stream.write(comment_string + "\n")
190 else:
191 output_stream.write(comment_string + " " + line.rstrip(' '))
192
193if __name__ == "__main__":
194 main()