123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781(*---------------------------------------------------------------------------
Copyright (c) 2015 The fpath programmers. All rights reserved.
Distributed under the ISC license, see terms at the end of the file.
fpath v0.7.3
---------------------------------------------------------------------------*)openAstring(* Unsafe string and byte manipulations. If you don't believe the
author's invariants, replacing with safe versions makes everything
safe in the library. He won't be upset. *)letbytes_unsafe_set=Bytes.unsafe_setletstring_unsafe_get=String.unsafe_get(* Errors *)leterr_invalid_segs=strf"%a: invalid segment"String.dumpsleterr_invalid_exts=strf"%a: invalid extension"String.dumps(* A few useful constants *)letwindows=Sys.os_type="Win32"letdir_sep_char=ifwindowsthen'\\'else'/'letdir_sep=String.of_chardir_sep_charletdir_sep_sub=String.subdir_sepletnot_dir_sepc=c<>dir_sep_charletdot="."letdot_dir=dot^dir_sepletdot_dir_sub=String.subdot_dirletdotdot=".."letdotdot_dir=dotdot^dir_sepletdotdot_dir_sub=String.subdotdot_dir(* Platform specific preliminaties *)moduleWindows=structletis_unc_pathp=String.is_prefix"\\\\"plethas_drivep=String.exists(Char.equal':')pletnon_unc_path_startp=matchString.find(Char.equal':')pwith|None->0|Somei->i+1(* exists by construction *)letparse_uncs=(* parses an UNC path, the \\ prefix was already parsed, adds a root path
if there's only a volume, UNC paths are always absolute. *)letp=String.sub~start:2sinletnot_bslashc=c<>'\\'inletparse_segp=String.Sub.span~min:1~sat:not_bslashpinletensure_rootr=Some(ifString.Sub.is_emptyrthen(s^"\\")elses)inmatchparse_segpwith|(seg1,_)whenString.Sub.is_emptyseg1->None(* \\ or \\\ *)|(seg1,rest)->letseg1_len=String.Sub.lengthseg1inmatchString.Sub.get_head~rev:trueseg1with|'.'whenseg1_len=1->(* \\.\device\ *)beginmatchparse_seg(String.Sub.tailrest)with|(seg,_)whenString.Sub.is_emptyseg->None|(_,rest)->ensure_rootrestend|'?'whenseg1_len=1->beginmatchparse_seg(String.Sub.tailrest)with|(seg2,_)whenString.Sub.is_emptyseg2->None|(seg2,rest)->if(String.Sub.get_head~rev:trueseg2=':')(* \\?\drive:\ *)then(ensure_rootrest)elseifnot(String.Sub.equal_bytesseg2(String.sub"UNC"))thenbegin(* \\?\server\share\ *)matchparse_seg(String.Sub.tailrest)with|(seg,_)whenString.Sub.is_emptyseg->None|(_,rest)->ensure_rootrestendelsebegin(* \\?\UNC\server\share\ *)matchparse_seg(String.Sub.tailrest)with|(seg,_)whenString.Sub.is_emptyseg->None|(_,rest)->matchparse_seg(String.Sub.tailrest)with|(seg,_)whenString.Sub.is_emptyseg->None|(_,rest)->ensure_rootrestendend|_->(* \\server\share\ *)beginmatchparse_seg(String.Sub.tailrest)with|(seg,_)whenString.Sub.is_emptyseg->None|(_,rest)->ensure_rootrestendletsub_split_volumep=(* splits a windows path into its volume (or drive) and actual file
path. When called the path in [p] is guaranteed to be non empty
and if [p] is an UNC path it is guaranteed to the be parseable by
parse_unc_windows. *)letsplit_beforei=String.subp~stop:i,String.subp~start:iinifnot(is_unc_pathp)thenbeginmatchString.find(Char.equal':')pwith|None->String.Sub.empty,String.subp|Somei->split_before(i+1)endelseletbslash~start=matchString.find~start(Char.equal'\\')pwith|None->assertfalse|Somei->iinleti=bslash~start:2inletj=bslash~start:(i+1)inmatchp.[i-1]with|'.'wheni=3->split_beforej|'?'wheni=3->ifp.[j-1]=':'thensplit_beforejelseif(String.Sub.equal_bytes(String.subp~start:(i+1)~stop:j)(String.sub"UNC"))thensplit_before(bslash~start:((bslash~start:(j+1))+1))elsesplit_before(bslash~start:(j+1))|_->split_beforejletis_rootp=let_,path=sub_split_volumepinString.Sub.lengthpath=1&&String.Sub.getpath0=dir_sep_charendmodulePosix=structlethas_volumep=String.is_prefix"//"pletis_rootp=String.equalpdir_sep||String.equalp"//"end(* Segments *)letis_seg_windowss=letvalidc=c<>'\x00'&&c<>dir_sep_char&&c<>'/'inString.for_allvalidsletis_seg_posixs=letvalidc=c<>'\x00'&&c<>dir_sep_charinString.for_allvalidsletis_seg=ifwindowsthenis_seg_windowselseis_seg_posixlet_split_last_segp=String.Sub.span~rev:true~sat:not_dir_sepplet_sub_last_segp=String.Sub.take~rev:true~sat:not_dir_sepplet_sub_last_non_empty_segp=(* returns empty on roots though *)letdir,last=_split_last_segpinmatchString.Sub.is_emptylastwith|false->last|true->_sub_last_seg(String.Sub.tail~rev:truedir)let_split_last_non_empty_segp=let(dir,last_segasr)=_split_last_segpinmatchString.Sub.is_emptylast_segwith|false->r,true|true->_split_last_seg(String.Sub.tail~rev:truedir),falseletsub_last_seg_windowsp=_sub_last_seg(snd(Windows.sub_split_volumep))letsub_last_seg_posixp=_sub_last_seg(String.subp)letsub_last_seg=ifwindowsthensub_last_seg_windowselsesub_last_seg_posixletsub_last_non_empty_seg_windowsp=_sub_last_non_empty_seg(snd(Windows.sub_split_volumep))letsub_last_non_empty_seg_posixp=_sub_last_non_empty_seg(String.subp)letsub_last_non_empty_seg=ifwindowsthensub_last_non_empty_seg_windowselsesub_last_non_empty_seg_posixletis_rel_seg=function"."|".."->true|_->falseletsub_is_rel_segseg=matchString.Sub.lengthsegwith|1whenString.Sub.getseg0='.'->true|2whenString.Sub.getseg0='.'&&String.Sub.getseg1='.'->true|_->falseletsub_is_dir_segseg=matchString.Sub.lengthsegwith|0->true|1whenString.Sub.getseg0='.'->true|2whenString.Sub.getseg0='.'&&String.Sub.getseg1='.'->true|_->falseletsegs_of_pathp=String.cuts~sep:dir_seppletsegs_to_pathsegs=String.concat~sep:dir_sepsegs(* File paths *)typet=string(* N.B. a path is never "" or something is wrooong. *)leterrs=Error(`Msg(strf"%a: invalid path"String.dumps))letvalidate_and_collapse_sepsp=(* collapse non-initial sequences of [dir_sep] to a single one and checks
no null byte *)letmax_idx=String.lengthp-1inletrecwith_bufblast_sepki=(* k is the write index in b *)ifi>max_idxthenOk(Bytes.sub_stringb0k)elseletc=string_unsafe_getpiinifc='\x00'thenerrpelseifc<>dir_sep_charthen(bytes_unsafe_setbkc;with_bufbfalse(k+1)(i+1))elseifnotlast_septhen(bytes_unsafe_setbkc;with_bufbtrue(k+1)(i+1))elsewith_bufbtruek(i+1)inletrectry_no_alloclast_sepi=ifi>max_idxthenOkpelseletc=string_unsafe_getpiinifc='\x00'thenerrpelseifc<>dir_sep_charthentry_no_allocfalse(i+1)elseifnotlast_septhentry_no_alloctrue(i+1)elseletb=Bytes.of_stringpin(* copy and overwrite starting from i *)with_bufbtruei(i+1)inletstart=(* Allow initial double sep for POSIX and UNC paths *)ifmax_idx>0then(ifp.[0]=dir_sep_charthen1else0)else0intry_no_allocfalsestartletof_string_windowss=ifs=""thenerrselseletp=String.map(func->ifc='/'then'\\'elsec)sinmatchvalidate_and_collapse_sepspwith|Error_ase->e|Okpassome->ifWindows.is_unc_pathpthen(matchWindows.parse_uncpwithNone->errs|Somep->Okp)elsematchString.find(Char.equal':')pwith|None->some|Someiwheni=String.lengthp-1->errp(* path is empty *)|Some_->Okpletof_string_posixp=ifp=""thenerrpelsevalidate_and_collapse_sepspletof_string=ifwindowsthenof_string_windowselseof_string_posixletvs=matchof_stringswith|Okp->p|Error(`Msgm)->invalid_argmletadd_segpseg=ifnot(is_segseg)theninvalid_arg(err_invalid_segseg);letsep=ifp.[String.lengthp-1]=dir_sep_charthen""elsedir_sepinString.concat~sep[p;seg]letappend_posixp0p1=ifp1.[0]=dir_sep_char(* absolute *)thenp1elseletsep=ifp0.[String.lengthp0-1]=dir_sep_charthen""elsedir_sepinString.concat~sep[p0;p1]letappend_windowsp0p1=ifWindows.is_unc_pathp1||Windows.has_drivep1thenp1elseifp1.[0]=dir_sep_charthen(* absolute *)p1elseletsep=ifp0.[String.lengthp0-1]=dir_sep_charthen""elsedir_sepinString.concat~sep[p0;p1]letappend=ifwindowsthenappend_windowselseappend_posixlet(/)=add_seglet(//)=appendletsplit_volume_windowsp=letvol,path=Windows.sub_split_volumepinString.Sub.to_stringvol,String.Sub.to_stringpathletsplit_volume_posixp=ifPosix.has_volumepthendir_sep,String.with_range~first:1pelse"",pletsplit_volume=ifwindowsthensplit_volume_windowselsesplit_volume_posixletsegs_windowsp=let_,path=Windows.sub_split_volumepinsegs_of_path(String.Sub.to_stringpath)letsegs_posixp=letsegs=segs_of_pathpinifPosix.has_volumepthenList.tlsegselsesegsletsegs=ifwindowsthensegs_windowselsesegs_posix(* File and directory paths *)letis_dir_pathp=sub_is_dir_seg(sub_last_segp)letis_file_pathp=not(is_dir_pathp)letto_dir_pathp=add_segp""letfilenamep=matchString.Sub.to_string(sub_last_segp)with|""|"."|".."->""|filename->filename(* Base and parent paths *)letsub_is_rootp=String.Sub.lengthp=1&&String.Sub.getp0=dir_sep_charlet_split_basep=letdir,last_seg=_split_last_segpinmatchString.Sub.is_emptydirwith|true->(* single seg *)dot_dir_sub,String.Sub.to_stringp|false->matchString.Sub.is_emptylast_segwith|false->dir,String.Sub.to_stringlast_seg|true->letdir_file=String.Sub.tail~rev:truedirinletdir,dir_last_seg=_split_last_segdir_fileinmatchString.Sub.is_emptydirwith|true->dot_dir_sub,String.Sub.to_stringp|false->dir,String.Sub.(to_string(extenddir_last_seg))letsplit_base_windowsp=letvol,path=Windows.sub_split_volumepinifsub_is_rootpaththenp,dot_direlseletdir,b=_split_basepathinString.Sub.(base_string(appendvoldir)),bletsplit_base_posixp=ifPosix.is_rootpthenp,dot_direlseletdir,b=_split_base(String.subp)inString.Sub.to_stringdir,bletsplit_base=ifwindowsthensplit_base_windowselsesplit_base_posixletbasep=snd(split_basep)let_basenamep=matchString.Sub.to_string(_sub_last_non_empty_segp)with|"."|".."->""|basename->basenameletbasename_windowsp=letvol,path=Windows.sub_split_volumepinifsub_is_rootpaththen""else_basenamepathletbasename_posixp=ifPosix.is_rootpthen""else_basename(String.subp)letbasenamep=ifwindowsthenbasename_windowspelsebasename_posixplet_parentp=(* The parent algorithm is not very smart. It tries to preserve the
original path and avoids dealing with normalization. We simply
only keep everything before the last non-empty, non-relative,
path segment and if the resulting path is empty we return
"./". Otherwise if the last non-empty segment is "." or ".." we
simply postfix with "../" *)let(dir,seg),is_last=_split_last_non_empty_segpinletdsep=ifis_lastthendir_sep_subelseString.Sub.emptyinifsub_is_rel_segsegthen[p;dsep;dotdot_dir_sub]elseifString.Sub.is_emptydirthen[dot_dir_sub]else[dir]letparent_windowsp=letvol,path=Windows.sub_split_volumepinifsub_is_rootpaththenpelseString.Sub.(base_string@@concat(vol::_parentpath))letparent_posixp=ifPosix.is_rootpthenpelseString.Sub.(base_string@@concat(_parent(String.subp)))letparent=ifwindowsthenparent_windowselseparent_posix(* Normalization *)letrem_empty_seg_windowsp=letvol,path=Windows.sub_split_volumepinifsub_is_rootpaththenpelseletmax=String.Sub.stop_pospath-1inifString.getpmax<>dir_sep_charthenpelseString.with_index_rangep~last:(max-1)letrem_empty_seg_posixp=matchString.lengthpwith|1->p|2->ifp.[0]<>dir_sep_char&&p.[1]=dir_sep_charthenString.of_charp.[0]elsep|len->letmax=len-1inifp.[max]<>dir_sep_charthenpelseString.with_index_rangep~last:(max-1)letrem_empty_seg=ifwindowsthenrem_empty_seg_windowselserem_empty_seg_posixletnormalize_rel_segssegs=(* result is non empty but may be [""] *)letrecloopacc=function|"."::[]->(""::acc)(* final "." remove but preserve directoryness. *)|"."::rest->loopaccrest|".."::rest->beginmatchaccwith|".."::_|[]->loop(".."::acc)rest|seg::acc->(* N.B. seg can't be "." *)matchrestwith|[]->(""::acc)(* preserve directoryness *)|rest->loopaccrestend|seg::rest->loop(seg::acc)rest|[]->matchaccwith|".."::_->(""::acc)(* normalize final .. to ../ *)|[]->[""]|acc->accinList.rev(loop[]segs)letnormalize_segs=function|""::segs->(* absolute path *)letrecrem_dotdots=function".."::ss->rem_dotdotsss|ss->ssin""::(rem_dotdots@@normalize_rel_segssegs)|segs->matchnormalize_rel_segssegswith|[""]->[".";""]|segs->segsletnormalize_windowsp=letvol,path=Windows.sub_split_volumepinletpath=String.Sub.to_stringpathinletpath=segs_to_path@@normalize_segs(segs_of_pathpath)inString.Sub.(to_string(concat[vol;String.subpath]))letnormalize_posixp=lethas_volume=Posix.has_volumepinletsegs=segs_of_pathpinletsegs=normalize_segs@@ifhas_volumethenList.tlsegselsesegsinletsegs=ifhas_volumethen""::segselsesegsinsegs_to_pathsegsletnormalize=ifwindowsthennormalize_windowselsenormalize_posix(* Prefixes *)letis_prefixprefixp=ifnot(String.is_prefixprefixp)thenfalseelse(* Further check the prefix is segment-based. If [prefix] ends with a
dir_sep_char nothing more needs to be checked. If it doesn't we need
to check that [p]'s remaining suffix is either empty or
starts with a directory separator. *)letsuff_start=String.lengthprefixinifprefix.[suff_start-1]=dir_sep_charthentrueelseifsuff_start=String.lengthpthen(* suffix empty *)trueelsep.[suff_start]=dir_sep_charlet_prefix_last_indexp0p1=(* last char index of segment-based prefix *)letl0=String.lengthp0inletl1=String.lengthp1inletp0,p1,max=ifl0<l1thenp0,p1,l0-1elsep1,p0,l1-1inletreclooplast_dir_sepip0p1=matchi>max||p0.[i]<>p1.[i]with|false->letlast_dir_sep=ifp0.[i]=dir_sep_charthenielselast_dir_sepinlooplast_dir_sep(i+1)p0p1|true->ifi=0thenNoneelseletlast=i-1iniflast_dir_sep=lastthenSomelastelsematchlast=maxwith|true->ifl1=l0thenSomelastelseifp1.[i]=dir_sep_charthenSomelastelseiflast_dir_sep<>-1thenSomelast_dir_sepelseNone|false->iflast_dir_sep<>-1thenSomelast_dir_sepelseNoneinloop(-1)0p0p1letfind_prefix_windowsp0p1=match_prefix_last_indexp0p1with|None->None|Somei->letv0_len=String.Sub.length(fst(Windows.sub_split_volumep0))inletv1_len=String.Sub.length(fst(Windows.sub_split_volumep1))inletmax_vlen=ifv0_len>v1_lenthenv0_lenelsev1_leninifi<max_vlenthenNoneelseSome(String.with_index_rangep0~last:i)letfind_prefix_posixp0p1=match_prefix_last_indexp0p1with|None->None|Some0whenPosix.has_volumep0||Posix.has_volumep1->None|Somei->Some(String.with_index_rangep0~last:i)letfind_prefix=ifwindowsthenfind_prefix_windowselsefind_prefix_posixletrem_prefixprefixp=matchis_prefixprefixpwith|false->None|true->matchString.lengthprefixwith|lenwhenlen=String.lengthp->None|len->letfirst=ifp.[len]=dir_sep_charthenlen+1elseleninmatchString.with_index_rangep~firstwith|""->Somedot_dir|q->Someq(* Roots and relativization *)let_relativize~rootp=letroot=(* root is always interpreted as a directory *)letroot=normalizerootinifroot.[String.lengthroot-1]=dir_sep_charthenrootelseroot^dir_sepinletp=normalizepinletrecwalkrootp=matchroot,pwith|(".."::_,s::_)whens<>".."->(* [root] has too many up segments. Cannot walk down to express [p],
e.g. "../a" can't be expressed relative to "../../". *)None|(sr::root,sp::(_::_asp))whensr=sp->(* the next directory in [root] and [p] match and it's not the last
segment of [p], walk to next segment *)walkrootp|[""],[""]->(* walk ends at the end of both path simultaneously, [p] is a
directory that matches exactly [root] interpreted as a directory. *)Some(segs_to_path[".";""])|root,p->(* walk ends here, either the next directory is different in
[root] and [p] or it is equal but it is the last one for [p]
and different from [""] (i.e. [p] is a file path and prefix
of [root]). To get to the current position from the remaining
root we need to go up the number of non-empty segments that
remain in [root] (length root - 1). To get to the path [p]
from the current position we just use [p] so prepending
length root - 1 ".." segments to [p] tells us how to go from
the remaining root to [p]. *)letsegs=List.fold_left(funacc_->dotdot::acc)p(List.tlroot)inSome(segs_to_pathsegs)inmatchsegsroot,segspwith|(""::_,s::_)whens<>""->None(* absolute/relative mismatch *)|(s::_,""::_)whens<>""->None(* absolute/relative mismatch *)|[".";""],p->(* p is relative and must be expressed w.r.t. "./", so it is itself. *)Some(segs_to_pathp)|root,p->(* walk in the segments of root and p until a segment mismatches.
at that point express the remaining p relative to the remaining
root. Note that because of normalization both [root] and [p] may
only have initial .. segments and [root] by construction has a
final "" segment. *)walkrootpletrelativize_windows~rootp=letrvol,root=Windows.sub_split_volumerootinletpvol,p=Windows.sub_split_volumepinifnot(String.Sub.equal_bytesrvolpvol)thenNoneelseletroot=String.Sub.to_stringrootinletp=String.Sub.to_stringpin_relativize~rootpletrelativize_posix~rootp=_relativize~rootpletrelativize=ifwindowsthenrelativize_windowselserelativize_posixletis_rooted~rootp=matchrelativize~rootpwith|None->false|Somer->not(String.equaldotdotr||String.is_prefixdotdot_dirr)(* Predicates and comparison *)letis_rel_posixp=p.[0]<>dir_sep_charletis_rel_windowsp=ifWindows.is_unc_pathpthenfalseelsep.[Windows.non_unc_path_startp]<>dir_sep_charletis_rel=ifwindowsthenis_rel_windowselseis_rel_posixletis_absp=not(is_relp)letis_root=ifwindowsthenWindows.is_rootelsePosix.is_rootletis_current_dir_posix?(prefix=false)p=matchprefixwith|false->String.equaldotp||String.equaldot_dirp|true->String.equaldotp||String.is_prefixdot_dirpletis_current_dir_windows?(prefix=false)p=ifWindows.is_unc_pathpthenfalseelseletstart=Windows.non_unc_path_startpinmatchString.lengthp-startwith|1->p.[start]='.'|nwhenn=2||prefix->p.[start]='.'&&p.[start+1]=dir_sep_char|_->falseletis_current_dir=ifwindowsthenis_current_dir_windowselseis_current_dir_posixletis_parent_dir_posix?(prefix=false)p=matchprefixwith|false->String.equaldotdotp||String.equaldotdot_dirp|true->String.equaldotdotp||String.is_prefixdotdot_dirpletis_parent_dir_windows?(prefix=false)p=ifWindows.is_unc_pathpthenfalseelseletstart=Windows.non_unc_path_startpinmatchString.lengthp-startwith|1->false|2->p.[start]='.'&&p.[start+1]='.'|nwhenn=3||prefix->p.[start]='.'&&p.[start+1]='.'&&p.[start+2]=dir_sep_char|_->falseletis_parent_dir=ifwindowsthenis_parent_dir_windowselseis_parent_dir_posixletis_dotfilep=matchbasenamepwith|""->false|s->s.[0]='.'letequal=String.equalletcompare=String.compare(* Conversions and pretty printing *)letto_stringp=pletppppfp=Format.pp_print_stringppf(to_stringp)letdumpppfp=String.dumpppf(to_stringp)(* File extensions *)typeext=stringletext_sep_char='.'letext_sep=String.of_charext_sep_charletext_sep_sub=String.Sub.of_charext_sep_charleteq_ext_sepc=c=ext_sep_charletneq_ext_sepc=c<>ext_sep_charletrecsub_multi_extseg=letfirst_not_sep=String.Sub.drop~sat:eq_ext_sepseginString.Sub.drop~sat:neq_ext_sepfirst_not_sepletsub_single_extseg=letname_dot,ext=String.Sub.span~rev:true~sat:neq_ext_sepseginifString.Sub.existsneq_ext_sepname_dotthenString.Sub.extend~max:1~rev:trueextelseString.Sub.emptyletsub_ext?(multi=false)seg=ifmultithensub_multi_extsegelsesub_single_extsegletsub_get_ext?multip=sub_ext?multi(sub_last_non_empty_segp)letget_ext?multip=String.Sub.to_string(sub_get_ext?multip)lethas_extep=letext=sub_get_ext~multi:truepinifString.Sub.is_emptyextthenfalseelseifnot(String.(Sub.is_suffix(sube)ext))thenfalseelseifnot(String.is_emptye)&&e.[0]=ext_sep_charthentrueelse(* Check there's a dot before the suffix [e] in [ext] *)letdot_index=String.Sub.lengthext-String.lengthe-1inString.Sub.getextdot_index=ext_sep_charletmem_extextsp=List.exists(funext->has_extextp)extsletexists_ext?(multi=false)p=letext=sub_get_ext~multipinifmultithenString.Sub.existseq_ext_sep(String.Sub.tailext)elsenot(String.Sub.is_emptyext)letadd_extep=ifString.is_emptyethenpelseifnot(is_sege)theninvalid_arg(err_invalid_exte)elseletseg=sub_last_non_empty_segpinifsub_is_dir_segsegthenpelselete_has_dot=e.[0]=ext_sep_charinletmaybe_dot=ife_has_dotthenString.Sub.emptyelseext_sep_subinlethas_empty=p.[String.lengthp-1]=dir_sep_charinletmaybe_empty=ifhas_emptythendir_sep_subelseString.Sub.emptyinletseg_end=String.Sub.stop_posseg-1inletprefix=String.sub_with_index_range~last:seg_endpinletpath=[prefix;maybe_dot;String.sube;maybe_empty]inString.Sub.(base_string(concatpath))let_split_ext?multip=letext=sub_get_ext?multipinifString.Sub.is_emptyextthenp,extelseletbefore_ext=String.Sub.start_posext-1inifString.Sub.stop_posext=String.lengthpthenString.with_index_rangep~last:before_ext,extelseletprefix=String.sub_with_index_rangep~last:before_extinString.Sub.(base_string(concat[prefix;dir_sep_sub])),extletrem_ext?multip=fst(_split_ext?multip)letset_ext?multiep=add_exte(rem_ext?multip)letsplit_ext?multip=letp,ext=_split_ext?multipinp,String.Sub.to_stringextlet(+)pe=add_exteplet(-+)pe=set_extep(* Path sets and maps *)typepath=tmoduleSet=structincludeSet.Make(String)letpp?sep:(pp_sep=Format.pp_print_cut)pp_eltppfps=letpp_elteltis_first=ifis_firstthen()elsepp_sepppf();Format.fprintfppf"%a"pp_eltelt;falseinignore(foldpp_eltpstrue)letdump_path=dumpletdumpppfss=letpp_elteltis_first=ifis_firstthen()elseFormat.fprintfppf"@ ";Format.fprintfppf"%a"dump_pathelt;falseinFormat.fprintfppf"@[<1>{";ignore(foldpp_eltsstrue);Format.fprintfppf"}@]";()leterr_empty()=invalid_arg"empty set"leterr_absentpps=invalid_arg(strf"%a not in set %a"dump_pathpdumpps)letget_min_eltps=trymin_eltpswithNot_found->err_empty()letmin_eltps=trySome(min_eltps)withNot_found->Noneletget_max_eltps=trymax_eltpswithNot_found->err_empty()letmax_eltps=trySome(max_eltps)withNot_found->Noneletget_any_eltps=trychoosepswithNot_found->err_empty()letchooseps=trySome(chooseps)withNot_found->Noneletgetpps=tryfindppswithNot_found->err_absentppsletfindpps=trySome(findpps)withNot_found->Noneletof_list=List.fold_left(funaccs->addsacc)emptyendmoduleMap=structincludeMap.Make(String)leterr_empty()=invalid_arg"empty map"leterr_absents=invalid_arg(strf"%s is not bound in map"s)letget_min_bindingm=trymin_bindingmwithNot_found->err_empty()letmin_bindingm=trySome(min_bindingm)withNot_found->Noneletget_max_bindingm=trymax_bindingmwithNot_found->err_empty()letmax_bindingm=trySome(max_bindingm)withNot_found->Noneletget_any_bindingm=trychoosemwithNot_found->err_empty()letchoosem=trySome(choosem)withNot_found->Noneletgetks=tryfindkswithNot_found->err_absentkletfindkm=trySome(findkm)withNot_found->Noneletdomm=fold(funk_acc->Set.addkacc)mSet.emptyletof_listbs=List.fold_left(funm(k,v)->addkvm)emptybsletpp?sep:(pp_sep=Format.pp_print_cut)pp_bindingppf(m:'at)=letpp_bindingkvis_first=ifis_firstthen()elsepp_sepppf();pp_bindingppf(k,v);falseinignore(foldpp_bindingmtrue)letdumppp_vppfm=letpp_bindingkvis_first=ifis_firstthen()elseFormat.fprintfppf"@ ";Format.fprintfppf"@[<1>(@[%a@],@ @[%a@])@]"dumpkpp_vv;falseinFormat.fprintfppf"@[<1>{";ignore(foldpp_bindingmtrue);Format.fprintfppf"}@]";()endtypeset=Set.ttype'amap='aMap.t(*---------------------------------------------------------------------------
Copyright (c) 2015 The fpath programmers
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---------------------------------------------------------------------------*)