Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Source file lTerm_history.ml
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513(*
* lTerm_history.ml
* ----------------
* Copyright : (c) 2012, Jeremie Dimino <jeremie@dimino.org>
* Licence : BSD3
*
* This file is a part of Lambda-Term.
*)openCamomileLibraryDefault.Camomileletreturn,(>>=)=Lwt.return,Lwt.(>>=)(* A node contains an entry of the history. *)typenode={mutabledata:Zed_string.t;mutablesize:int;mutableprev:node;}typet={mutableentries:node;(* Points to the first entry (the most recent). Its [prev] is a fake
node used as marker, is after the oldest entry. *)mutablefull_size:int;mutablelength:int;mutablemax_size:int;mutablemax_entries:int;mutableold_count:int;mutablecache:Zed_string.tlistoption;(* When set, the cache is equal to the list of entries, from the
most recent to the oldest. *)}letentry_sizestr=letzChar_newline=Zed_char.unsafe_of_char'\n'andzChar_slash=Zed_char.unsafe_of_char'\\'inletsize=ref0inleteos=Zed_string.bytesstrinletreccalcofs=ifofs<eosthenletch,ofs=Zed_string.extract_nextstrofsinifZed_char.comparechzChar_newline=0||Zed_char.comparechzChar_slash=0thensize:=!size+2elsesize:=!size+1;calcofsincalc0;!size+1(* Check that [size1 + size2 < limit], handling overflow. *)letsize_oksize1size2limit=letsum=size1+size2insum>=0&&sum<=limitletcreate?(max_size=max_int)?(max_entries=max_int)init=ifmax_size<0theninvalid_arg"LTerm_history.create: negative maximum size";ifmax_entries<0theninvalid_arg"LTerm_history.create: negative maximum number of entries";letrecauxsizecountnodeentries=matchentrieswith|[]->(size,count,node)|entry::entries->letentry_size=entry_sizeentryinifsize_oksizeentry_sizemax_size&&count+1<max_entriesthenbeginletnext={data=Zed_string.empty();prev=node;size=0}innode.data<-entry;node.size<-entry_size;aux(size+entry_size)(count+1)nextentriesendelse(size,count,node)inletrecnode={data=Zed_string.empty();size=0;prev=node}inletsize,count,marker=aux00nodeinitinnode.prev<-marker;{entries=node;full_size=size;length=count;max_size=max_size;max_entries=max_entries;old_count=count;cache=None;}letspaces=UCharInfo.load_property_tbl`White_Spaceletis_space_uCharch=UCharTbl.Bool.getspaceschletis_spacech=Zed_char.for_allis_space_uCharchletis_emptystr=Zed_string.for_allis_spacestrletis_duphistoryentry=history.length>0&&history.entries.data=entry(* Remove the oldest entry of history, precondition: the history
contains at least one entry. *)letdrop_oldesthistory=letlast=history.entries.prev.previn(* Make [last] become the end of entries marker. *)history.entries.prev<-last;(* Update counters. *)history.length<-history.length-1;history.full_size<-history.full_size-last.size;ifhistory.old_count>0thenhistory.old_count<-history.old_count-1;(* Clear the marker so its contents can be garbage collected. *)last.data<-Zed_string.empty();last.size<-0letadd_auxhistorydatasize=ifsize<=history.max_sizethenbegin(* Check length. *)ifhistory.length=history.max_entriesthenbeginhistory.cache<-None;(* We know that [max_entries > 0], so the precondition is
verified. *)drop_oldesthistoryend;(* Check size. *)ifnot(size_okhistory.full_sizesizehistory.max_size)thenbeginhistory.cache<-None;(* We know that size <= max_size, so we are here only if there
is at least one other entry in the history, so the
precondition is verified. *)drop_oldesthistory;whilenot(size_okhistory.full_sizesizehistory.max_size)do(* Same here. *)drop_oldesthistorydoneend;(* Add the entry. *)letnode={data=data;size=size;prev=history.entries.prev}inhistory.entries.prev<-node;history.entries<-node;history.length<-history.length+1;history.full_size<-history.full_size+size;matchhistory.cachewith|None->()|Somel->history.cache<-Some(data::l)endletaddhistory?(skip_empty=true)?(skip_dup=true)entry=ifhistory.max_entries>0&&history.max_size>0&¬(skip_empty&&is_emptyentry)&¬(skip_dup&&is_duphistoryentry)thenadd_auxhistoryentry(entry_sizeentry)letreclist_of_nodesmarkeraccnode=ifnode==markerthenaccelselist_of_nodesmarker(node.data::acc)node.prevletcontentshistory=matchhistory.cachewith|Somel->l|None->letmarker=history.entries.previnletl=list_of_nodesmarker[]marker.previnhistory.cache<-Somel;lletsizehistory=history.full_sizeletlengthhistory=history.lengthletold_counthistory=history.old_countletmax_sizehistory=history.max_sizeletmax_entrieshistory=history.max_entriesletset_old_counthistoryn=ifn<0theninvalid_arg"LTerm_history.set_old_count: negative old count";ifn>history.lengththeninvalid_arg"LTerm_history.set_old_count: old count greater than the length of the history";history.old_count<-nletset_max_sizehistorysize=ifsize<0theninvalid_arg"LTerm_history.set_max_size: negative maximum size";ifsize<history.full_sizethenbeginhistory.cache<-None;(* 0 <= size < full_size so there is at least one element. *)drop_oldesthistory;whilesize<history.full_sizedo(* Same here. *)drop_oldesthistorydoneend;history.max_size<-sizeletset_max_entrieshistoryn=ifn<0theninvalid_arg"LTerm_history.set_max_entries: negative maximum number of entries";ifn<history.lengththenbeginhistory.cache<-None;(* 0 <= n < length so there is at least one element. *)drop_oldesthistory;whilen<history.lengthdo(* Same here. *)drop_oldesthistorydoneend;history.max_entries<-n(*let escape_utf8 entry =
let len = String.length entry in
let buf = Buffer.create len in
let rec loop ofs =
if ofs = len then
Buffer.contents buf
else
match String.unsafe_get entry ofs with
| '\n' ->
Buffer.add_string buf "\\n";
loop (ofs + 1)
| '\\' ->
Buffer.add_string buf "\\\\";
loop (ofs + 1)
| ch when Char.code ch <= 127 ->
Buffer.add_char buf ch;
loop (ofs + 1)
| _ ->
let ofs' = Zed_utf8.unsafe_next entry ofs in
Buffer.add_substring buf entry ofs (ofs' - ofs);
loop ofs'
in
loop 0*)letescapeentry=letlen=Zed_string.bytesentryinletbuf=Zed_string.Buf.createleninletzChar_n=Zed_char.unsafe_of_char'n'inletzChar_slash=Zed_char.unsafe_of_char'\\'inletzChar_nl=Zed_char.unsafe_of_char'\n'inletrecloopofs=ifofs=lenthenZed_string.Buf.contentsbufelseletch,ofs=Zed_string.extract_nextentryofsinifZed_char.comparechzChar_nl=0thenbeginZed_string.Buf.add_zCharbufzChar_slash;Zed_string.Buf.add_zCharbufzChar_n;loopofs;endelseifZed_char.comparechzChar_slash=0thenbeginZed_string.Buf.add_zCharbufzChar_slash;Zed_string.Buf.add_zCharbufzChar_slash;loopofs;endelsebeginZed_string.Buf.add_zCharbufch;loopofs;endinloop0(*let unescape_utf8 line =
let len = String.length line in
let buf = Buffer.create len in
let rec loop ofs size =
if ofs = len then
(Buffer.contents buf, size + 1)
else
match String.unsafe_get line ofs with
| '\\' ->
if ofs = len then begin
Buffer.add_char buf '\\';
(Buffer.contents buf, size + 3)
end else begin
match String.unsafe_get line (ofs + 1) with
| 'n' ->
Buffer.add_char buf '\n';
loop (ofs + 2) (size + 2)
| '\\' ->
Buffer.add_char buf '\\';
loop (ofs + 2) (size + 2)
| _ ->
Buffer.add_char buf '\\';
loop (ofs + 1) (size + 2)
end
| ch when Char.code ch <= 127 ->
Buffer.add_char buf ch;
loop (ofs + 1) (size + 1)
| _ ->
let ofs' = Zed_utf8.unsafe_next line ofs in
Buffer.add_substring buf line ofs (ofs' - ofs);
loop ofs' (size + ofs' - ofs)
in
loop 0 0*)letunescapeline=leteos=Zed_string.byteslineinletbuf=Zed_string.Buf.create0inletzChar_n=Zed_char.unsafe_of_char'n'inletzChar_slash=Zed_char.unsafe_of_char'\\'inletzChar_nl=Zed_char.unsafe_of_char'\n'inletrecloopofssize=ifofs>=eosthen(Zed_string.Buf.contentsbuf,size+1)elseletch,ofs=Zed_string.extract_nextlineofsinifZed_char.comparechzChar_slash=0thenifofs>=eosthen(Zed_string.Buf.add_zCharbufzChar_slash;(Zed_string.Buf.contentsbuf,size+3);)else(letnext,ofs_next=Zed_string.extract_nextlineofsinifZed_char.comparenextzChar_n=0then(Zed_string.Buf.add_zCharbufzChar_nl;loopofs_next(size+2);)elseifZed_char.comparenextzChar_slash=0then(Zed_string.Buf.add_zCharbufzChar_slash;loopofs_next(size+2);)else(Zed_string.Buf.add_zCharbufzChar_slash;loopofs(size+2);))else(Zed_string.Buf.add_zCharbufch;loopofs(size+Zed_char.sizech);)inloop00letsection=Lwt_log.Section.make"lambda-term(history)"letrecsafe_lockffnfdcmdofs=Lwt.catch(fun()->Lwt_unix.lockffdcmdofs>>=fun()->returntrue)(function|Unix.Unix_error(Unix.EINTR,_,_)->safe_lockffnfdcmdofs|Unix.Unix_error(error,_,_)->Lwt_log.ign_warning_f~section"failed to lock file '%s': %s"fn(Unix.error_messageerror);returnfalse|exn->Lwt.failexn)letopen_historyfn=Lwt.catch(fun()->Lwt_unix.openfilefn[Unix.O_RDWR]0>>=funfd->safe_lockffnfdLwt_unix.F_LOCK0>>=funlocked->return(Some(fd,locked)))(function|Unix.Unix_error(Unix.ENOENT,_,_)->returnNone|Unix.Unix_error(Unix.EACCES,_,_)->Lwt_log.ign_info_f"cannot open file '%s' in read and write mode: %s"fn(Unix.error_messageUnix.EACCES);(* If the file cannot be openned in read & write mode,
open it in read only mode but do not lock it. *)Lwt.catch(fun()->Lwt_unix.openfilefn[Unix.O_RDONLY]0>>=funfd->return(Some(fd,false)))(function|Unix.Unix_error(Unix.ENOENT,_,_)->returnNone|exn->Lwt.failexn)|exn->Lwt.failexn)letloadhistory?log?(skip_empty=true)?(skip_dup=true)fn=(* In case we do not load anything. *)history.old_count<-history.length;ifhistory.max_entries=0||history.max_size=0then(* Do not bother loading the file for nothing... *)return()elsebeginletlog=matchlogwith|Somefunc->func|None->funlinemsg->Lwt_log.ign_error_f~section"File %S, at line %d: %s"fnlinemsgin(* File opening. *)open_historyfn>>=funhistory_file->matchhistory_filewith|None->return()|Some(fd,locked)->(* File loading. *)letic=Lwt_io.of_fd~mode:Lwt_io.inputfdinLwt.finalize(fun()->letrecauxnum=Lwt_io.read_line_optic>>=funline->matchlinewith|None->return()|Someline->(tryletline=Zed_string.of_utf8lineinletentry,size=unescapelineinifnot(skip_empty&&is_emptyentry)&¬(skip_dup&&is_duphistoryentry)thenbeginadd_auxhistoryentrysize;history.old_count<-history.lengthendwith|Zed_string.Invalid(msg,_)->lognummsg|Zed_utf8.Invalid(msg,_)->lognummsg);aux(num+1)inaux1)(fun()->(* Cleanup. *)(iflockedthensafe_lockffnfdLwt_unix.F_ULOCK0elsereturntrue)>>=fun_->Lwt_unix.closefd)endletrecskip_nodesnodecount=ifcount=0thennodeelseskip_nodesnode.prev(count-1)letreccopyhistorymarkernodeskip_emptyskip_dup=ifnode!=markerthenbeginletline=escapenode.datainifnot(skip_empty&&is_emptyline)&¬(skip_dup&&is_duphistoryline)thenadd_auxhistorylinenode.size;copyhistorymarkernode.prevskip_emptyskip_dupendletrecdump_entriesocmarkernode=ifnode==markerthenreturn()elsebeginLwt_io.write_lineoc(Zed_string.to_utf8node.data)>>=fun()->dump_entriesocmarkernode.prevendletsavehistory?max_size?max_entries?(skip_empty=true)?(skip_dup=true)?(append=true)?(perm=0o666)fn=letmax_size=matchmax_sizewith|Somem->m|None->history.max_sizeandmax_entries=matchmax_entrieswith|Somem->m|None->history.max_entriesinlethistory_save=create~max_size~max_entries[]inifhistory_save.max_size=0||history_save.max_entries=0||(notappend&&history.old_count=history.length)then(* Just empty the history. *)Lwt_unix.openfilefn[Unix.O_CREAT;Unix.O_TRUNC]perm>>=Lwt_unix.closeelseifappend&&history.old_count=history.lengththen(* Do not touch the file. *)return()elsebeginLwt_unix.openfilefn[Unix.O_CREAT;Unix.O_RDWR]perm>>=funfd->(* Lock the entire file. *)safe_lockffnfdUnix.F_LOCK0>>=funlocked->Lwt.finalize(fun()->beginifappendthenbegin(* Load existing entries into [history_save].
We return the number of entries read. This may be greater
than the number of entries stored in [history_save]:
- because of limits
- because the history files contains duplicated lines
and/or empty lines and [skip_dup] and/or [skip_empty]
have been specified. *)letic=Lwt_io.of_fd~mode:Lwt_io.input~close:returnfdinletrecauxcount=Lwt_io.read_line_optic>>=funline->matchlinewith|None->history_save.old_count<-history_save.length;Lwt_io.closeic>>=fun()->returncount|Someline->letline=Zed_string.unsafe_of_utf8linein(* Do not bother unescaping. Tests remain the same
on the unescaped version. *)ifnot(skip_empty&&is_emptyline)&¬(skip_dup&&is_duphistory_saveline)thenadd_auxhistory_saveline(Zed_string.bytesline+1);aux(count+1)inaux0endelsereturn0end>>=funcount->letmarker=history.entries.previn(* Copy new entries into the saving history. *)copyhistory_savemarker(skip_nodesmarker.prevhistory.old_count)skip_emptyskip_dup;beginifappend&&history_save.old_count=countthen(* We are in append mode and no old entries were removed: do
not modify the file and append new entries at the end of
the file. *)returncountelse(* Otherwise truncate the file and save everything. *)Lwt_unix.lseekfd0Unix.SEEK_SET>>=fun_->Lwt_unix.ftruncatefd0>>=fun()->return0end>>=funto_skip->(* Save entries to the temporary file. *)letoc=Lwt_io.of_fd~mode:Lwt_io.output~close:returnfdinletmarker=history_save.entries.previndump_entriesocmarker(skip_nodesmarker.prevto_skip)>>=fun()->Lwt_io.closeoc>>=fun()->(* Done! *)history.old_count<-history.length;return())(fun()->(iflockedthensafe_lockffnfdLwt_unix.F_ULOCK0elsereturntrue)>>=fun_->Lwt_unix.closefd)end