mirror of
https://github.com/Mercury-Language/mercury.git
synced 2026-04-15 09:23:44 +00:00
git_hooks/update_copyright.pre-commit:
Document the operation of this script in more detail.
Document the use of the new Makefile, and of the new environment
variable that can be used to avoid redundant copies of the update_copyright
program's executable.
git_hooks/Makefile:
Add a trivial makefile to shorten the build and clean commands.
git_hooks/update_copyright.m:
Merge contiguous year ranges in copyright lines.
418 lines
13 KiB
Mathematica
418 lines
13 KiB
Mathematica
%---------------------------------------------------------------------------%
|
|
% vim: ft=mercury ts=4 sw=4 et
|
|
%---------------------------------------------------------------------------%
|
|
% Copyright (C) 2024 YesLogic Pty. Ltd.
|
|
% Copyright (C) 2024 The Mercury team.
|
|
% This file may only be copied under the terms of the GNU General
|
|
% Public License - see the file COPYING in the Mercury distribution.
|
|
%---------------------------------------------------------------------------%
|
|
%
|
|
% This program updates the first matching Copyright line of each file to
|
|
% include the current year. If one or more file names are specified on the
|
|
% command line, then those files will be updated in-place (files already
|
|
% containing up-to-date Copyright lines will not be modified).
|
|
% If no file names are specified, the input will be read from standard input,
|
|
% and the output written to standard output.
|
|
%
|
|
% Usage: update_copyright [OPTIONS] [FILES]...
|
|
%
|
|
% Options:
|
|
%
|
|
% -q, --quiet Quiet mode: don't print anything except error messages.
|
|
% -s, --suffix STR Match only Copyright lines with STR in the suffix.
|
|
%
|
|
% Exit status:
|
|
%
|
|
% 0 No files modified.
|
|
% 1 One or more files modified.
|
|
% 2 An error occurred.
|
|
%
|
|
%---------------------------------------------------------------------------%
|
|
|
|
:- module update_copyright.
|
|
:- interface.
|
|
|
|
:- import_module io.
|
|
|
|
:- pred main(io::di, io::uo) is det.
|
|
|
|
%---------------------------------------------------------------------------%
|
|
%---------------------------------------------------------------------------%
|
|
|
|
:- implementation.
|
|
|
|
:- import_module bool.
|
|
:- import_module char.
|
|
:- import_module getopt.
|
|
:- import_module int.
|
|
:- import_module list.
|
|
:- import_module maybe.
|
|
:- import_module string.
|
|
:- import_module time.
|
|
|
|
:- type year_range
|
|
---> years(int, int). % can be equal
|
|
|
|
:- type option
|
|
---> quiet
|
|
; suffix.
|
|
|
|
:- type options
|
|
---> options(bool, maybe(string)).
|
|
% The values of the quiet and suffix options respectively.
|
|
|
|
:- type mod_state
|
|
---> unmodified % no copyright line found yet
|
|
; found_unmodified % found copyright, already up-to-date
|
|
; found_modified. % found copyright, updated
|
|
|
|
%---------------------------------------------------------------------------%
|
|
|
|
main(!IO) :-
|
|
io.command_line_arguments(Args, !IO),
|
|
OptionOps = option_ops_multi(short_option, long_option, option_default),
|
|
getopt.process_options(OptionOps, Args, NonOptionArgs, OptionResult),
|
|
(
|
|
OptionResult = ok(OptionTable),
|
|
current_year(Year, !IO),
|
|
getopt.lookup_bool_option(OptionTable, quiet, Quiet),
|
|
getopt.lookup_maybe_string_option(OptionTable, suffix,
|
|
MaybeExpectSuffix),
|
|
Options = options(Quiet, MaybeExpectSuffix),
|
|
(
|
|
NonOptionArgs = [],
|
|
process_stdin(Options, Year, !IO)
|
|
;
|
|
NonOptionArgs = [_ | _],
|
|
process_files(Options, Year, NonOptionArgs, !IO)
|
|
)
|
|
;
|
|
OptionResult = error(Error),
|
|
report_error_message(option_error_to_string(Error), !IO)
|
|
).
|
|
|
|
%---------------------------------------------------------------------------%
|
|
|
|
:- pred short_option(char::in, option::out) is semidet.
|
|
|
|
short_option('q', quiet).
|
|
short_option('s', suffix).
|
|
|
|
:- pred long_option(string::in, option::out) is semidet.
|
|
|
|
long_option("quiet", quiet).
|
|
long_option("suffix", suffix).
|
|
|
|
:- pred option_default(option::out, option_data::out) is multi.
|
|
|
|
option_default(quiet, bool(no)).
|
|
option_default(suffix, maybe_string(no)).
|
|
|
|
%---------------------------------------------------------------------------%
|
|
|
|
:- pred current_year(int::out, io::di, io::uo) is det.
|
|
|
|
current_year(Year, !IO) :-
|
|
time(Time, !IO),
|
|
localtime(Time, TM, !IO),
|
|
Year = 1900 + TM ^ tm_year.
|
|
|
|
:- pred process_stdin(options::in, int::in, io::di, io::uo) is det.
|
|
|
|
process_stdin(Options, CurrentYear, !IO) :-
|
|
io.input_stream(InputStream, !IO),
|
|
read_lines_loop(InputStream, Options, CurrentYear,
|
|
[], RevLines, unmodified, _ModState, !IO),
|
|
io.output_stream(OutputStream, !IO),
|
|
list.foldl(io.write_string(OutputStream), list.reverse(RevLines), !IO).
|
|
|
|
:- pred process_files(options::in, int::in, list(string)::in,
|
|
io::di, io::uo) is det.
|
|
|
|
process_files(_Options, _CurrentYear, [], !IO).
|
|
process_files(Options, CurrentYear, [FileName | FileNames], !IO) :-
|
|
process_file(Options, CurrentYear, FileName, Continue, !IO),
|
|
(
|
|
Continue = yes,
|
|
process_files(Options, CurrentYear, FileNames, !IO)
|
|
;
|
|
Continue = no
|
|
).
|
|
|
|
:- pred process_file(options::in, int::in, string::in, bool::out,
|
|
io::di, io::uo) is det.
|
|
|
|
process_file(Options, CurrentYear, FileName, Continue, !IO) :-
|
|
io.open_input(FileName, OpenInputRes, !IO),
|
|
(
|
|
OpenInputRes = ok(InputStream),
|
|
read_lines_loop(InputStream, Options, CurrentYear,
|
|
[], RevLines, unmodified, ModState, !IO),
|
|
io.close_input(InputStream, !IO),
|
|
Options = options(Quiet, _),
|
|
(
|
|
( ModState = unmodified
|
|
; ModState = found_unmodified
|
|
),
|
|
(
|
|
Quiet = no,
|
|
io.format("Unchanged: %s\n", [s(FileName)], !IO)
|
|
;
|
|
Quiet = yes
|
|
),
|
|
Continue = yes
|
|
;
|
|
ModState = found_modified,
|
|
% It would be better to write to a temporary file and then
|
|
% atomically rename that temporary file to the original file,
|
|
% but that would require extra work to preserve the file ownership
|
|
% and permissions. For simplicity, we forgo atomicity.
|
|
io.open_output(FileName, OpenOutputRes, !IO),
|
|
(
|
|
OpenOutputRes = ok(OutputStream),
|
|
list.foldl(io.write_string(OutputStream),
|
|
list.reverse(RevLines), !IO),
|
|
io.close_output(OutputStream, !IO),
|
|
(
|
|
Quiet = no,
|
|
io.format("Modified: %s\n", [s(FileName)], !IO)
|
|
;
|
|
Quiet = yes
|
|
),
|
|
maybe_set_modified_exit_status(!IO),
|
|
Continue = yes
|
|
;
|
|
OpenOutputRes = error(Error),
|
|
report_io_error("Error opening " ++ FileName, Error, !IO),
|
|
Continue = no
|
|
)
|
|
)
|
|
;
|
|
OpenInputRes = error(Error),
|
|
report_io_error("Error opening " ++ FileName, Error, !IO),
|
|
Continue = no
|
|
).
|
|
|
|
%---------------------------------------------------------------------------%
|
|
|
|
:- pred read_lines_loop(io.text_input_stream::in, options::in, int::in,
|
|
list(string)::in, list(string)::out, mod_state::in, mod_state::out,
|
|
io::di, io::uo) is det.
|
|
|
|
read_lines_loop(InputStream, Options, CurrentYear,
|
|
!RevLines, !ModState, !IO) :-
|
|
io.read_line_as_string(InputStream, ReadRes, !IO),
|
|
(
|
|
ReadRes = ok(Line),
|
|
( if
|
|
!.ModState = unmodified,
|
|
parse_copyright_line(Options, Line, Prefix, Ranges0, Suffix)
|
|
then
|
|
list.sort(Ranges0, Ranges1),
|
|
merge_adjacent_ranges_if_possible(Ranges1, Ranges2),
|
|
( if add_to_ranges(CurrentYear, Ranges2, Ranges) then
|
|
make_copyright_line(Prefix, Ranges, Suffix, NewLine),
|
|
!:RevLines = [NewLine | !.RevLines],
|
|
!:ModState = found_modified
|
|
else
|
|
!:RevLines = [Line | !.RevLines],
|
|
!:ModState = found_unmodified
|
|
)
|
|
else
|
|
!:RevLines = [Line | !.RevLines]
|
|
),
|
|
read_lines_loop(InputStream, Options, CurrentYear,
|
|
!RevLines, !ModState, !IO)
|
|
;
|
|
ReadRes = eof
|
|
;
|
|
ReadRes = error(Error),
|
|
report_io_error("Error reading", Error, !IO)
|
|
).
|
|
|
|
:- pred parse_copyright_line(options::in, string::in,
|
|
string::out, list(year_range)::out, string::out) is semidet.
|
|
|
|
parse_copyright_line(Options, Line, Prefix, Ranges, Suffix) :-
|
|
string.sub_string_search(Line, "Copyright ", AfterCopyright),
|
|
find_prefix_end(Line, ' ', AfterCopyright, PrefixEnd),
|
|
find_suffix_start(Line, PrefixEnd, PrefixEnd, SuffixStart),
|
|
Options = options(_, MaybeExpectSuffix),
|
|
(
|
|
MaybeExpectSuffix = no
|
|
;
|
|
MaybeExpectSuffix = yes(ExpectSuffix),
|
|
string.sub_string_search_start(Line, ExpectSuffix, SuffixStart, _)
|
|
),
|
|
string.unsafe_between(Line, 0, PrefixEnd, Prefix),
|
|
string.unsafe_between(Line, PrefixEnd, SuffixStart, Mid),
|
|
string.unsafe_between(Line, SuffixStart, length(Line), Suffix),
|
|
parse_ranges(Mid, Ranges).
|
|
|
|
:- pred find_prefix_end(string::in, char::in, int::in, int::out) is semidet.
|
|
|
|
find_prefix_end(Str, PrevC, I0, I) :-
|
|
string.unsafe_index_next(Str, I0, I1, C),
|
|
% We'll assume the first digit following whitespace begins the year ranges.
|
|
( if
|
|
char.is_digit(C),
|
|
char.is_whitespace(PrevC)
|
|
then
|
|
I = I0
|
|
else
|
|
find_prefix_end(Str, C, I1, I)
|
|
).
|
|
|
|
:- pred find_suffix_start(string::in, int::in, int::in, int::out) is semidet.
|
|
|
|
find_suffix_start(Str, I0, LastNonWs, I) :-
|
|
( if string.unsafe_index_next(Str, I0, I1, C) then
|
|
( if
|
|
( char.is_digit(C)
|
|
; C = ('-')
|
|
; C = (',')
|
|
)
|
|
then
|
|
find_suffix_start(Str, I1, I1, I)
|
|
else if char.is_whitespace(C) then
|
|
find_suffix_start(Str, I1, LastNonWs, I)
|
|
else
|
|
I = LastNonWs
|
|
)
|
|
else
|
|
I = LastNonWs
|
|
).
|
|
|
|
:- pred parse_ranges(string::in, list(year_range)::out) is semidet.
|
|
|
|
parse_ranges(Str, Ranges) :-
|
|
Words = string.words_separator(is_whitespace_or_comma, Str),
|
|
list.map(parse_range, Words, Ranges).
|
|
|
|
:- pred is_whitespace_or_comma(char::in) is semidet.
|
|
|
|
is_whitespace_or_comma(C) :-
|
|
( char.is_whitespace(C)
|
|
; C = (',')
|
|
).
|
|
|
|
:- pred parse_range(string::in, year_range::out) is semidet.
|
|
|
|
parse_range(Str, Range) :-
|
|
Words = string.split_at_char('-', Str),
|
|
(
|
|
Words = [S],
|
|
string.to_int(S, N),
|
|
Range = years(N, N)
|
|
;
|
|
Words = [S1, S2],
|
|
string.to_int(S1, N1),
|
|
string.to_int(S2, N2),
|
|
N1 =< N2,
|
|
Range = years(N1, N2)
|
|
).
|
|
|
|
:- pred merge_adjacent_ranges_if_possible(
|
|
list(year_range)::in, list(year_range)::out) is det.
|
|
|
|
merge_adjacent_ranges_if_possible(Ranges0, Ranges) :-
|
|
(
|
|
( Ranges0 = []
|
|
; Ranges0 = [_]
|
|
),
|
|
Ranges = Ranges0
|
|
;
|
|
Ranges0 = [Range1, Range2 | Ranges3plus],
|
|
Range1 = years(RangeLo1, RangeHi1),
|
|
Range2 = years(RangeLo2, RangeHi2),
|
|
( if RangeLo2 =< RangeHi1 + 1 then
|
|
Range12 = years(RangeLo1, RangeHi2),
|
|
merge_adjacent_ranges_if_possible([Range12 | Ranges3plus], Ranges)
|
|
else
|
|
merge_adjacent_ranges_if_possible([Range2 | Ranges3plus],
|
|
TailRanges),
|
|
Ranges = [Range1 | TailRanges]
|
|
)
|
|
).
|
|
|
|
:- pred add_to_ranges(int::in, list(year_range)::in, list(year_range)::out)
|
|
is semidet.
|
|
|
|
add_to_ranges(Year, Ranges0, Ranges) :-
|
|
(
|
|
Ranges0 = [],
|
|
Ranges = [years(Year, Year)]
|
|
;
|
|
Ranges0 = [R0 | Ranges1],
|
|
( if year_in_range(Year, R0) then
|
|
fail
|
|
else if extend_range(Year, R0, R) then
|
|
Ranges = [R | Ranges1]
|
|
else
|
|
add_to_ranges(Year, Ranges1, Ranges2),
|
|
Ranges = [R0 | Ranges2]
|
|
)
|
|
).
|
|
|
|
:- pred year_in_range(int::in, year_range::in) is semidet.
|
|
|
|
year_in_range(Year, Range) :-
|
|
Range = years(Lo, Hi),
|
|
Year >= Lo,
|
|
Year =< Hi.
|
|
|
|
:- pred extend_range(int::in, year_range::in, year_range::out) is semidet.
|
|
|
|
extend_range(Year, Range0, Range) :-
|
|
Range0 = years(Lo, Hi),
|
|
Year = Hi + 1,
|
|
Range = years(Lo, Year).
|
|
|
|
:- pred make_copyright_line(string::in, list(year_range)::in, string::in,
|
|
string::out) is det.
|
|
|
|
make_copyright_line(Prefix, Ranges, Suffix, Line) :-
|
|
Mid = string.join_list(", ", list.map(range_to_string, Ranges)),
|
|
Line = Prefix ++ Mid ++ Suffix.
|
|
|
|
:- func range_to_string(year_range) = string.
|
|
|
|
range_to_string(Range) = Str :-
|
|
Range = years(Lo, Hi),
|
|
( if Lo = Hi then
|
|
Str = string.from_int(Lo)
|
|
else
|
|
Str = string.from_int(Lo) ++ "-" ++ string.from_int(Hi)
|
|
).
|
|
|
|
%---------------------------------------------------------------------------%
|
|
|
|
:- pred report_io_error(string::in, io.error::in, io::di, io::uo) is det.
|
|
|
|
report_io_error(Prefix, Error, !IO) :-
|
|
Message = Prefix ++ ": " ++ io.error_message(Error),
|
|
report_error_message(Message, !IO).
|
|
|
|
:- pred report_error_message(string::in, io::di, io::uo) is det.
|
|
|
|
report_error_message(Message, !IO) :-
|
|
io.stderr_stream(ErrorStream, !IO),
|
|
io.write_string(ErrorStream, Message, !IO),
|
|
io.nl(ErrorStream, !IO),
|
|
io.set_exit_status(2, !IO).
|
|
|
|
:- pred maybe_set_modified_exit_status(io::di, io::uo) is det.
|
|
|
|
maybe_set_modified_exit_status(!IO) :-
|
|
% Don't override exit status for general errors.
|
|
io.get_exit_status(ExitStatus0, !IO),
|
|
( if ExitStatus0 = 0 then
|
|
io.set_exit_status(1, !IO)
|
|
else
|
|
true
|
|
).
|
|
|
|
%---------------------------------------------------------------------------%
|
|
:- end_module update_copyright.
|
|
%---------------------------------------------------------------------------%
|