3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2013, by the GROMACS development team, led by
6 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
7 # and including many others, as listed in the AUTHORS file in the
8 # top-level source directory and at http://www.gromacs.org.
10 # GROMACS is free software; you can redistribute it and/or
11 # modify it under the terms of the GNU Lesser General Public License
12 # as published by the Free Software Foundation; either version 2.1
13 # of the License, or (at your option) any later version.
15 # GROMACS is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 # Lesser General Public License for more details.
20 # You should have received a copy of the GNU Lesser General Public
21 # License along with GROMACS; if not, see
22 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
23 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25 # If you want to redistribute modifications to GROMACS, please
26 # consider that scientific software is very special. Version
27 # control is crucial - bugs must be traceable. We will be happy to
28 # consider code for inclusion in the official distribution, but
29 # derived work must not be called official GROMACS. Details are found
30 # in the README & COPYING files - if they are missing, get the
31 # official version at http://www.gromacs.org.
33 # To help us fund GROMACS development, we humbly ask that you cite
34 # the research papers on the package. Check out http://www.gromacs.org.
41 from optparse import OptionParser
43 class CopyrightState(object):
45 """Information about an existing (or non-existing) copyright header."""
47 def __init__(self, has_copyright, is_correct, is_newstyle, years, other_copyrights):
48 self.has_copyright = has_copyright
49 self.is_correct = is_correct
50 self.is_newstyle = is_newstyle
52 self.other_copyrights = other_copyrights
54 class CopyrightChecker(object):
56 """Logic for analyzing existing copyright headers and generating new ones."""
58 _header = ["", "This file is part of the GROMACS molecular simulation package.", ""]
59 _copyright = "Copyright (c) {0}, by the GROMACS development team, led by"
61 Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
62 and including many others, as listed in the AUTHORS file in the
63 top-level source directory and at http://www.gromacs.org.
65 GROMACS is free software; you can redistribute it and/or
66 modify it under the terms of the GNU Lesser General Public License
67 as published by the Free Software Foundation; either version 2.1
68 of the License, or (at your option) any later version.
70 GROMACS is distributed in the hope that it will be useful,
71 but WITHOUT ANY WARRANTY; without even the implied warranty of
72 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
73 Lesser General Public License for more details.
75 You should have received a copy of the GNU Lesser General Public
76 License along with GROMACS; if not, see
77 http://www.gnu.org/licenses, or write to the Free Software Foundation,
78 Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
80 If you want to redistribute modifications to GROMACS, please
81 consider that scientific software is very special. Version
82 control is crucial - bugs must be traceable. We will be happy to
83 consider code for inclusion in the official distribution, but
84 derived work must not be called official GROMACS. Details are found
85 in the README & COPYING files - if they are missing, get the
86 official version at http://www.gromacs.org.
88 To help us fund GROMACS development, we humbly ask that you cite
89 the research papers on the package. Check out http://www.gromacs.org.
90 """.strip().splitlines()
92 def check_copyright(self, comment_block):
93 """Analyze existing copyright header for correctness and extract information."""
94 copyright_re = r'Copyright \(c\) (([0-9]{4}[,-])*[0-9]{4}),? by the GROMACS development team'
101 other_copyrights = []
102 for line in comment_block:
103 if 'Copyright' in line:
105 match = re.match(copyright_re, line)
107 existing_years = match.group(1)
108 new_line = self._copyright.format(existing_years)
112 other_copyrights.append(line)
113 if next_header_line != -1 or next_footer_line != 0:
116 if line.startswith('Written by the Gromacs development team'):
118 if next_header_line >= 0:
119 if line == self._header[next_header_line]:
120 next_header_line += 1
121 if next_header_line >= len(self._header):
122 next_header_line = -1
126 elif next_footer_line >= 0:
127 if line == self._footer[next_footer_line]:
128 next_footer_line += 1
129 if next_footer_line >= len(self._footer):
130 next_footer_line = -1
135 if next_header_line != -1 or next_footer_line != -1:
138 return CopyrightState(has_copyright, is_correct, is_newstyle, existing_years, other_copyrights)
140 def process_copyright(self, state, options, current_years, reporter):
141 """Determine whether a copyrigth header needs to be updated and report issues."""
145 if options.replace_years:
146 if state.years != current_years:
148 reporter.report('copyright years replaced')
149 new_years = current_years
151 new_years = state.years
152 if not new_years.endswith(current_years):
153 if options.update_year:
155 new_years += ',' + current_years
156 if options.check or not need_update:
157 reporter.report('copyright year outdated')
159 reporter.report('copyright year added')
161 new_years = current_years
163 if not state.has_copyright:
164 if options.add_missing:
166 if options.check or not need_update:
167 reporter.report('copyright header missing')
168 elif options.add_missing:
169 reporter.report('copyright header added')
171 if not state.is_newstyle:
172 if options.replace_header:
174 if options.check or not need_update:
175 reporter.report('copyright header incorrect')
177 reporter.report('copyright header replaced')
178 elif not state.is_correct:
179 if options.update_header:
181 if options.check or not need_update:
182 reporter.report('copyright header outdated')
184 reporter.report('copyright header updated')
186 return need_update, new_years
188 def get_copyright_text(self, years, other_copyrights):
189 """Construct a new copyright header."""
191 output.extend(self._header)
193 for line in other_copyrights:
194 outline = line.rstrip()
195 if outline.endswith(','):
196 outline = outline[:-1]
197 if not outline.endswith('.'):
199 output.append(outline)
200 output.append(self._copyright.format(years))
201 output.extend(self._footer)
204 class Reporter(object):
206 """Wrapper for reporting issues in a file."""
208 def __init__(self, reportfile, filename):
209 self._reportfile = reportfile
210 self._filename = filename
212 def report(self, text):
213 self._reportfile.write(self._filename + ': ' + text + '\n');
215 class CommentHandlerC(object):
217 """Handler for extracting and creating C-style comments."""
219 def extract_first_comment_block(self, content_lines):
220 if not content_lines or not content_lines[0].startswith('/*'):
222 comment_block = [content_lines[0][2:].strip()]
224 while line_index < len(content_lines):
225 line = content_lines[line_index]
226 if '*/' in content_lines[line_index]:
228 comment_block.append(line.lstrip('* ').rstrip())
230 return (comment_block, line_index + 1)
232 def create_comment_block(self, lines):
234 output.append(('/* ' + lines[0]).rstrip())
235 output.extend([(' * ' + x).rstrip() for x in lines[1:]])
239 class CommentHandlerSh(object):
241 """Handler for extracting and creating sh-style comments."""
243 def extract_first_comment_block(self, content_lines):
244 if not content_lines or not content_lines[0].startswith('#'):
248 while line_index < len(content_lines):
249 line = content_lines[line_index]
250 if not line.startswith('#'):
252 comment_block.append(line.lstrip('# ').rstrip())
254 if line == '# the research papers on the package. Check out http://www.gromacs.org.':
256 while line_index < len(content_lines):
257 line = content_lines[line_index].rstrip()
258 if len(line) > 0 and line != '#':
261 return (comment_block, line_index)
263 def create_comment_block(self, lines):
265 output.extend([('# ' + x).rstrip() for x in lines])
269 comment_handlers = {'c': CommentHandlerC(), 'sh': CommentHandlerSh()}
271 def select_comment_handler(override, filename):
272 """Select comment handler for a file based on file name and input options."""
274 if not filetype and filename != '-':
275 basename = os.path.basename(filename)
276 root, ext = os.path.splitext(basename)
277 if ext == '.cmakein':
278 dummy, ext2 = os.path.splitext(root)
281 if ext in ('.c', '.cpp', '.h', '.y', '.l', '.pre'):
283 elif basename in ('CMakeLists.txt', 'GMXRC', 'git-pre-commit') or \
284 ext in ('.cmake', '.cmakein', '.py', '.sh', '.bash', '.csh', '.zsh'):
286 if filetype in comment_handlers:
287 return comment_handlers[filetype]
289 sys.stderr.write("Unsupported input format: {0}\n".format(filetype))
290 elif filename != '-':
291 sys.stderr.write("Unsupported input format: {0}\n".format(filename))
293 sys.stderr.write("No file name or file type provided.\n")
296 def create_copyright_header(years, other_copyrights=None, language='c'):
297 if language not in comment_handlers:
298 sys.strerr.write("Unsupported language: {0}\n".format(language))
300 copyright_checker = CopyrightChecker()
301 comment_handler = comment_handlers[language]
302 copyright_lines = copyright_checker.get_copyright_text(years, other_copyrights)
303 comment_lines = comment_handler.create_comment_block(copyright_lines)
304 return '\n'.join(comment_lines) + '\n'
306 def process_options():
307 """Process input options."""
308 parser = OptionParser()
309 parser.add_option('-l', '--lang',
310 help='Comment type to use (c or sh)')
311 parser.add_option('-y', '--years',
312 help='Comma-separated list of years')
313 parser.add_option('-F', '--files',
314 help='File to read list of files from')
315 parser.add_option('--check', action='store_true',
316 help='Do not modify the files, only check the copyright (default action). ' +
317 'If specified together with --update, do the modifications ' +
318 'but produce output as if only --check was provided.')
319 parser.add_option('--update-year', action='store_true',
320 help='Update the copyright year if outdated')
321 parser.add_option('--replace-years', action='store_true',
322 help='Replace the copyright years with those given with --years')
323 parser.add_option('--update-header', action='store_true',
324 help='Update the copyright header if outdated')
325 parser.add_option('--replace-header', action='store_true',
326 help='Replace any copyright header with the current one')
327 parser.add_option('--add-missing', action='store_true',
328 help='Add missing copyright headers')
329 options, args = parser.parse_args()
333 with open(options.files, 'r') as filelist:
334 filenames = [x.strip() for x in filelist.read().splitlines()]
338 # Default is --check if nothing provided.
339 if not options.check and not options.update_year and \
340 not options.update_header and not options.replace_header and \
341 not options.add_missing:
344 return options, filenames
347 """Do processing as a stand-alone script."""
348 options, filenames = process_options()
349 years = options.years
351 years = str(datetime.date.today().year)
352 if years.endswith(','):
355 checker = CopyrightChecker()
357 # Process each input file in turn.
358 for filename in filenames:
359 comment_handler = select_comment_handler(options.lang, filename)
361 # Read the input file. We are doing an in-place operation, so can't
362 # operate in pass-through mode.
364 contents = sys.stdin.read().splitlines()
365 reporter = Reporter(sys.stderr, '<stdin>')
367 with open(filename, 'r') as inputfile:
368 contents = inputfile.read().splitlines()
369 reporter = Reporter(sys.stdout, filename)
372 # Keep lines that must be at the beginning of the file and skip them in
374 if contents and (contents[0].startswith('#!/') or \
375 contents[0].startswith('%code requires') or \
376 contents[0].startswith('/* #if')):
377 output.append(contents[0])
378 contents = contents[1:]
379 # Remove and skip empty lines at the beginning.
380 while contents and len(contents[0]) == 0:
381 contents = contents[1:]
383 # Analyze the first comment block in the file.
384 comment_block, line_count = comment_handler.extract_first_comment_block(contents)
385 state = checker.check_copyright(comment_block)
386 need_update, file_years = checker.process_copyright(state, options, years, reporter)
389 # Remove the original comment if it was a copyright comment.
390 if state.has_copyright:
391 contents = contents[line_count:]
392 new_block = checker.get_copyright_text(file_years, state.other_copyrights)
393 output.extend(comment_handler.create_comment_block(new_block))
395 # Write the output file if required.
396 if need_update or filename == '-':
397 # Append the rest of the input file as it was.
398 output.extend(contents)
399 output = '\n'.join(output) + '\n'
401 sys.stdout.write(output)
403 with open(filename, 'w') as outputfile:
404 outputfile.write(output)
406 if __name__ == "__main__":