blob: 2e700e2ab856942067b04ab13be69d5031a50a3b [file] [log] [blame] [edit]
#!/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()