| #!/usr/bin/env python3 |
| # |
| # update-headers - Updates the header comment in source files |
| # Copyright (C) 2013 Lorenzo Villani |
| # Updated for Python3 by Steve Kondik (part of UChroma) |
| # |
| # This program is free software: you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation, either version 3 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| # |
| |
| |
| from argparse import ArgumentParser |
| from functools import partial |
| from os import chmod, stat, walk |
| from os.path import isdir, isfile, join, splitext |
| from shutil import copyfile |
| from tempfile import NamedTemporaryFile |
| |
| |
| # File extension -> comment string |
| COMMENT_STRING = { |
| ".c": "//", |
| ".cc": "//", |
| ".cpp": "//", |
| ".el": ";;;;", |
| ".h": "//", |
| ".hh": "//", |
| ".hpp": "//", |
| ".hs": "--", |
| ".java": "//", |
| ".js": "//", |
| ".li": ";;;;", |
| ".m": "//", |
| ".mm": "//", |
| ".py": "#", |
| ".swift": "//", |
| ".yml": "#", |
| } |
| |
| |
| def main(): |
| # Parse command line |
| arg_parser = ArgumentParser() |
| arg_parser.add_argument("-c", "--comment-string", default="#") |
| arg_parser.add_argument("header", nargs=1) |
| arg_parser.add_argument("path", nargs="+") |
| |
| args = arg_parser.parse_args() |
| |
| # Input stream for the header boilerplate |
| header = open(args.header[0], "r") |
| |
| # Process files and directories |
| for path in args.path: |
| if isfile(path): |
| update_file(path, header, args.comment_string) |
| elif isdir(path): |
| update_directory(path, header, args.comment_string) |
| |
| |
| def update_file(path, header, comment_string): |
| """ |
| Updates the header boilerplate for a single file. |
| |
| :param path: A file path. |
| """ |
| original_stat = stat(path) |
| input_stream = open(path, "r") |
| tempfile = NamedTemporaryFile(mode="w") |
| |
| update_header(input_stream, header, tempfile, comment_string) |
| tempfile.flush() |
| |
| copyfile(tempfile.name, path) |
| chmod(path, original_stat.st_mode) |
| |
| |
| def update_directory(path, header, comment_string): |
| """ |
| Recursively updates the header boilerplate for all recognized files below |
| the specified directory. |
| |
| :param path: A directory path. |
| """ |
| def is_recognized(entry): |
| return isfile(entry) and splitext(entry)[1] in COMMENT_STRING |
| |
| def visit(_, directory, files): |
| entries = list(map(partial(join, directory), files)) |
| applicable = list(filter(is_recognized, entries)) |
| |
| for path in applicable: |
| ext = splitext(path)[1] |
| |
| update_file(path, header, COMMENT_STRING[ext]) |
| |
| for dirpath, dirnames, filenames in walk(path): |
| visit(None, dirpath, dirnames + filenames) |
| |
| |
| def update_header(input_stream, new_header, output_stream, comment_string="#"): |
| """ |
| Replaces or inserts a new header in a file. |
| |
| @param input_stream: The input file with the old (or missing) header. |
| @type input_stream: file |
| |
| @param new_header: Input stream of the new header. |
| @type new_header: file |
| |
| @param output_stream: Output stream for the new file with the header |
| replaced. |
| @type output_stream: file |
| |
| @param comment_string: The string used to start a comment which spans until |
| the end of the line. |
| @type comment_string: str |
| """ |
| class State: |
| Start, FoundHeaderStart, Done = list(range(3)) |
| |
| state = State.Start |
| |
| for line in input_stream: |
| if state == State.Start: |
| # At the beginning of the file. |
| if line.startswith("#!"): |
| # Ignore the shebang and copy this line as-is. No state change. |
| output_stream.write(line) |
| elif line.startswith(comment_string): |
| # Start -> FoundHeaderStart. |
| state = State.FoundHeaderStart |
| else: |
| # Inject header then state transition: Start -> Done. |
| inject_header(new_header, output_stream, comment_string) |
| output_stream.write(line) |
| |
| state = State.Done |
| elif state == State.FoundHeaderStart: |
| # We have found the beginning of the header comment, now we have to |
| # look for the first non comment line, then inject our header, |
| # then transition from FoundHeaderStart -> Done. |
| if not line.startswith(comment_string): |
| inject_header(new_header, output_stream, comment_string) |
| output_stream.write(line) |
| state = State.Done |
| elif state == State.Done: |
| # Copy input to output verbatim |
| output_stream.write(line) |
| else: |
| if state == State.Start or state == State.FoundHeaderStart: |
| # Input is empty, inject the header and be done with it |
| inject_header(new_header, output_stream, comment_string) |
| |
| state = State.Done |
| |
| output_stream.flush() |
| |
| |
| def inject_header(header_stream, output_stream, comment_string): |
| """ |
| Writes the header onto the output stream. |
| |
| @param header_stream: Input stream for the header. A seek to the beginning |
| of the file is performed with every call. |
| @type header_stream: file |
| |
| @param output_stream: The stream where the header will be written. |
| @type output_stream: file |
| |
| @param comment_string: The string used to start a comment which spans until |
| the end of the line. |
| @type comment_string: str |
| """ |
| header_stream.seek(0) |
| |
| for line in header_stream: |
| if line.strip() == "": |
| output_stream.write(comment_string + "\n") |
| else: |
| output_stream.write(comment_string + " " + line.rstrip(' ')) |
| |
| if __name__ == "__main__": |
| main() |