#! /usr/bin/env escript
%-module(besea).
%% =====================================================================
%% Bengts Erlang Self Extracting Archive
%%
%% Copyright (C) 2007 Bengt Kleberg
%%
%% This progam is free software; you can redistribute it and/or modify
%% it under the terms of the GNU Lesser General Public License as
%% published by the Free Software Foundation; either version 2 of the
%% License, or (at your option) any later version.
%%
%% This library 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
%% Lesser General Public License for more details.
%%
%% You should have received a copy of the GNU Lesser General Public
%% License along with this library; if not, write to the Free Software
%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
%% USA
%%
%% Author contact: bengt.kleberg@comhem.se
%%
%% ===============================================================%%
%% @doc Handle a self extracting Erlang archive.
%%
%%%
This escript makes it possible to create an Erlang
%%% self extracting archive. The archive can list its contents and
%%% extract its contents. it is also possible to run Erlang
%%% modules in the archive, without first extracting them.
-export([main/1]).
%% @spec main(Arguments::list()) -> item()
% input
% Arguments : list() of list().
% returns
%
% execptions
%
% main() is called from escript and has a single list of lists as its
% argument. These are the scripts arguments.
main( Args ) ->
Script = script_name(),
case Args of
["extract"|Wanted] -> extract( Script, Wanted );
["help"] -> help( Script );
["help", Command] -> help( Script, Command );
["list"|Contents] -> list( Script, Contents );
["new", File|Contents] -> new( Script, File, Contents );
["run", Module, Function|Arguments] -> run( Script, Module, Function, Arguments );
_ -> help( Script )
end.
%% @spec binary_part_starts(Binary::binary(), Part::binary()) -> integer()
% input
% Binary : any binary (here used with file contents)
% Part : any binary (here used with a string expected in the file)
% returns
% -1 if Part is <<>> or not found, 0 -> size(Binary) if found.
%
% execption
%
% find out the offset (to be used by erlang:split_binary/2)
% where a binary Part starts in Binary.
binary_part_starts( Binary, Part ) -> binary_part_starts( Binary, Part, 0, Part, 0 ).
binary_part_starts( _Binary, <<>>, Found, _Whole_part, _N ) -> Found;
binary_part_starts( <<>>, _Part, _Found, _Whole_part, _N ) -> -1;
binary_part_starts( <>, <>, Found, Whole_part, N ) ->
binary_part_starts( Tb, Tp, Found, Whole_part, N+1 );
binary_part_starts( <<_B:1/binary,Tb/binary>>, _Part, _Found, Whole_part, N ) ->
binary_part_starts( Tb, Whole_part, N+1, Whole_part, N+1 ).
%% @spec code(File::string()) -> binary()
% input
% File : a file name
% returns
% the contents of File, before the separator between code and data
% execption
%
% find the code (script) part of a File
code( File ) ->
{ok, File_contents} = file:read_file( File ),
{Code, _Rest} = split_file( File_contents, end_of_code() ),
Code.
%% @spec data(File::string()) -> binary()
% input
% File : a file name
% returns
% the contents of File, after the separator between code and data
% execption
%
% find the data part of a File
data( Script ) ->
{ok, File_contents} = file:read_file( Script ),
{_Code, Data} = split_file( File_contents, end_of_code() ),
erlang:list_to_binary( uncomment(Data) ).
% constants
data_size_binary() -> 20.
data_size_printable_binary() -> data_size_binary() * printable_binary_octet_size().
end_of_code() ->
erlang:list_to_binary( [line_start(), <<"End of Code. Beginning of Data (if any).">>, line_end()] ).
%% @spec extract(Script::string(), Wanted_list::list()) -> item()
% input
% Script : the file name of his script
% Wanted_list : list() of file names that we want to extract
% returns
%
% execption
%
% extract all files in the archive (this script) if Wanted_list is []
% or
% extract only files in Wanted_list from the archive
%
extract( Script, Wanted_list ) ->
Zip_archive = data( Script ),
case Wanted_list of
[] -> zip:extract( Zip_archive );
_Wanted -> extract( Script, Wanted_list, Zip_archive )
end.
extract( _Script, Wanted_list, Zip_archive ) ->
Fun = fun (File_info) -> is_file_wanted( Wanted_list, File_info ) end,
zip:extract( Zip_archive, [{file_filter, Fun}] ).
help( Script ) ->
io:fwrite( "The commands are: extract help list new run~n" ),
io:fwrite( "Do:~n~s help ~n", [Script] ),
io:fwrite( "for more help~n" ).
help( Script, "extract" ) ->
io:fwrite( "~s extract~n", [Script] ),
io:fwrite( "Extract the contents of this script archive.~n" );
help( Script, "help" ) ->
help( Script );
help( Script, "list" ) ->
io:fwrite( "~s list~n", [Script] ),
io:fwrite( "List the contents of this script archive.~n" ),
io:fwrite( "~s list ... ~n", [Script] ),
io:fwrite( "List to in the archive. Warn if they do not exist.~n" ),
io:fwrite( "If any of to is a directory all files under it will be recursively listed.~n" );
help( Script, "new" ) ->
io:fwrite( "~s new ~n", [Script] ),
io:fwrite( "Create a new copy of the code of this script called ~n" ),
io:fwrite( "~s new ... ~n", [Script] ),
io:fwrite( "Create a new copy of of the code of this script called archiving to into it~n" ),
io:fwrite( "If any of to is a directory all files under it will be recursively added to the archive.~n" );
help( Script, "run" ) ->
io:fwrite( "~s run [ ...]~n", [Script] ),
io:fwrite( "Run module:function( Arg1, ... ), where all arguments are strings~n" ),
io:fwrite( "Module and function are in this script archive~n" ),
io:fwrite( "Without any arguments you will get module:function()~n" );
help( Script, Command ) ->
io:fwrite( "Unknown command ~s~n", [Command] ),
help( Script ).
%% @spec is_file_wanted(Wanted_list::list(), File_info::tuple()) -> boolean()
% input
% Wanted_list : list() of file names that we want
% File_info : tuple from Zip archive. Only the File_name list is of interest
% returns
% boolean()
% execption
%
% find out if the File_name in the Zip archive tuple, is one that we want.
% ie, if is present in the Wanted_list.
% Wanted_list can be the name of a directory, and then we want all files in that directory.
is_file_wanted( Wanted_list,
{_File_info, File_name, _Other_info, _Comment, _Size, _Other_size} ) ->
Fun = fun(Wanted) ->
case Wanted of
File_name -> true;
_Else ->
%% no direct match with Wanted and File_name.
%% it is also possible that Wanted is a directory path
%% in the begining of File_anme
%% if not present add '/' to the end of Wanted
Directory = case lists:reverse( Wanted ) of
[$/|_T] -> Wanted;
Reversed -> lists:reverse( [$/|Reversed] )
end,
case string:str( File_name, Directory ) of
1 -> true;
_N -> false
end
end
end,
lists:any( Fun, Wanted_list ).
% constants
line_end() -> <<"\n">>.
line_end_size() -> erlang:size( line_end() ).
line_start() -> <<"% ">>.
line_start_size() -> erlang:size( line_start() ).
%% @spec list(Script::string(), Contents::list()) -> item()
% input
% Script : the file name of his script
% Contents : list() of file names that we want to list
% returns
%
% execption
%
% list all the contents of this Script/archive if Wanted_list is []
% or
% list only files in Wanted_list from the archive
list( Script, Contents ) ->
Zip_archive = data( Script ),
case Contents of
[] -> zip:t( Zip_archive );
Contents -> list( Script, Contents, zip:list_dir(Zip_archive) )
end.
list( Script, Contents, {ok, [_Comment|Files]} ) ->
Fun = fun({_File_info, File_name, _Other_info, _Comment1, _Size, _Other_size}) -> io:fwrite( "~s~n", [File_name] ) end,
lists:foreach( Fun, wanted_files(Script, Contents, Files) );
list( Script, _Contents, {error, _Reason} ) ->
io:fwrite( "~s: error opening archive~n", [Script] ).
%% @spec new(Script::string(), Name::list(), Files::list()) -> item()
% input
% Script : the file name of his script
% Name : the file name of the new script
% Files : list() of file names that we want to put into File
% returns
%
% execption
%
% Create a new script called File using this script as a template.
% all files in Contents will be placed in the new archive.
% if contents is [] the new script/archive will be empty
new( Script, Name, Files ) ->
Code = code( Script ),
Comment_data = new_comment_data( Script, Name, Files ),
New = [Code, end_of_code()|Comment_data],
file:write_file( Name, erlang:list_to_binary(New) ).
%% @spec new_comment_data(Script::string(), Name::list(), Files::list()) -> binary()
% input
% Script : the file name of his script
% Name : the file name of the new script
% Files : list() of file names that we want to put into File
% returns
% The contents of files in a zip archive, as lots of commented lines
% execption
%
% Take the Files and but them into as ziop archive.,
% turn the zip archive into commented (starts with %) lines
% these are suitable to have as data part in an escript
new_comment_data( _Script, _Name, [] ) -> [<<>>];
new_comment_data( Script, Name, Files ) ->
case zip_create( Name, Files ) of
Zip_archive when is_binary(Zip_archive) -> printable_binary_comment_lines( Zip_archive );
Error ->
io:fwrite( "~s: could not create archive: ~w~n", [Script, Error] ),
[<<>>]
end.
%% @spec printable_binary_comment_lines(Binary::binary()) -> binary()
% input
% Binary : any binary (in this case a zip archive)
% returns
% the binary as a lot of commented (starts with %) lines
% execption
%
% take a binary and change it into lots of lines of hex numbers.
% all lines start with % and are suitable commnts in an escript
printable_binary_comment_lines( Binary ) ->
printable_binary_comment_lines( Binary, data_size_binary(), line_start(), line_end(), [] ).
printable_binary_comment_lines( Binary, Size, Start, End, Acc ) ->
case Binary of
<> ->
Commented_line = [Start, printable_binary(Chunk), End],
printable_binary_comment_lines( T, Size, Start, End, [Commented_line|Acc] );
<<_Empty:0, Last/binary>> ->
Commented_line = [Start, printable_binary(Last), End],
lists:reverse( [Commented_line|Acc] )
end.
printable_binary( <<>> ) -> [];
printable_binary( <> ) ->
[printable_binary_octet(Octet)|printable_binary(T)].
% no more binary since we have less than 8 bits
printable_binary_octet( <> ) ->
[printable_binary_4bits( High ), printable_binary_4bits( Low )].
printable_binary_octet_size() -> 2.
printable_binary_4bits( 0 ) -> <<"0">>;
printable_binary_4bits( 1 ) -> <<"1">>;
printable_binary_4bits( 2 ) -> <<"2">>;
printable_binary_4bits( 3 ) -> <<"3">>;
printable_binary_4bits( 4 ) -> <<"4">>;
printable_binary_4bits( 5 ) -> <<"5">>;
printable_binary_4bits( 6 ) -> <<"6">>;
printable_binary_4bits( 7 ) -> <<"7">>;
printable_binary_4bits( 8 ) -> <<"8">>;
printable_binary_4bits( 9 ) -> <<"9">>;
printable_binary_4bits( 10 ) -> <<"a">>;
printable_binary_4bits( 11 ) -> <<"b">>;
printable_binary_4bits( 12 ) -> <<"c">>;
printable_binary_4bits( 13 ) -> <<"d">>;
printable_binary_4bits( 14 ) -> <<"e">>;
printable_binary_4bits( 15 ) -> <<"f">>.
%% @spec run(Script::string(), Module::list(), Function::string(), Arguments::list()) -> item()
% input
% Script : the name of this script/archive
% Module : the name of the module to run
% Function : the name of the function to run
% Arguments : a list of arguments (all strings) to the function
% returns
%
% execption
%
% load all erlang byte code files from the archive.
% run module functiton with arguments
run( Script, Module, Function, Arguments ) ->
Data = data( Script ),
run( Script, Module, Function, Arguments, zip:extract( Data, [memory]) ).
run( Script, Module, Function, Arguments, {ok, Files} ) ->
Extension = code:objfile_extension(),
Fun = fun({File_name, File_contents}) ->
case filename:extension( File_name ) of
Extension ->
M = erlang:list_to_atom( filename:basename(File_name, Extension) ),
case code:load_binary( M, File_name, File_contents ) of
{module, M} -> ok;
Error -> io:fwrite( "~s: failed to load ~s: ~p~n", [Script, File_name, Error] )
end;
_Else -> ok
end
end,
lists:foreach( Fun, Files ),
M = erlang:list_to_atom( Module ),
F = erlang:list_to_atom( Function ),
erlang:apply( M, F, Arguments );
run( Script, _Module, _Function, _Arguments, {error, _Reason} ) ->
io:fwrite( "~s: error extracting archive~n", [Script] ).
%% @spec script_name() -> string()
% input
%
% returns
% the name of this script as a string.
%
% execption
%
script_name() ->
[Script_name|_T] = init:get_plain_arguments(),
Script_name.
%% @spec slit_file(File_contents::binary(), Separator::binary()) -> tuple()
% input
% File_contents : file contents as binary()
% Separator : a part of file contens, also binary()
% returns
% a tuple of binaries. First member is the file contents before Separator
% the second memeber is the file contents after Separator
%
% execption
%
% split a binary into two parts. Before and after Separator.
% Separator itself is not part of before, and not first in after.
% But it might be somewhere else in after.
split_file( File_contents, Separator ) ->
case binary_part_starts( File_contents, Separator ) of
-1 -> {File_contents,<<>>};
N -> % split right before End_of_code or else the first character will be doubled
{Code,Rest} = erlang:split_binary( File_contents, N ),
{_Separator,Data} = erlang:split_binary( Rest, erlang:size(Separator) ),
{Code,Data}
end.
%% @spec uncomment(Binary::binary()) -> binary()
% input
% Binary : lot of commented (starts with %) lines
% returns
% the binary without commentes and lines
% execption
%
% take lots of lines of hex numbers.
% all lines start with % and are suitable comments in an escript
% change them into a binary,
uncomment( <<>> ) -> [<<>>];
uncomment( Printable_binary_comment_lines ) ->
uncomment( Printable_binary_comment_lines, data_size_printable_binary(), line_start_size(), line_end_size(), [] ).
uncomment( Binary, Size, Start_size, End_size, Acc ) ->
case Binary of
<<>> -> lists:reverse( Acc );
<<_Start:Start_size/binary, Printable_binary:Size/binary, _End:End_size/binary, T/binary>> ->
Unprintable_binary = unprintable_binary( Printable_binary ),
uncomment( T, Size, Start_size, End_size, [Unprintable_binary|Acc] );
<<_Start:Start_size/binary, Last_line_end/binary>> ->
{Last,_End} = erlang:split_binary( Last_line_end, erlang:size(Last_line_end) - End_size ),
lists:reverse( [unprintable_binary(Last)|Acc] )
end.
unprintable_binary( <<>> ) -> [];
unprintable_binary( <> ) ->
[unprintable_binary_octet(Printable_octet)|unprintable_binary(T)].
unprintable_binary_octet( <> ) ->
<<(unprintable_binary_4bits( High ) * 16 + unprintable_binary_4bits( Low ))>>.
unprintable_binary_4bits( <<"0">> ) -> 0;
unprintable_binary_4bits( <<"1">> ) -> 1;
unprintable_binary_4bits( <<"2">> ) -> 2;
unprintable_binary_4bits( <<"3">> ) -> 3;
unprintable_binary_4bits( <<"4">> ) -> 4;
unprintable_binary_4bits( <<"5">> ) -> 5;
unprintable_binary_4bits( <<"6">> ) -> 6;
unprintable_binary_4bits( <<"7">> ) -> 7;
unprintable_binary_4bits( <<"8">> ) -> 8;
unprintable_binary_4bits( <<"9">> ) -> 9;
unprintable_binary_4bits( <<"a">> ) -> 10;
unprintable_binary_4bits( <<"b">> ) -> 11;
unprintable_binary_4bits( <<"c">> ) -> 12;
unprintable_binary_4bits( <<"d">> ) -> 13;
unprintable_binary_4bits( <<"e">> ) -> 14;
unprintable_binary_4bits( <<"f">> ) -> 15.
%% @spec wanted_files(Script::string(), Wanted_list::list(), File_infos::list()) -> list()
% input
% Script : the name of this script/archive
% Wanted_list : list() of file names that we want
% File_infos : list of tuple from Zip archive.
% returns
% a list of the files we want form the archive
% execption
%
% find out if the File_name in the Zip archive tuple, is one that we want.
% ie, if is present in the Wanted_list.
% Wanted_list can be the name of a directory, and then we want all files in that directory.
wanted_files( Script, Wanted_list, File_infos ) ->
Fun = fun(Wanted) ->
case wanted_files( Wanted, File_infos ) of
[] ->
io:fwrite( "~s: ~s: No such file or directory in archive~n", [Script, Wanted] ),
[];
Found -> Found
end
end,
lists:flatmap( Fun, Wanted_list ).
wanted_files( Wanted, File_infos ) ->
Fun = fun(File_info) -> is_file_wanted( [Wanted], File_info ) end,
lists:filter( Fun, File_infos ).
%% @spec zip_create(Name::string(), Files::list()) -> binary()
% input
% Name : name of the zip archive we want to create
% Files : the files we want in the zip archive
% returns
% zip archive
% execption
%
% create a zip archive with Files in it.
zip_create( Name, Files ) ->
Fun = fun (File) ->
case filelib:is_dir( File ) of
%% this is a directory. recurse over it, and its subdirectories,
%% accumulate all files
true -> filelib:fold_files( File, ".+", true, fun (F, Acc) -> [F|Acc] end, [] );
false -> [File]
end
end,
All_files = lists:flatmap( Fun, Files ),
case zip:create( Name, All_files, [memory] ) of
{ok, {Name, Zip_archive}} -> Zip_archive;
{error, Error} -> Error
end.