Packages

Erlang version of diff(1)

Current section

Files

Jump to
ediff src ediff.erl
Raw

src/ediff.erl

%%#!/usr/bin/env escript
%%%
%%% Erlang diff(1)
%%%
%%% Copyright 2017 by Anthony Howe. All rights reserved.
%%%
%%% MIT License.
%%%
%%% Ported from http://www.ioccc.org/2015/howe/spoiler/prog.c
%%%
-module(ediff).
-export([main/1, diff/2, diff/3]).
-ifdef(EUNIT).
-compile(export_all).
-endif.
-define(BUFSIZ, (8*1024)).
-define(INIT_FILE_SIZE, 5).
usage() ->
io:format("usage: ediff [-d] file1 file2~n"),
io:format("-d\t\twrite edit distance~n"),
halt(2).
main(Args) ->
case egetopt:parse(Args, [
{ $d, flag, write_distance }
]) of
{ok, Options, ArgsN} ->
process(Options, ArgsN);
{error, Reason, Opt} ->
io:format("~s -~c~n", [Reason, Opt]),
usage()
end.
process(_Opts, Files) when length(Files) /= 2 ->
usage();
process(Opts, [File1, File2]) ->
try
process(Opts, File1, File2)
catch
throw:{error, File, Reason} ->
io:format(standard_error, "ediff: ~s: ~s~n", [File, str:error(Reason)]),
halt(1)
end.
process(Opts, File1, File2) ->
Fp1 = open_file(File1),
Fp2 = open_file(File2),
case {Fp1, Fp2} of
{standard_io, standard_io} ->
throw({error, "-", 'only one file argument can refer to standard input'});
_ ->
diff(Fp1, Fp2, Opts)
end,
file:close(Fp1),
file:close(Fp2).
open_file("-") ->
io:setopts(standard_io, [binary]),
standard_io;
open_file(File) ->
case file:open(File, [read, binary, {read_ahead, ?BUFSIZ}]) of
{error, Reason} ->
throw({error, File, Reason});
{ok, Fp} ->
Fp
end.
diff(Fp1, Fp2) ->
diff(Fp1, Fp2, []).
diff(Fp1, Fp2, Opts) ->
{Distance, Script} = npdif:compute(hash_file(Fp1), hash_file(Fp2)),
case proplists:get_value(write_distance, Opts, false) of
true ->
io:format("~B~n", [Distance]);
false ->
dump_script(Fp1, Fp2, Script)
end.
hash_file(Fp) ->
% Building list faster than an array when reading a file.
Hashes = hash_file(Fp, [#{seek => 0}]),
% Once built, convert list to array for faster lookups.
array:from_list(lists:reverse(Hashes)).
hash_file(Fp, Lines) ->
{ok, Position} = file:position(Fp, cur),
case file:read_line(Fp) of
{ok, Line} ->
hash_file(Fp, [#{seek => Position, hash => fnv:hash56(Line)} | Lines]);
eof ->
Lines;
Error ->
throw(Error)
end.
dump_script(_FpA, _FpB, []) ->
ok;
dump_script(FpA, FpB, [Curr | Rest]) ->
case Curr of
{true, {Aline, _Aseek}, {Bline, Bseek}} ->
{Bstop, Rest1} = append_block(Bline, Rest),
case Bline < Bstop of
true ->
io:format("~Ba~B,~B~n", [Aline, Bline, Bstop]);
false ->
io:format("~Ba~B~n", [Aline, Bline])
end,
file:position(FpB, Bseek),
echo_lines(FpB, "> ", Bstop - Bline + 1);
{false, {Aline, Aseek}, {Bline, _Bseek}} ->
{Astop, Rest1} = delete_block(Aline, Rest),
case Aline < Astop of
true ->
io:format("~B,~Bd~B~n", [Aline, Astop, Bline]);
false ->
io:format("~Bd~B~n", [Aline, Bline])
end,
file:position(FpA, Aseek),
echo_lines(FpA, "< ", Astop - Aline + 1)
end,
dump_script(FpA, FpB, Rest1).
append_block(Lineno, []) ->
{Lineno, []};
append_block(Lineno, [Next | Rest] = Script) ->
{Op, _, {Bline, _}} = Next,
case Op andalso Lineno+1 == Bline of
false ->
{Lineno, Script};
true ->
append_block(Bline, Rest)
end.
delete_block(Lineno, []) ->
{Lineno, []};
delete_block(Lineno, [Next | Rest] = Script) ->
{Op, {Aline, _}, _} = Next,
case not Op andalso Lineno+1 == Aline of
false ->
{Lineno, Script};
true ->
delete_block(Aline, Rest)
end.
echo_lines(_, _, 0) ->
ok;
echo_lines(Fp, Prefix, Count) ->
case file:read_line(Fp) of
eof ->
eof;
{ok, Line} ->
file:write(standard_io, Prefix),
file:write(standard_io, Line),
case str:at(Line, str:len(Line)-1) == $\n of
false ->
io:nl();
true ->
ok
end,
echo_lines(Fp, Prefix, Count-1)
end.