···22-------------------------------------------------------------------------------
33v%%VERSION%%
4455-Fpath is an OCaml module for handling file system paths on POSIX and
66-Windows operating systems. Fpath processes paths without accessing the
77-file system and is independent from any system library.
55+Fpath is an OCaml module for handling file system paths with POSIX or
66+Windows conventions. Fpath processes paths without accessing the file
77+system and is independent from any system library.
8899Fpath depends on [Astring][astring] and is distributed under the ISC
1010license.
+230-266
src/fpath.ml
···2525let dir_sep_char = if windows then '\\' else '/'
2626let dir_sep = String.of_char dir_sep_char
2727let dir_sep_sub = String.sub dir_sep
2828+let not_dir_sep c = c <> dir_sep_char
28292930let dot = "."
3031let dot_sub = String.sub dot
···3536let dotdot_dir = dotdot ^ dir_sep
3637let dotdot_dir_sub = String.sub dotdot_dir
37383838-(* Structural preliminaries *)
3939+(* Platform specific preliminaties *)
39404040-let validate_and_collapse_seps p =
4141- (* collapse non-initial sequences of [dir_sep] to a single one and checks
4242- no null byte *)
4343- let max_idx = String.length p - 1 in
4444- let rec with_buf b last_sep k i = (* k is the write index in b *)
4545- if i > max_idx then Some (Bytes.sub_string b 0 k) else
4646- let c = string_unsafe_get p i in
4747- if c = '\x00' then None else
4848- if c <> dir_sep_char
4949- then (bytes_unsafe_set b k c; with_buf b false (k + 1) (i + 1)) else
5050- if not last_sep
5151- then (bytes_unsafe_set b k c; with_buf b true (k + 1) (i + 1)) else
5252- with_buf b true k (i + 1)
5353- in
5454- let rec try_no_alloc last_sep i =
5555- if i > max_idx then Some p else
5656- let c = string_unsafe_get p i in
5757- if c = '\x00' then None else
5858- if c <> dir_sep_char then try_no_alloc false (i + 1) else
5959- if not last_sep then try_no_alloc true (i + 1) else
6060- let b = Bytes.of_string p in (* copy and overwrite starting from i *)
6161- with_buf b true i (i + 1)
6262- in
6363- let start = (* Allow initial double sep *)
6464- if max_idx > 0 then (if p.[0] = dir_sep_char then 1 else 0) else 0
6565- in
6666- try_no_alloc false start
4141+module Windows = struct
67426868-let is_unc_path_windows p = String.is_prefix "\\\\" p
6969-let windows_non_unc_path_start_index p =
7070- match String.find (Char.equal ':') p with
4343+ let is_unc_path p = String.is_prefix "\\\\" p
4444+ let has_drive p = String.exists (Char.equal ':') p
4545+ let non_unc_path_start p = match String.find (Char.equal ':') p with
7146 | None -> 0
7247 | Some i -> i + 1 (* exists by construction *)
73487474-let parse_unc_windows s =
7575- (* parses an UNC path, the \\ prefix was already parsed, adds a root path
7676- if there's only a volume, UNC paths are always absolute. *)
7777- let p = String.sub ~start:2 s in
7878- let not_bslash c = c <> '\\' in
7979- let parse_seg p = String.Sub.span ~min:1 ~sat:not_bslash p in
8080- let ensure_root r = Some (if String.Sub.is_empty r then (s ^ "\\") else s) in
8181- match parse_seg p with
8282- | (seg1, _) when String.Sub.is_empty seg1 -> None (* \\ or \\\ *)
8383- | (seg1, rest) ->
8484- let seg1_len = String.Sub.length seg1 in
8585- match String.Sub.get_head ~rev:true seg1 with
8686- | '.' when seg1_len = 1 -> (* \\.\device\ *)
8787- begin match parse_seg (String.Sub.tail rest) with
8888- | (seg, _) when String.Sub.is_empty seg -> None
8989- | (_, rest) -> ensure_root rest
9090- end
9191- | '?' when seg1_len = 1 ->
9292- begin match parse_seg (String.Sub.tail rest) with
9393- | (seg2, _) when String.Sub.is_empty seg2 -> None
9494- | (seg2, rest) ->
9595- if (String.Sub.get_head ~rev:true seg2 = ':') (* \\?\drive:\ *)
9696- then (ensure_root rest) else
9797- if not (String.Sub.equal_bytes seg2 (String.sub "UNC"))
9898- then begin (* \\?\server\share\ *)
9999- match parse_seg (String.Sub.tail rest) with
100100- | (seg, _) when String.Sub.is_empty seg -> None
101101- | (_, rest) -> ensure_root rest
102102- end else begin (* \\?\UNC\server\share\ *)
103103- match parse_seg (String.Sub.tail rest) with
104104- | (seg, _) when String.Sub.is_empty seg -> None
105105- | (_, rest) ->
106106- match parse_seg (String.Sub.tail rest) with
107107- | (seg, _) when String.Sub.is_empty seg -> None
108108- | (_, rest) -> ensure_root rest
109109- end
110110- end
111111- | _ -> (* \\server\share\ *)
112112- begin match parse_seg (String.Sub.tail rest) with
113113- | (seg, _) when String.Sub.is_empty seg -> None
114114- | (_, rest) -> ensure_root rest
115115- end
4949+ let parse_unc s =
5050+ (* parses an UNC path, the \\ prefix was already parsed, adds a root path
5151+ if there's only a volume, UNC paths are always absolute. *)
5252+ let p = String.sub ~start:2 s in
5353+ let not_bslash c = c <> '\\' in
5454+ let parse_seg p = String.Sub.span ~min:1 ~sat:not_bslash p in
5555+ let ensure_root r = Some (if String.Sub.is_empty r then (s ^ "\\") else s)
5656+ in
5757+ match parse_seg p with
5858+ | (seg1, _) when String.Sub.is_empty seg1 -> None (* \\ or \\\ *)
5959+ | (seg1, rest) ->
6060+ let seg1_len = String.Sub.length seg1 in
6161+ match String.Sub.get_head ~rev:true seg1 with
6262+ | '.' when seg1_len = 1 -> (* \\.\device\ *)
6363+ begin match parse_seg (String.Sub.tail rest) with
6464+ | (seg, _) when String.Sub.is_empty seg -> None
6565+ | (_, rest) -> ensure_root rest
6666+ end
6767+ | '?' when seg1_len = 1 ->
6868+ begin match parse_seg (String.Sub.tail rest) with
6969+ | (seg2, _) when String.Sub.is_empty seg2 -> None
7070+ | (seg2, rest) ->
7171+ if (String.Sub.get_head ~rev:true seg2 = ':') (* \\?\drive:\ *)
7272+ then (ensure_root rest) else
7373+ if not (String.Sub.equal_bytes seg2 (String.sub "UNC"))
7474+ then begin (* \\?\server\share\ *)
7575+ match parse_seg (String.Sub.tail rest) with
7676+ | (seg, _) when String.Sub.is_empty seg -> None
7777+ | (_, rest) -> ensure_root rest
7878+ end else begin (* \\?\UNC\server\share\ *)
7979+ match parse_seg (String.Sub.tail rest) with
8080+ | (seg, _) when String.Sub.is_empty seg -> None
8181+ | (_, rest) ->
8282+ match parse_seg (String.Sub.tail rest) with
8383+ | (seg, _) when String.Sub.is_empty seg -> None
8484+ | (_, rest) -> ensure_root rest
8585+ end
8686+ end
8787+ | _ -> (* \\server\share\ *)
8888+ begin match parse_seg (String.Sub.tail rest) with
8989+ | (seg, _) when String.Sub.is_empty seg -> None
9090+ | (_, rest) -> ensure_root rest
9191+ end
11692117117-let sub_split_volume_windows p =
118118- (* splits a windows path into its volume (or drive) and actual file
119119- path. When called the path in [p] is guaranteed to be non empty
120120- and if [p] is an UNC path it is guaranteed to the be parseable by
121121- parse_unc_windows. *)
122122- let split_before i = String.sub p ~stop:i, String.sub p ~start:i in
123123- if not (is_unc_path_windows p) then
124124- begin match String.find (Char.equal ':') p with
125125- | None -> String.Sub.empty, String.sub p
126126- | Some i -> split_before (i + 1)
127127- end
128128- else
129129- let bslash ~start = match String.find ~start (Char.equal '\\') p with
130130- | None -> assert false | Some i -> i
131131- in
132132- let i = bslash ~start:2 in
133133- let j = bslash ~start:(i + 1) in
134134- match p.[i-1] with
135135- | '.' when i = 3 -> split_before j
136136- | '?' when i = 3 ->
137137- if p.[j-1] = ':' then split_before j else
138138- if (String.Sub.equal_bytes
139139- (String.sub p ~start:(i + 1) ~stop:j)
140140- (String.sub "UNC"))
141141- then split_before (bslash ~start:((bslash ~start:(j + 1)) + 1))
9393+ let sub_split_volume p =
9494+ (* splits a windows path into its volume (or drive) and actual file
9595+ path. When called the path in [p] is guaranteed to be non empty
9696+ and if [p] is an UNC path it is guaranteed to the be parseable by
9797+ parse_unc_windows. *)
9898+ let split_before i = String.sub p ~stop:i, String.sub p ~start:i in
9999+ if not (is_unc_path p) then
100100+ begin match String.find (Char.equal ':') p with
101101+ | None -> String.Sub.empty, String.sub p
102102+ | Some i -> split_before (i + 1)
103103+ end
104104+ else
105105+ let bslash ~start = match String.find ~start (Char.equal '\\') p with
106106+ | None -> assert false | Some i -> i
107107+ in
108108+ let i = bslash ~start:2 in
109109+ let j = bslash ~start:(i + 1) in
110110+ match p.[i-1] with
111111+ | '.' when i = 3 -> split_before j
112112+ | '?' when i = 3 ->
113113+ if p.[j-1] = ':' then split_before j else
114114+ if (String.Sub.equal_bytes
115115+ (String.sub p ~start:(i + 1) ~stop:j)
116116+ (String.sub "UNC"))
117117+ then split_before (bslash ~start:((bslash ~start:(j + 1)) + 1))
142118 else split_before (bslash ~start:(j + 1))
143143- | _ -> split_before j
119119+ | _ -> split_before j
144120145145-let is_root_posix p = String.equal p dir_sep || String.equal p "//"
146146-let is_root_windows p =
147147- let _, p = sub_split_volume_windows p in
148148- String.Sub.equal_bytes dir_sep_sub p
121121+ let is_root p =
122122+ let _, p = sub_split_volume p in
123123+ String.Sub.get_head p = dir_sep_char
124124+end
125125+126126+module Posix = struct
127127+ let has_volume p = String.is_prefix "//" p
128128+ let is_root p = String.equal p dir_sep || String.equal p "//"
129129+end
149130150131(* Segments *)
151132···159140160141let is_seg = if windows then is_seg_windows else is_seg_posix
161142162162-let not_dir_sep c = c <> dir_sep_char
163163-164143let _split_last_seg p = String.Sub.span ~rev:true ~sat:not_dir_sep p
165144let _sub_last_seg p = String.Sub.take ~rev:true ~sat:not_dir_sep p
166166-let sub_last_seg_windows p = _sub_last_seg (snd (sub_split_volume_windows p))
167167-let sub_last_seg_posix p = _sub_last_seg (String.sub p)
168168-let sub_last_seg = if windows then sub_last_seg_windows else sub_last_seg_posix
169169-170170-let _sub_last_non_empty_seg p = (* result is empty only roots *)
145145+let _sub_last_non_empty_seg p = (* returns empty on roots though *)
171146 let dir, last = _split_last_seg p in
172147 match String.Sub.is_empty last with
173148 | false -> last
174149 | true -> _sub_last_seg (String.Sub.tail ~rev:true dir)
175150151151+let _split_last_non_empty_seg p =
152152+ let (dir, last_seg as r) = _split_last_seg p in
153153+ match String.Sub.is_empty last_seg with
154154+ | false -> r, true
155155+ | true -> _split_last_seg (String.Sub.tail ~rev:true dir), false
156156+157157+let sub_last_seg_windows p = _sub_last_seg (snd (Windows.sub_split_volume p))
158158+let sub_last_seg_posix p = _sub_last_seg (String.sub p)
159159+let sub_last_seg = if windows then sub_last_seg_windows else sub_last_seg_posix
160160+176161let sub_last_non_empty_seg_windows p =
177177- _sub_last_non_empty_seg (snd (sub_split_volume_windows p))
162162+ _sub_last_non_empty_seg (snd (Windows.sub_split_volume p))
178163179164let sub_last_non_empty_seg_posix p =
180165 _sub_last_non_empty_seg (String.sub p)
···183168 if windows then sub_last_non_empty_seg_windows else
184169 sub_last_non_empty_seg_posix
185170186186-let _split_last_non_empty_seg p =
187187- let (dir, last_seg as r) = _split_last_seg p in
188188- match String.Sub.is_empty last_seg with
189189- | false -> r, true
190190- | true -> _split_last_seg (String.Sub.tail ~rev:true dir), false
191191-192192-let seg_is_rel = function "." | ".." -> true | _ -> false
193193-let sub_seg_is_rel seg =
171171+let is_rel_seg = function "." | ".." -> true | _ -> false
172172+let sub_is_rel_seg seg =
194173 String.Sub.(equal_bytes dot_sub seg || equal_bytes dotdot_sub seg)
195174175175+let segs_of_path p = String.cuts ~sep:dir_sep p
176176+let segs_to_path segs = String.concat ~sep:dir_sep segs
177177+196178(* File paths *)
197179198180type t = string (* N.B. a path is never "" or something is wrooong. *)
181181+182182+let validate_and_collapse_seps p =
183183+ (* collapse non-initial sequences of [dir_sep] to a single one and checks
184184+ no null byte *)
185185+ let max_idx = String.length p - 1 in
186186+ let rec with_buf b last_sep k i = (* k is the write index in b *)
187187+ if i > max_idx then Some (Bytes.sub_string b 0 k) else
188188+ let c = string_unsafe_get p i in
189189+ if c = '\x00' then None else
190190+ if c <> dir_sep_char
191191+ then (bytes_unsafe_set b k c; with_buf b false (k + 1) (i + 1)) else
192192+ if not last_sep
193193+ then (bytes_unsafe_set b k c; with_buf b true (k + 1) (i + 1)) else
194194+ with_buf b true k (i + 1)
195195+ in
196196+ let rec try_no_alloc last_sep i =
197197+ if i > max_idx then Some p else
198198+ let c = string_unsafe_get p i in
199199+ if c = '\x00' then None else
200200+ if c <> dir_sep_char then try_no_alloc false (i + 1) else
201201+ if not last_sep then try_no_alloc true (i + 1) else
202202+ let b = Bytes.of_string p in (* copy and overwrite starting from i *)
203203+ with_buf b true i (i + 1)
204204+ in
205205+ let start = (* Allow initial double sep for POSIX and UNC paths *)
206206+ if max_idx > 0 then (if p.[0] = dir_sep_char then 1 else 0) else 0
207207+ in
208208+ try_no_alloc false start
199209200210let of_string_windows p =
201211 if p = "" then None else
···203213 match validate_and_collapse_seps p with
204214 | None -> None
205215 | Some p as some ->
206206- if is_unc_path_windows p then parse_unc_windows p else
216216+ if Windows.is_unc_path p then Windows.parse_unc p else
207217 match String.find (Char.equal ':') p with
208218 | None -> some
209209- | Some i -> if i = String.length p - 1 then None else (Some p)
219219+ | Some i when i = String.length p - 1 -> None (* path is empty *)
220220+ | Some _ -> Some p
210221211222let of_string_posix p = if p = "" then None else validate_and_collapse_seps p
212223let of_string = if windows then of_string_windows else of_string_posix
···218229let add_seg p seg =
219230 if not (is_seg seg) then invalid_arg (err_invalid_seg seg);
220231 let sep = if p.[String.length p - 1] = dir_sep_char then "" else dir_sep in
221221- String.concat [p; sep; seg]
232232+ String.concat ~sep [p; seg]
222233223234let append_posix p0 p1 =
224235 if p1.[0] = dir_sep_char (* absolute *) then p1 else
225236 let sep = if p0.[String.length p0 - 1] = dir_sep_char then "" else dir_sep in
226226- String.concat [p0; sep; p1]
237237+ String.concat ~sep [p0; p1]
227238228239let append_windows p0 p1 =
229229- if is_unc_path_windows p1 then p1 else
230230- match String.find (Char.equal ':') p1 with
231231- | Some _ (* drive *) -> p1
232232- | None ->
233233- if p1.[0] = dir_sep_char then (* absolute *) p1 else
234234- let sep =
235235- if p0.[String.length p0 - 1] = dir_sep_char then "" else dir_sep
236236- in
237237- String.concat [p0; sep; p1]
240240+ if Windows.is_unc_path p1 || Windows.has_drive p1 then p1 else
241241+ if p1.[0] = dir_sep_char then (* absolute *) p1 else
242242+ let sep = if p0.[String.length p0 - 1] = dir_sep_char then "" else dir_sep in
243243+ String.concat ~sep [p0; p1]
238244239245let append = if windows then append_windows else append_posix
240246···242248let ( // ) = append
243249244250let split_volume_windows p =
245245- let vol, path = sub_split_volume_windows p in
251251+ let vol, path = Windows.sub_split_volume p in
246252 String.Sub.to_string vol, String.Sub.to_string path
247253248254let split_volume_posix p =
249249- if String.is_prefix "//" p then dir_sep, String.with_range ~first:1 p else
250250- "", p
255255+ if Posix.has_volume p then dir_sep, String.with_range ~first:1 p else "", p
251256252257let split_volume = if windows then split_volume_windows else split_volume_posix
253258254259let segs_windows p =
255255- let _, path = sub_split_volume_windows p in
256256- let path = String.Sub.to_string path in
257257- String.cuts ~sep:dir_sep path
260260+ let _, path = Windows.sub_split_volume p in
261261+ segs_of_path (String.Sub.to_string path)
258262259263let segs_posix p =
260260- let segs = String.cuts ~sep:dir_sep p in
261261- if String.is_prefix "//" p then List.tl segs else segs
264264+ let segs = segs_of_path p in
265265+ if Posix.has_volume p then List.tl segs else segs
262266263267let segs = if windows then segs_windows else segs_posix
264268···279283280284(* Base and parent paths *)
281285282282-let split_base_windows p =
283283- let vol, path = sub_split_volume_windows p in
284284- if String.Sub.equal_bytes dir_sep_sub path then (* root *) p, dot_dir else
285285- let dir, last_seg = _split_last_seg path in
286286+let sub_is_root p = String.Sub.get_head p = dir_sep_char
287287+288288+let _split_base p =
289289+ let dir, last_seg = _split_last_seg p in
286290 match String.Sub.is_empty dir with
287287- | true -> (* single seg *)
288288- String.Sub.base_string (String.Sub.append vol dot_dir_sub),
289289- String.Sub.to_string path
291291+ | true -> (* single seg *) dot_dir_sub, String.Sub.to_string p
290292 | false ->
291293 match String.Sub.is_empty last_seg with
292292- | false ->
293293- String.Sub.base_string (String.Sub.append vol dir),
294294- String.Sub.to_string last_seg
294294+ | false -> dir, String.Sub.to_string last_seg
295295 | true ->
296296 let dir_file = String.Sub.tail ~rev:true dir in
297297 let dir, dir_last_seg = _split_last_seg dir_file in
298298 match String.Sub.is_empty dir with
299299- | true ->
300300- String.Sub.base_string (String.Sub.append vol dot_dir_sub),
301301- String.Sub.to_string path
302302- | false ->
303303- String.Sub.base_string (String.Sub.append vol dir),
304304- String.Sub.to_string (String.Sub.extend dir_last_seg)
299299+ | true -> dot_dir_sub, String.Sub.to_string p
300300+ | false -> dir, String.Sub.(to_string (extend dir_last_seg))
301301+302302+let split_base_windows p =
303303+ let vol, path = Windows.sub_split_volume p in
304304+ if sub_is_root path then p, dot_dir else
305305+ let dir, b = _split_base path in
306306+ String.Sub.(base_string (append vol dir)), b
305307306308let split_base_posix p =
307307- if is_root_posix p then p, dot_dir else
308308- let dir, last_seg = _split_last_seg (String.sub p) in
309309- match String.Sub.is_empty dir with
310310- | true -> (* single seg *) dot_dir, p
311311- | false ->
312312- match String.Sub.is_empty last_seg with
313313- | false -> String.Sub.to_string dir, String.Sub.to_string last_seg
314314- | true ->
315315- let dir_file = String.Sub.tail ~rev:true dir in
316316- let dir, dir_last_seg = _split_last_seg dir_file in
317317- match String.Sub.is_empty dir with
318318- | true -> dot_dir, p
319319- | false ->
320320- String.Sub.to_string dir,
321321- String.Sub.to_string (String.Sub.extend dir_last_seg)
309309+ if Posix.is_root p then p, dot_dir else
310310+ let dir, b = _split_base (String.sub p) in
311311+ String.Sub.to_string dir, b
322312323313let split_base = if windows then split_base_windows else split_base_posix
324314325315let base p = snd (split_base p)
326316327327-let basename_windows p =
328328- let vol, path = sub_split_volume_windows p in
329329- if String.Sub.equal_bytes dir_sep_sub path then (* root *) "" else
330330- let basename =
331331- let dir, last_seg = _split_last_seg path in
332332- match String.Sub.is_empty dir with
333333- | true -> (* single seg *) String.Sub.to_string path
334334- | false ->
335335- match String.Sub.is_empty last_seg with
336336- | false -> String.Sub.to_string last_seg
337337- | true ->
338338- let dir_file = String.Sub.tail ~rev:true dir in
339339- let _, dir_last_seg = _split_last_seg dir_file in
340340- String.Sub.to_string dir_last_seg
341341- in
342342- match basename with "." | ".." -> "" | basename -> basename
317317+let _basename p = match String.Sub.to_string (_sub_last_non_empty_seg p) with
318318+| "." | ".." -> ""
319319+| basename -> basename
343320344344-let basename_posix p =
345345- if p = dir_sep || p = "//" then (* root *) "" else
346346- let basename =
347347- let dir, last_seg = _split_last_seg (String.sub p) in
348348- match String.Sub.is_empty dir with
349349- | true -> (* single seg *) p
350350- | false ->
351351- match String.Sub.is_empty last_seg with
352352- | false -> String.Sub.to_string last_seg
353353- | true ->
354354- let dir_file = String.Sub.tail ~rev:true dir in
355355- let _, dir_last_seg = _split_last_seg dir_file in
356356- String.Sub.to_string dir_last_seg
357357- in
358358- match basename with "." | ".." -> "" | basename -> basename
321321+let basename_windows p =
322322+ let vol, path = Windows.sub_split_volume p in
323323+ if sub_is_root path then "" else _basename path
359324325325+let basename_posix p = if Posix.is_root p then "" else _basename (String.sub p)
360326let basename p = if windows then basename_windows p else basename_posix p
361327362362-(* The parent algorithm is not very smart. It tries not to preserve
363363- the original path and avoids dealing with normalization. We simply
364364- remove everyting (i.e. a potential "") after the last non-empty,
365365- non-relative, path segment and if the resulting path is empty we
366366- return "./". If the last non-empty segment is "." or ".." we then
367367- simply postfix "../" *)
368368-369328let _parent p =
329329+ (* The parent algorithm is not very smart. It tries to preserve the
330330+ original path and avoids dealing with normalization. We simply
331331+ only keep everything before the last non-empty, non-relative,
332332+ path segment and if the resulting path is empty we return
333333+ "./". Otherwise if the last non-empty segment is "." or ".." we
334334+ simply postfix with "../" *)
370335 let (dir, seg), is_last = _split_last_non_empty_seg p in
371336 let dsep = if is_last then dir_sep_sub else String.Sub.empty in
372372- match String.Sub.is_empty dir with
373373- | true ->
374374- if sub_seg_is_rel seg then [p; dsep; dotdot_dir_sub] else [dot_dir_sub]
375375- | false ->
376376- if sub_seg_is_rel seg then [p; dsep; dotdot_dir_sub] else [dir]
337337+ if sub_is_rel_seg seg then [p; dsep; dotdot_dir_sub] else
338338+ if String.Sub.is_empty dir then [dot_dir_sub] else [dir]
377339378340let parent_windows p =
379379- let vol, path = sub_split_volume_windows p in
380380- if String.Sub.equal_bytes dir_sep_sub path then (* root *) p else
341341+ let vol, path = Windows.sub_split_volume p in
342342+ if sub_is_root path then p else
381343 String.Sub.(base_string @@ concat (vol :: _parent path))
382344383345let parent_posix p =
384384- if is_root_posix p then p else
346346+ if Posix.is_root p then p else
385347 String.Sub.(base_string @@ concat (_parent (String.sub p)))
386348387349let parent = if windows then parent_windows else parent_posix
···389351(* Normalization *)
390352391353let rem_empty_seg_windows p =
392392- let vol, path = sub_split_volume_windows p in
393393- if String.Sub.equal_bytes dir_sep_sub path then (* root *) p else
394394- let dir, last_seg = _split_last_seg path in
395395- if not (String.Sub.is_empty last_seg) then p else
396396- let p = String.Sub.tail ~rev:true dir in
397397- String.Sub.(base_string @@ concat [vol; p])
354354+ let vol, path = Windows.sub_split_volume p in
355355+ if sub_is_root path then p else
356356+ let max = String.Sub.length path - 1 in
357357+ if String.Sub.get path max <> dir_sep_char then p else
358358+ String.with_index_range p ~last:(max - 1)
398359399360let rem_empty_seg_posix p = match String.length p with
400361| 1 -> p
···410371let rem_empty_seg =
411372 if windows then rem_empty_seg_windows else rem_empty_seg_posix
412373413413-let normalize_rel_segs_rev segs =
374374+let normalize_rel_segs segs = (* result is non empty but may be [""] *)
414375 let rec loop acc = function
415376 | "." :: [] -> ("" :: acc) (* final "." remove but preserve directoryness. *)
416377 | "." :: rest -> loop acc rest
···426387 | [] ->
427388 match acc with
428389 | ".." :: _ -> ("" :: acc) (* normalize final .. to ../ *)
390390+ | [] -> [""]
429391 | acc -> acc
430392 in
431431- loop [] segs
393393+ List.rev (loop [] segs)
432394433395let normalize_segs = function
434396| "" :: segs -> (* absolute path *)
435435- let rec rem_dotdots = function
436436- | ".." :: segs -> rem_dotdots segs
437437- | [] -> [""]
438438- | segs -> segs
439439- in
440440- "" :: (rem_dotdots (List.rev (normalize_rel_segs_rev segs)))
397397+ let rec rem_dotdots = function ".." :: ss -> rem_dotdots ss | ss -> ss in
398398+ "" :: (rem_dotdots @@ normalize_rel_segs segs)
441399| segs ->
442442- match List.rev (normalize_rel_segs_rev segs) with
443443- | [] | [""] -> ["."; ""]
400400+ match normalize_rel_segs segs with
401401+ | [""] -> ["."; ""]
444402 | segs -> segs
445403446404let normalize_windows p =
447447- let vol, path = sub_split_volume_windows p in
405405+ let vol, path = Windows.sub_split_volume p in
448406 let path = String.Sub.to_string path in
449449- let segs = normalize_segs (String.cuts ~sep:dir_sep path) in
450450- let path = String.concat ~sep:dir_sep segs in
407407+ let path = segs_to_path @@ normalize_segs (segs_of_path path) in
451408 String.Sub.(to_string (concat [vol; String.sub path]))
452409453410let normalize_posix p =
454454- let segs = String.cuts ~sep:dir_sep p in
455455- let has_volume = String.is_prefix "//" p in
456456- let segs = normalize_segs (if has_volume then List.tl segs else segs) in
411411+ let has_volume = Posix.has_volume p in
412412+ let segs = segs_of_path p in
413413+ let segs = normalize_segs @@ if has_volume then List.tl segs else segs in
457414 let segs = if has_volume then "" :: segs else segs in
458458- String.concat ~sep:dir_sep segs
415415+ segs_to_path segs
459416460417let normalize = if windows then normalize_windows else normalize_posix
461418···469426 starts with a directory separator. *)
470427 let suff_start = String.length prefix in
471428 if prefix.[suff_start - 1] = dir_sep_char then true else
472472- if suff_start = String.length p then true else
429429+ if suff_start = String.length p then (* suffix empty *) true else
473430 p.[suff_start] = dir_sep_char
474431475475-let seg_prefix_last_index p0 p1 =
476476- (* Warning doesn't care about volumes *)
432432+let _prefix_last_index p0 p1 = (* last char index of segment-based prefix *)
477433 let l0 = String.length p0 in
478434 let l1 = String.length p1 in
479435 let p0, p1, max = if l0 < l1 then p0, p1, l0 - 1 else p1, p0, l1 - 1 in
···495451 in
496452 loop (-1) 0 p0 p1
497453498498-let find_prefix_windows p0 p1 = match seg_prefix_last_index p0 p1 with
454454+let find_prefix_windows p0 p1 = match _prefix_last_index p0 p1 with
499455| None -> None
500456| Some i ->
501501- let v0_len = String.Sub.length (fst (sub_split_volume_windows p0)) in
502502- let v1_len = String.Sub.length (fst (sub_split_volume_windows p1)) in
503503- let vmax = if v0_len > v1_len then v0_len else v1_len in
504504- if i < vmax then None else
505505- Some (String.with_index_range p0 ~last:i)
457457+ let v0_len = String.Sub.length (fst (Windows.sub_split_volume p0)) in
458458+ let v1_len = String.Sub.length (fst (Windows.sub_split_volume p1)) in
459459+ let max_vlen = if v0_len > v1_len then v0_len else v1_len in
460460+ if i < max_vlen then None else Some (String.with_index_range p0 ~last:i)
506461507507-let find_prefix_posix p0 p1 = match seg_prefix_last_index p0 p1 with
462462+let find_prefix_posix p0 p1 = match _prefix_last_index p0 p1 with
508463| None -> None
509509-| Some 0 when String.is_prefix "//" p0 || String.is_prefix "//" p1 -> None
464464+| Some 0 when Posix.has_volume p0 || Posix.has_volume p1 -> None
510465| Some i -> Some (String.with_index_range p0 ~last:i)
511466512467let find_prefix = if windows then find_prefix_windows else find_prefix_posix
···529484 let prooted = normalize (append root p) in
530485 if is_prefix nroot prooted then Some prooted else None
531486532532-let relativize ~root p =
487487+let _relativize ~root p =
533488 let root = (* root is always interpreted as a directory *)
534489 let root = normalize root in
535490 if root.[String.length root - 1] = dir_sep_char then root else
···548503 | [""], [""] ->
549504 (* walk ends at the end of both path simultaneously, [p] is a
550505 directory that matches exactly [root] expressed as a directory. *)
551551- Some ["."; ""]
506506+ Some (segs_to_path ["."; ""])
552507 | root, p ->
553508 (* walk ends here, either the next directory is different in
554509 [root] and [p] or it is equal but it is the last one for [p]
···559514 from the current position we just use [p] so prepending
560515 length root - 1 .. segments to [p] tells us how to go from
561516 the remaining root to [p]. *)
562562- Some (List.fold_left (fun acc _ -> dotdot :: acc) p (List.tl root))
517517+ let segs = List.fold_left (fun acc _ -> dotdot :: acc) p (List.tl root) in
518518+ Some (segs_to_path segs)
563519 in
564520 match segs root, segs p with
565521 | ("" :: _, s :: _)
···568524 None
569525 | ["."; ""], p ->
570526 (* p is relative and expressed w.r.t. "./", so it is itself. *)
571571- Some p
527527+ Some (segs_to_path p)
572528 | root, p ->
573529 (* walk in the segments of root and p until a segment mismatches.
574530 at that point express the remaining p relative to the remaining
···577533 final "" segment. *)
578534 walk root p
579535580580-let relativize ~root p = match relativize ~root p with
581581-| None -> None
582582-| Some segs -> Some (String.concat ~sep:dir_sep segs)
536536+let relativize_windows ~root p =
537537+ let rvol, root = Windows.sub_split_volume root in
538538+ let pvol, p = Windows.sub_split_volume p in
539539+ if not (String.Sub.equal_bytes rvol pvol) then None else
540540+ let root = String.Sub.to_string root in
541541+ let p = String.Sub.to_string p in
542542+ _relativize ~root p
543543+544544+let relativize_posix ~root p = _relativize ~root p
545545+546546+let relativize = if windows then relativize_windows else relativize_posix
583547584548(* Predicates and comparison *)
585549586550let is_rel_posix p = p.[0] <> dir_sep_char
587551let is_rel_windows p =
588588- if is_unc_path_windows p then false else
589589- p.[windows_non_unc_path_start_index p] <> dir_sep_char
552552+ if Windows.is_unc_path p then false else
553553+ p.[Windows.non_unc_path_start p] <> dir_sep_char
590554591555let is_rel = if windows then is_rel_windows else is_rel_posix
592556let is_abs p = not (is_rel p)
593593-let is_root = if windows then is_root_windows else is_root_posix
557557+let is_root = if windows then Windows.is_root else Posix.is_root
594558595559let is_current_dir_posix p = String.equal p dot || String.equal p dot_dir
596560let is_current_dir_windows p =
597597- if is_unc_path_windows p then false else
598598- let start = windows_non_unc_path_start_index p in
561561+ if Windows.is_unc_path p then false else
562562+ let start = Windows.non_unc_path_start p in
599563 match String.length p - start with
600564 | 1 -> p.[start] = '.'
601565 | 2 -> p.[start] = '.' && p.[start + 1] = dir_sep_char
···639603 if multi then multi_ext_sub seg else single_ext_sub seg
640604641605let get_ext ?multi p =
642642- String.Sub.to_string (ext_sub ?multi (sub_last_seg p))
606606+ String.Sub.to_string (ext_sub ?multi (sub_last_non_empty_seg p))
643607644608let has_ext e p =
645645- let seg = String.Sub.drop ~sat:eq_ext_sep (sub_last_seg p) in
609609+ let seg = String.Sub.drop ~sat:eq_ext_sep (sub_last_non_empty_seg p) in
646610 if not (String.Sub.is_suffix (String.sub e) seg) then false else
647611 if not (String.is_empty e) && e.[0] = ext_sep_char then true else
648612 (* check there's a dot before the suffix in [seg] *)
···651615 String.Sub.get seg dot_index = ext_sep_char
652616653617let ext_exists ?(multi = false) p =
654654- let ext = ext_sub ~multi (sub_last_seg p) in
618618+ let ext = ext_sub ~multi (sub_last_non_empty_seg p) in
655619 if not multi then not (String.Sub.is_empty ext) else
656620 if String.Sub.is_empty ext then false else
657621 match String.Sub.find ~rev:true eq_ext_sep ext with (* find another dot *)
+90-87
src/fpath.mli
···1919 distinguishes {e directory paths}
2020 (["a/b/"]) from {e file paths} (["a/b"]).}}
21212222+ The path segments ["."] and [".."] are {{!is_rel_seg}{e relative
2323+ path segments}} that respectively denote the current and parent
2424+ directory. The {{!basename}{e basename}} of a path is its last
2525+ non-empty segment if it is not a relative path segment and the empty
2626+ string otherwise.
2727+2228 Consult a few {{!tips}important tips}.
23292430 {b Note.} [Fpath] processes paths without accessing the file system.
···3440 ["/"] on POSIX and ["\\"] on Windows. *)
35413642val is_seg : string -> bool
3737-(** [is_seg s] is [true] iff [s] does not contain {!dir_sep} or a
3838- [0x00] byte. *)
4343+(** [is_seg s] is [true] iff [s] does not contain {!dir_sep} or ['/'] or
4444+ a [0x00] byte. *)
39454046val is_rel_seg : string -> bool
4147(** [is_rel_seg s] is true iff [s] is a relative segment, that is
···5056(** [v s] is the string [s] as path.
51575258 @raise Invalid_argument if [s] is not a {{!of_string}valid path}. Use
5353- {!of_string} to deal with foreign input and errors. *)
5959+ {!of_string} to deal with untrusted input and errors. *)
54605561val add_seg : t -> string -> t
5656-(** [add_seg p seg] adds [seg] at the end of [p]. If [seg] is [""]
5757- it is only added if [p] has no final empty segment. {{!ex_add_seg}Examples}.
6262+(** [add_seg p seg] adds [seg] at the end of [p] if [p]'s last segment
6363+ is non-empty and replaces it otherwise. {{!ex_add_seg}Examples}.
58645965 @raise Invalid_argument if {!is_seg}[ seg] is [false]. *)
6066···6672 {ul
6773 {- If [p'] is absolute or has a non-empty {{!split_volume}volume} then
6874 [p'] is returned.}
6969- {- Otherwise appends [p'] to [p] using a {!dir_sep} if needed.}}
7575+ {- Otherwise appends [p'] segments to [p] using {!add_seg}.}}
7076 {{!ex_append}Examples}. *)
71777278val ( // ) : t -> t -> t
···105111 {- [equal p (v @@ (fst @@ split_volume p) ^ (String.concat ~sep:dir_sep
106112 (segs p)))]}} *)
107113108108-(** {1:filedir File and directory paths} *)
114114+(** {1:filedir File and directory paths}
115115+116116+ {b Note.} The following properties are derived from the syntactic
117117+ semantics of paths which can be different from the one a file
118118+ system attributes to them. *)
109119110120val is_dir_path : t -> bool
111111-(** [is_dir_path p] is [true] iff [p] represents a directory. This means
112112- that [p]'s last segment is either [""], ["."] or [".."].
121121+(** [is_dir_path p] is [true] iff [p] represents a directory. This
122122+ means that [p]'s last segment is either [""], ["."] or [".."].
123123+ The property is invariant with respect to {{!normalize}normalization}.
113124 {{!ex_is_dir_path}Examples}. *)
114125115126val is_file_path : t -> bool
116127(** [is_file_path p] is [true] iff [p] represents a file. This is the
117128 negation of {!is_dir_path}. This means that [p]'s last segment is
118118- neither empty nor ["."], nor [".."]. {{!ex_is_file_path}Examples}. *)
129129+ neither empty nor ["."], nor [".."]. The property is invariant
130130+ with respect to {{!normalize}normalization}.
131131+ {{!ex_is_file_path}Examples}. *)
119132120133val to_dir_path : t -> t
121134(** [to_dir_path p] is {!add_seg}[ p ""] it ensure that the result
122122- represents a {{!is_dir_path}directory} (and, if converted to
123123- a string, that it will end with a {!dir_sep}).
135135+ represents a {{!is_dir_path}directory} and, if converted to a
136136+ string, that it ends with a {!dir_sep}.
124137 {{!ex_to_dir_path}Examples}. *)
125138126139val filename : t -> string
127140(** [filename p] is the file name of [p]. This is the last segment of
128141 [p] if [p] is a {{!is_file_path}file path} and the empty string
129129- otherwise. See also {!basename}. {{!ex_filename}Examples}. *)
142142+ otherwise. The result is invariant with respect to
143143+ {{!normalize}normalization}. See also
144144+ {!basename}. {{!ex_filename}Examples}. *)
130145131146(** {1:parentbase Base and parent paths} *)
132147···141156 {{!is_root}root path} there are no such segments and [b]
142157 is ["./"].}
143158 {- [d] is a {{!is_dir_path}directory} such that [d // b]
144144- represents the same path as [p] (they may however differ
145145- syntactically when converted to a string).}}
159159+ represents the same path as [p]. They may however differ
160160+ syntactically when converted to a string.}}
146161 {{!ex_split_base}Examples}.
147162148163 {b Note.} {{!normalize}Normalizing} [p] before using the function
149149- ensures that [b] will be a {{!is_rel_seg}relative segment} iff [p] cannot
164164+ ensures that [b] is a {{!is_rel_seg}relative segment} iff [p] cannot
150165 be named (like in ["."], ["../../"], ["/"], etc.). *)
151166152167val base : t -> t
153168(** [base p] is [snd (split_base p)]. *)
154169155170val basename : t -> string
156156-(** [basename p] is [p]'s last non-empty empty segment if
157157- {{!is_rel_seg}non-relative} or the empty string otherwise. The
158158- latter occurs on {{!is_root}root paths} and on paths whose last
159159- non-empty segment is relative. See also {!filename} and
171171+(** [basename p] is [p]'s last non-empty segment if non-relative or
172172+ the empty string otherwise. The latter occurs only on {{!is_root}root
173173+ paths} and on paths whose last non-empty segment is a
174174+ {{!is_rel_seg}relative segment}. See also {!filename} and
160175 {!base}. {{!ex_basename}Examples}.
161176162177 {b Note.} {{!normalize}Normalizing} [p] before using the function
···174189(** {1:norm Normalization} *)
175190176191val rem_empty_seg : t -> t
177177-(** [rem_empty_seg p] removes the empty segment of [p] if it
178178- exists and [p] is not a {{!is_root}root path}. This ensure that if
179179- [p] is converted to a string it will not have a trailing
180180- {!dir_sep} unless [p] is a root path. Note that this may affect
181181- [p]'s {{!is_dir_path}directoryness}.
182182- {{!ex_rem_empty_seg}Examples}. *)
192192+(** [rem_empty_seg p] removes the empty segment of [p] if it exists
193193+ and [p] is not a {{!is_root}root path}. This ensure that if [p] is
194194+ converted to a string it will not have a trailing {!dir_sep}
195195+ unless [p] is a root path. Note that this may affect [p]'s
196196+ {{!is_dir_path}directoryness}. {{!ex_rem_empty_seg}Examples}. *)
183197184198val normalize : t -> t
185199(** [normalize p] is a path that represents the same path as [p],
···197211198212(** {1:prefix Prefixes}
199213200200- {b Warning.} The {{!is_prefix}prefix property} between paths does
201201- not entail directory containement in general, as it is, by
202202- definition, a syntactic test. For example [is_prefix (v "..") (v
203203- "../..")] is [true], but the second path is not contained in the
204204- first one or [is_prefix (v "..") (v ".")] is [false]. However, on
205205- {{!normalize}normalized}, {{!is_abs}absolute} paths, the prefix relation
206206- does entail directory containement. See also {!rooted}. *)
214214+ {b Warning.} The syntactic {{!is_prefix}prefix relation} between
215215+ paths does not, in general, entail directory containement. The following
216216+ examples show this:
217217+{[
218218+is_prefix (v "..") (v "../..") = true
219219+is_prefix (v "..") (v ".") = false
220220+]}
221221+ However, on {{!normalize}normalized}, {{!is_abs}absolute} paths,
222222+ the prefix relation does entail directory containement. See also
223223+ {!is_rooted}. *)
207224208225val is_prefix : t -> t -> bool
209226(** [is_prefix prefix p] is [true] if [prefix] is a prefix of
···235252 prefix [prefix] and preserves [p]'s
236253 {{!is_dir_path}directoryness}. This means that [q] is a always
237254 {{!is_rel}relative} and that the path [prefix // q] and [p] represent the
238238- same paths (they may however differ syntactically when
239239- converted to a string).}}
255255+ same paths. They may however differ syntactically when
256256+ converted to a string.}}
240257 {{!ex_rem_prefix}Examples}. *)
241258242259(** {1 Roots and relativization} *)
···246263 {ul
247264 {- [Some q] if there exists a {{!is_relative}relative} path [q] such
248265 that [root // q] and [p] represent the same paths,
249249- {{!is_dir_path}directoryness} included (they may however differ
250250- syntactically when converted to a string).}
266266+ {{!is_dir_path}directoryness} included. They may however differ
267267+ syntactically when converted to a string.}
251268 {- [None] otherwise}}
252269253270 {{!ex_relativize}Examples.} *)
254271255255-(*
256256-257257-val is_rooted : root:t -> t -> bool
258258-(** [is_rooted root p] is [true] iff [p] is equal or contained in the
259259- directory represented by [root] (if [root] is a {{!is_file_path}file path},
260260- the path {!to_dir_path}[ root] is used instead).
261261- {{!ex_is_rooted}Examples.} *)
262262-263263-val rooted_append : ?normalized:bool -> root:t -> t -> t option
264264-(** [rooted_append ~root p] {{!appends}appends} [p] to [root] and
265265- returns a result iff [is_rooted root (append root t)] is [true].
266266- If [normalized] is [true] the result is normalized.
267267- {{!ex_rooted_append}Examples.} *)
268268-*)
269269-270270-(*
271271-272272-273273- the path [p] is contained in path [root].
274274- {ul
275275- {- [None] if [prefix]
276276- [is_prefix (normalize root) (normalize @@ append root p) = false].}
277277- {- [Some (normalize @@ append root p)] otherwise.}}
278278- In other words it ensures that an absolute path [p] or a relative
279279- path [p] expressed w.r.t. [root] expresses a path that is
280280- within the [root] file hierarchy. {{!ex_rooted}Examples}. *)
281281-282272(** {1:predicates Predicates and comparison} *)
283273284274val is_rel : t -> bool
···317307*)
318308319309val is_dotfile : t -> bool
320320-(** [is_dotfile p] is [true] iff [p]'s last non-empty segment is not
321321- ["."] or [".."] and starts with a ['.']. {{!ex_is_dotfile}Examples}.
310310+(** [is_dotfile p] is [true] iff [p]'s {{!basename}basename} is non
311311+ empty and starts with a ['.'].
322312323313 {b Warning.} By definition this is a syntactic test. For example it will
324314 return [false] on [".ssh/."]. {{!normalize}Normalizing} the
···345335(** [of_string s] is the string [s] as a path. [None] is returned if
346336 {ul
347337 {- [s] or the path following the {{!split_volume}volume} is empty ([""]),
348348- expect on Windows UNC paths, see below.}
338338+ except on Windows UNC paths, see below.}
349339 {- [s] has null byte (['\x00']).}
350340 {- On Windows, [s] is an invalid UNC path (e.g. ["\\\\"] or ["\\\\a"])}}
351341 The following transformations are performed on the string:
···373363 character. If there is no such occurence in the segment, the
374364 extension is empty. With these definitions, ["."], [".."],
375365 ["..."] and dot files like [".ocamlinit"] or ["..ocamlinit"] have
376376- no extension, but [".emacs.d"] and ["..emacs.d"] do have one. *)
366366+ no extension, but [".emacs.d"] and ["..emacs.d"] do have one.
367367+368368+ {b Warning.} The following functions act on paths whose
369369+ {{!basename}basename} is non empty and do nothing otherwise.
370370+ {{!normalize}Normalizing} [p] before using the functions ensures
371371+ that the functions do nothing iff [p] cannot be named (like in
372372+ ["."], ["../../"], ["/"], etc.). *)
377373378374type ext = string
379375(** The type for file extensions. *)
380376381377val get_ext : ?multi:bool -> t -> ext
382382-(** [get_ext p] is [p]'s last non-empty segment file extension or the empty
383383- string if there is no extension. If [multi] is [true] (defaults to
384384- [false]), returns the multiple file extension. {{!ex_get_ext}Examples}. *)
378378+(** [get_ext p] is [p]'s {{!basename}basename} file extension or the
379379+ empty string if there is no extension. If [multi] is [true]
380380+ (defaults to [false]), returns the multiple file
381381+ extension. {{!ex_get_ext}Examples}. *)
385382386383val has_ext : ext -> t -> bool
387384(** [has_ext e p] is [true] iff [ext p = e || ext ~multi:true p = e].
···389386 the test. {{!ex_has_ext}Examples}. *)
390387391388val ext_exists : ?multi:bool -> t -> bool
392392-(** [ext_exists ~multi p] is [true] iff [p]'s last segment has an
393393- extension. If [multi] is [true] (default to [false]) returns
394394- [true] iff [p] has {e more than one} extension.
389389+(** [ext_exists ~multi p] is [true] iff [p]'s last non-empty
390390+ segment has an extension. If [multi] is [true] (default to [false])
391391+ returns [true] iff [p] has {e more than one} extension.
395392 {{!ex_ext_exists}Examples}. *)
396393397394val add_ext : ext -> t -> t
···556553 a path}. This usually means that we don't care whether the path
557554 is a {{!is_file_path}file path} (e.g. ["a"]) or a
558555 {{!is_dir_path}directory path} (e.g. ["a/"]).}
559559- {- Windows accepts both ['\\'] and ['/'] as directory
560560- separator. However [Fpath] on Windows converts ['/'] to ['\\'] on
561561- the fly. Therefore you should either use ['/'] for defining
556556+ {- Windows accepts both ['\\'] and ['/'] as directory separator.
557557+ However [Fpath] on Windows converts ['/'] to ['\\'] on the
558558+ fly. Therefore you should either use ['/'] for defining
562559 constant paths you inject with {!v} or better, construct them
563563- directly with {!(/)}. {!to_string} will convert these paths
564564- to strings using the platform's specific directory
565565- separator {!dir_sep}.}
560560+ directly with {!(/)}. {!to_string} then converts paths to strings
561561+ using the platform's specific directory separator {!dir_sep}.}
566562 {- Avoid platform specific {{!split_volume}volumes} or hard-coding file
567563 hierarchy conventions in your constants.}
568564 {- Do not assume there is a single root path and that it is
569569- [/]. On Windows each {{!split_volume}volume} can have a root path.
570570- Use {!is_root} to detect root paths.}
565565+ ["/"]. On Windows each {{!split_volume}volume} can have a root path.
566566+ Use {!is_root} on {{!normalize}normalized} paths to detect roots.}
571567 {- Do not use {!to_string} to construct URIs, {!to_string} uses
572568 {!dir_sep} to separate segments, on Windows this is ['\\'] which
573573- is not what URIs expect. Access the path segments directly
574574- with {!segs}, note that you will need to percent encode these.}}
569569+ is not what URIs expect. Access path segments directly
570570+ with {!segs}; note that you will need to percent encode these.}}
575571576572 {1:ex Examples}
577573···926922927923 {2:ex_get_ext {!get_ext}}
928924 {ul
925925+ {- [get_ext (v "/") = ""]}
929926 {- [get_ext (v "/a/b") = ""]}
930930- {- [get_ext (v "a/.") = ""]}
931931- {- [get_ext (v "a/..") = ""]}
927927+ {- [get_ext (v "a.mli/.") = ""]}
928928+ {- [get_ext (v "a.mli/..") = ""]}
932929 {- [get_ext (v "a/.ocamlinit") = ""]}
930930+ {- [get_ext (v "a/.ocamlinit/") = ""]}
933931 {- [get_ext (v "/a/b.") = "."]}
934932 {- [get_ext (v "/a/b.mli") = ".mli"]}
935933 {- [get_ext (v "a.tar.gz") = ".gz"]}
936934 {- [get_ext (v "a/.emacs.d") = ".d"]}
935935+ {- [get_ext (v "a/.emacs.d/") = ".d"]}
937936 {- [get_ext ~multi:true (v "/a/b.mli") = ".mli"]}
938937 {- [get_ext ~multi:true (v "a.tar.gz") = ".tar.gz"]}
939939- {- [get_ext ~multi:true (v "a/.emacs.d") = ".d"]}}
938938+ {- [get_ext ~multi:true (v "a/.emacs.d") = ".d"]}
939939+ {- [get_ext ~multi:true (v "a/.emacs.d/") = ".d/"]}}
940940941941 {2:ex_has_ext {!has_ext}}
942942 {ul
943943 {- [has_ext ".mli" (v "a/b.mli") = true]}
944944 {- [has_ext "mli" (v "a/b.mli") = true]}
945945+ {- [has_ext "mli" (v "a/b.mli/") = true]}
945946 {- [has_ext "mli" (v "a/bmli") = false]}
946947 {- [has_ext ".tar.gz" (v "a/f.tar.gz") = true]}
947948 {- [has_ext "tar.gz" (v "a/f.tar.gz") = true]}
···953954 {- [ext_exists (v "a/f.") = true]}
954955 {- [ext_exists (v "a/f.gz") = true]}
955956 {- [ext_exists (v "a/f.tar.gz") = true]}
957957+ {- [ext_exists (v "a/f.tar.gz/") = true]}
956958 {- [ext_exists (v ".emacs.d") = true]}
959959+ {- [ext_exists (v ".emacs.d/") = true]}
957960 {- [ext_exists ~multi:true (v "a/f.gz") = false]}
958961 {- [ext_exists ~multi:true (v "a/f.tar.gz") = true]}
959962 {- [ext_exists ~multi:true (v ".emacs.d") = false]}}
+33-3
test/test_path.ml
···507507 eqp (Fpath.normalize @@ v "a/..b") (v "a/..b");
508508 eqp (Fpath.normalize @@ v "./a") (v "a");
509509 eqp (Fpath.normalize @@ v "../a") (v "../a");
510510+ eqp (Fpath.normalize @@ v "a/..") (v "./");
510511 eqp (Fpath.normalize @@ v "../../a") (v "../../a");
511512 eqp (Fpath.normalize @@ v "./a/..") (v "./");
512513 eqp (Fpath.normalize @@ v "/a/b/./..") (v "/a/");
···729730 relativize (v "../a") (v "../../b") (Some (v "../../b"));
730731 relativize (v "a") (v "../../b") (Some (v "../../../b"));
731732 relativize (v "a/c") (v "../../b") (Some (v "../../../../b"));
733733+ if windows then begin
734734+ relativize (v "C:a\\c") (v "C:..\\..\\b") (Some (v "..\\..\\..\\..\\b"));
735735+ relativize (v "C:a\\c") (v "..\\..\\b") None;
736736+ relativize (v "\\\\?\\UNC\\server\\share\\a\\b\\c")
737737+ (v "\\\\?\\UNC\\server\\share\\d\\e\\f") (Some (v "../../../d/e/f"));
738738+ end;
732739 ()
733740734741let is_abs_rel = test "Fpath.is_abs_rel" @@ fun () ->
···828835 ()
829836830837let get_ext = test "Fpath.get_ext" @@ fun () ->
838838+ eq_str (Fpath.get_ext @@ v "/") "";
831839 eq_str (Fpath.get_ext @@ v ".") "";
832840 eq_str (Fpath.get_ext @@ v "..") "";
833841 eq_str (Fpath.get_ext @@ v "...") "";
···839847 eq_str (Fpath.get_ext @@ v ".a...") ".";
840848 eq_str (Fpath.get_ext @@ v ".a....") ".";
841849 eq_str (Fpath.get_ext @@ v "a/...") "";
842842- eq_str (Fpath.get_ext @@ v "a/.") "";
843843- eq_str (Fpath.get_ext @@ v "a/..") "";
850850+ eq_str (Fpath.get_ext @@ v "a.mli/.") "";
851851+ eq_str (Fpath.get_ext @@ v "a.mli/..") "";
844852 eq_str (Fpath.get_ext @@ v "a/.a") "";
845853 eq_str (Fpath.get_ext @@ v "a/..b") "";
846854 eq_str (Fpath.get_ext @@ v "a/..b.a") ".a";
855855+ eq_str (Fpath.get_ext @@ v "a/..b.a/") ".a";
847856 eq_str (Fpath.get_ext @@ v "a/..b..ac") ".ac";
857857+ eq_str (Fpath.get_ext @@ v "a/..b..ac/") ".ac";
848858 eq_str (Fpath.get_ext @@ v "/a/b") "";
849859 eq_str (Fpath.get_ext @@ v "/a/b.") ".";
860860+ eq_str (Fpath.get_ext @@ v "/a/b./") ".";
850861 eq_str (Fpath.get_ext @@ v "a/.ocamlinit") "";
862862+ eq_str (Fpath.get_ext @@ v "a/.ocamlinit/") "";
851863 eq_str (Fpath.get_ext @@ v "a/.emacs.d") ".d";
864864+ eq_str (Fpath.get_ext @@ v "a/.emacs.d/") ".d";
852865 eq_str (Fpath.get_ext @@ v "/a/b.mli") ".mli";
866866+ eq_str (Fpath.get_ext @@ v "/a/b.mli/") ".mli";
853867 eq_str (Fpath.get_ext @@ v "a.tar.gz") ".gz";
868868+ eq_str (Fpath.get_ext @@ v "a.tar.gz/") ".gz";
854869 eq_str (Fpath.get_ext @@ v "./a.") ".";
870870+ eq_str (Fpath.get_ext @@ v "./a./") ".";
855871 eq_str (Fpath.get_ext @@ v "./a..") ".";
872872+ eq_str (Fpath.get_ext @@ v "./a../") ".";
856873 eq_str (Fpath.get_ext @@ v "./.a.") ".";
857857- eq_str (Fpath.get_ext @@ v "./.a..") ".";
874874+ eq_str (Fpath.get_ext @@ v "./.a../") ".";
858875 eq_str (Fpath.get_ext ~multi:true @@ v ".") "";
859876 eq_str (Fpath.get_ext ~multi:true @@ v "..") "";
860877 eq_str (Fpath.get_ext ~multi:true @@ v "...") "";
···862879 eq_str (Fpath.get_ext ~multi:true @@ v ".....") "";
863880 eq_str (Fpath.get_ext ~multi:true @@ v ".a") "";
864881 eq_str (Fpath.get_ext ~multi:true @@ v ".a.") ".";
882882+ eq_str (Fpath.get_ext ~multi:true @@ v ".a./") ".";
865883 eq_str (Fpath.get_ext ~multi:true @@ v ".a..") "..";
866884 eq_str (Fpath.get_ext ~multi:true @@ v ".a...") "...";
867885 eq_str (Fpath.get_ext ~multi:true @@ v ".a....") "....";
···870888 eq_str (Fpath.get_ext ~multi:true @@ v "a/..") "";
871889 eq_str (Fpath.get_ext ~multi:true @@ v "a/..b") "";
872890 eq_str (Fpath.get_ext ~multi:true @@ v "a/..b.a") ".a";
891891+ eq_str (Fpath.get_ext ~multi:true @@ v "a/..b.a/") ".a";
873892 eq_str (Fpath.get_ext ~multi:true @@ v "a/..b..ac") "..ac";
893893+ eq_str (Fpath.get_ext ~multi:true @@ v "a/..b..ac/") "..ac";
874894 eq_str (Fpath.get_ext ~multi:true @@ v "a/.emacs.d") ".d";
895895+ eq_str (Fpath.get_ext ~multi:true @@ v "a/.emacs.d/") ".d";
875896 eq_str (Fpath.get_ext ~multi:true @@ v "/a/b.mli") ".mli";
897897+ eq_str (Fpath.get_ext ~multi:true @@ v "/a/b.mli/") ".mli";
876898 eq_str (Fpath.get_ext ~multi:true @@ v "a.tar.gz") ".tar.gz";
899899+ eq_str (Fpath.get_ext ~multi:true @@ v "a.tar.gz/") ".tar.gz";
877900 eq_str (Fpath.get_ext ~multi:true @@ v "./a.") ".";
901901+ eq_str (Fpath.get_ext ~multi:true @@ v "./a./") ".";
878902 eq_str (Fpath.get_ext ~multi:true @@ v "./a..") "..";
903903+ eq_str (Fpath.get_ext ~multi:true @@ v "./a../") "..";
879904 eq_str (Fpath.get_ext ~multi:true @@ v "./.a.") ".";
905905+ eq_str (Fpath.get_ext ~multi:true @@ v "./.a./") ".";
880906 eq_str (Fpath.get_ext ~multi:true @@ v "./.a..") "..";
907907+ eq_str (Fpath.get_ext ~multi:true @@ v "./.a../") "..";
881908 ()
882909883910let has_ext = test "Fpath.has_ext" @@ fun () ->
···905932 eq_bool (Fpath.has_ext "..." @@ v "....") false;
906933 eq_bool (Fpath.has_ext "..." @@ v ".a...") true;
907934 eq_bool (Fpath.has_ext ".mli" @@ v "a/b.mli") true;
935935+ eq_bool (Fpath.has_ext ".mli" @@ v "a/b.mli/") true;
908936 eq_bool (Fpath.has_ext "mli" @@ v "a/b.mli") true;
909937 eq_bool (Fpath.has_ext "mli" @@ v "a/bmli") false;
910938 eq_bool (Fpath.has_ext "mli" @@ v "a/.mli") false;
911939 eq_bool (Fpath.has_ext ".tar.gz" @@ v "a/f.tar.gz") true;
912940 eq_bool (Fpath.has_ext "tar.gz" @@ v "a/f.tar.gz") true;
941941+ eq_bool (Fpath.has_ext "tar.gz" @@ v "a/f.tar.gz/") true;
913942 eq_bool (Fpath.has_ext "tar.gz" @@ v "a/ftar.gz") false;
914943 eq_bool (Fpath.has_ext "tar.gz" @@ v "a/tar.gz") false;
915944 eq_bool (Fpath.has_ext "tar.gz" @@ v "a/.tar.gz") false;
916945 eq_bool (Fpath.has_ext ".tar" @@ v "a/f.tar.gz") false;
917946 eq_bool (Fpath.has_ext ".ocamlinit" @@ v ".ocamlinit") false;
947947+ eq_bool (Fpath.has_ext ".ocamlinit/" @@ v ".ocamlinit") false;
918948 eq_bool (Fpath.has_ext ".ocamlinit" @@ v "..ocamlinit") false;
919949 eq_bool (Fpath.has_ext "..ocamlinit" @@ v "...ocamlinit") false;
920950 eq_bool (Fpath.has_ext "..ocamlinit" @@ v ".a..ocamlinit") true;