ocaml-containers/src/core/CCIO.ml
Simon Cruanes 58ac755f82
chore: move to ounit2
see if that fixes the windows build.
2022-06-09 21:39:09 -04:00

440 lines
10 KiB
OCaml

(* This file is free software, part of containers. See file "license" for more details. *)
(** {1 IO Utils} *)
type 'a iter = ('a -> unit) -> unit
type 'a or_error = ('a, string) result
type 'a gen = unit -> 'a option
let gen_empty () = None
let gen_singleton x =
let done_ = ref false in
fun () -> if !done_ then None else (done_ := true; Some x)
let gen_filter_map f gen =
(* tailrec *)
let rec next () =
match gen() with
| None -> None
| Some x ->
match f x with
| None -> next()
| (Some _) as res -> res
in next
let gen_of_array arr =
let r = ref 0 in
fun () ->
if !r = Array.length arr then None
else (
let x = arr.(!r) in
incr r;
Some x
)
let rec gen_iter f g = match g() with
| None -> ()
| Some x -> f x; gen_iter f g
let gen_flat_map f next_elem =
let state = ref `Init in
let rec next() =
match !state with
| `Init -> get_next_gen()
| `Run gen ->
begin match gen () with
| None -> get_next_gen ()
| (Some _) as x -> x
end
| `Stop -> None
and get_next_gen() = match next_elem() with
| None -> state:=`Stop; None
| Some x ->
try state := `Run (f x); next()
with e -> state := `Stop; raise e
in
next
type 'a seq_of_gen_state_ =
| Of_gen_thunk of 'a gen
| Of_gen_saved of 'a Seq.node
let seq_of_gen_ g =
let rec consume r () = match !r with
| Of_gen_saved cons -> cons
| Of_gen_thunk g ->
begin match g() with
| None ->
r := Of_gen_saved Seq.Nil;
Nil
| Some x ->
let tl = consume (ref (Of_gen_thunk g)) in
let l = Seq.Cons (x, tl) in
r := Of_gen_saved l;
l
end
in
consume (ref (Of_gen_thunk g))
let finally_ f x ~h =
try
let res = f x in
h x;
res
with e ->
h x;
raise e
let with_in ?(mode=0o644) ?(flags=[Open_text]) filename f =
let ic = open_in_gen (Open_rdonly::flags) mode filename in
finally_ f ic ~h:close_in
let read_chunks_gen ?(size=1024) ic =
let buf = Bytes.create size in
let next() =
let n = input ic buf 0 size in
if n = 0
then None
else Some (Bytes.sub_string buf 0 n)
in
next
let read_chunks_iter ?size ic =
let g = read_chunks_gen ?size ic in
fun yield -> gen_iter yield g
let read_chunks_seq ?size ic =
seq_of_gen_ (read_chunks_gen ?size ic)
let read_line ic =
try Some (input_line ic)
with End_of_file -> None
let read_lines_gen ic =
let stop = ref false in
fun () ->
if !stop then None
else try Some (input_line ic)
with End_of_file -> (stop:=true; None)
let read_lines_seq ic =
seq_of_gen_ (read_lines_gen ic)
let read_lines_iter ic =
let g = read_lines_gen ic in
fun yield -> gen_iter yield g
let read_lines_l ic =
let l = ref [] in
try
while true do
l := input_line ic :: !l
done;
assert false
with End_of_file ->
List.rev !l
(* thanks to nicoo for this trick *)
type _ ret_type =
| Ret_string : string ret_type
| Ret_bytes : Bytes.t ret_type
let read_all_
: type a. op:a ret_type -> size:int -> in_channel -> a
= fun ~op ~size ic ->
let buf = ref (Bytes.create size) in
let len = ref 0 in
try
while true do
(* resize *)
if !len = Bytes.length !buf then (
buf := Bytes.extend !buf 0 !len;
);
assert (Bytes.length !buf > !len);
let n = input ic !buf !len (Bytes.length !buf - !len) in
len := !len + n;
if n = 0 then raise Exit; (* exhausted *)
done;
assert false (* never reached*)
with Exit ->
match op with
| Ret_string -> Bytes.sub_string !buf 0 !len
| Ret_bytes -> Bytes.sub !buf 0 !len
let read_all_bytes ?(size=1024) ic = read_all_ ~op:Ret_bytes ~size ic
let read_all ?(size=1024) ic = read_all_ ~op:Ret_string ~size ic
(*$R
let s = String.make 200 'y' in
let s = Printf.sprintf "a\nb\n %s\nlast line\n" s in
File.with_temp ~prefix:"test_containers" ~suffix:""
(fun name ->
with_out name @@ fun oc ->
output_string oc s;
flush oc;
let s' = with_in name read_all in
OUnit2.assert_equal ~printer:(fun s->s) s s'
)
*)
let with_out ?(mode=0o644) ?(flags=[Open_creat; Open_trunc; Open_text]) filename f =
let oc = open_out_gen (Open_wronly::flags) mode filename in
finally_ f oc ~h:close_out
let with_out_a ?mode ?(flags=[]) filename f =
with_out ?mode ~flags:(Open_wronly::Open_creat::Open_append::flags) filename f
let write_line oc s =
output_string oc s;
output_char oc '\n'
let write_gen ?(sep="") oc g =
let rec recurse g = match g() with
| None -> ()
| Some s ->
output_string oc sep;
output_string oc s;
recurse g
in match g() with
| None -> ()
| Some s ->
output_string oc s;
recurse g
let write_seq ?(sep="") oc seq : unit =
let rec recurse g = match g() with
| Seq.Nil -> ()
| Seq.Cons(s,seq) ->
output_string oc sep;
output_string oc s;
recurse seq
in match seq() with
| Seq.Nil -> ()
| Seq.Cons(s,seq) ->
output_string oc s;
recurse seq
let rec write_lines oc g = match g () with
| None -> ()
| Some l ->
write_line oc l;
write_lines oc g
let write_lines_seq oc seq =
Seq.iter (write_line oc) seq
let write_lines_iter oc i = i (write_line oc)
let write_lines_l oc l =
List.iter (write_line oc) l
(* test {read,write}_lines. Need to concatenate the lists because some
strings in the random input might contain '\n' themselves *)
(*$QR
Q.(list_of_size Gen.(0 -- 40) printable_string) (fun l ->
let l' = ref [] in
File.with_temp ~prefix:"test_containers" ~suffix:""
(fun name ->
with_out name @@ fun oc ->
write_lines_l oc l;
flush oc;
l' := with_in name read_lines_l;
);
String.concat "\n" l = String.concat "\n" !l'
)
*)
(*$QR
Q.(list_of_size Gen.(0 -- 40) printable_string) (fun l ->
let l' = ref [] in
File.with_temp ~prefix:"test_containers" ~suffix:""
(fun name ->
with_out name @@ fun oc ->
write_lines oc (Gen.of_list l);
flush oc;
l' := with_in name (fun ic -> read_lines_gen ic |> Gen.to_list);
);
String.concat "\n" l = String.concat "\n" !l'
)
*)
let with_in_out ?(mode=0o644) ?(flags=[Open_creat]) filename f =
let ic = open_in_gen (Open_rdonly::flags) mode filename in
let oc = open_out_gen (Open_wronly::flags) mode filename in
try
let x = f ic oc in
close_out oc; (* must be first?! *)
close_in ic;
x
with e ->
close_out_noerr oc;
close_in_noerr ic;
raise e
let copy_into ?(bufsize=4_096) ic oc : unit =
let buf = Bytes.create bufsize in
let cont = ref true in
while !cont do
let n = input ic buf 0 bufsize in
if n > 0 then (
output oc buf 0 n;
) else (
cont := false
)
done
(*$QR
Q.(list_of_size Gen.(0 -- 40) printable_string) (fun l ->
let s = ref "" in
File.with_temp ~prefix:"test_containers1" ~suffix:""
(fun name1 ->
with_out name1 @@ fun oc1 ->
write_gen ~sep:"" oc1 (Gen.of_list l);
flush oc1;
File.with_temp ~prefix:"test_containers2" ~suffix:""
(fun name2 ->
with_out name2 @@ fun oc2 ->
CCIO.with_in name1 (fun ic1 -> copy_into ic1 oc2);
flush oc2;
s := with_in name2 read_all;);
);
String.concat "" l = !s
)
*)
let tee funs g () = match g() with
| None -> None
| Some x as res ->
List.iter (fun f -> f x) funs;
res
(* TODO: lines/unlines: string gen -> string gen *)
(* TODO: words: string gen -> string gen,
with a state machine that goes:
- 0: read input chunk
- switch to "search for ' '", and yield word
- goto 0 if no ' ' found
- yield leftover when g returns Stop
*)
module File = struct
type t = string
let to_string f = f
let make f =
if Filename.is_relative f
then Filename.concat (Sys.getcwd()) f
else f
let exists f = Sys.file_exists f
let is_directory f = Sys.is_directory f
let remove_exn f = Sys.remove f
let remove f =
try Ok (Sys.remove f)
with exn ->
Error (Printexc.to_string exn)
let read_exn f = with_in f (read_all_ ~op:Ret_string ~size:4096)
let read f =
try Ok (read_exn f) with e -> Error (Printexc.to_string e)
let append_exn f x =
with_out ~flags:[Open_append; Open_creat; Open_text] f
(fun oc -> output_string oc x; flush oc)
let append f x =
try Ok (append_exn f x) with e -> Error (Printexc.to_string e)
let write_exn f x =
with_out f
(fun oc -> output_string oc x; flush oc)
let write f x =
try Ok (write_exn f x) with e -> Error (Printexc.to_string e)
let remove_noerr f = try Sys.remove f with _ -> ()
let read_dir_base d =
if Sys.is_directory d
then
let arr = Sys.readdir d in
gen_of_array arr
else fun () -> None
let cons_ x tl =
let first=ref true in
fun () ->
if !first then (
first := false;
Some x
) else tl ()
let rec walk d =
if not (Sys.file_exists d) then gen_empty
else if Sys.is_directory d
then (
(* try to list the directory *)
let arr = try Sys.readdir d with Sys_error _ -> [||] in
let tail = gen_of_array arr in
let tail = gen_flat_map
(fun s -> walk (Filename.concat d s))
tail
in cons_ (`Dir,d) tail
)
else gen_singleton (`File, d)
(*$R
OUnit2.assert_bool "walk categorizes files"
(File.walk "."
|> Gen.for_all
(function
| `File, f -> not (Sys.is_directory f)
| `Dir, f -> Sys.is_directory f
)
)
*)
let walk_seq d = seq_of_gen_ (walk d)
let walk_iter d = fun yield -> gen_iter yield (walk d)
let walk_l d =
let l = ref [] in
let g = walk d in
let rec aux () = match g() with
| None -> !l
| Some x -> l := x :: !l; aux ()
in aux ()
type walk_item = [`File | `Dir] * t
let read_dir ?(recurse=false) d =
if recurse
then
gen_filter_map
(function
| `File, f -> Some f
| `Dir, _ -> None)
(walk d)
else read_dir_base d
let show_walk_item ((i,f):walk_item) =
(match i with
| `File -> "file:"
| `Dir -> "dir:"
) ^ f
let with_temp ?temp_dir ~prefix ~suffix f =
let name = Filename.temp_file ?temp_dir prefix suffix in
finally_ f name ~h:remove_noerr
end