From 0b9de82c3aaebe6e0692894f9f7e596698c5130f Mon Sep 17 00:00:00 2001 From: Paolo Donadeo Date: Fri, 15 May 2026 23:12:51 +0200 Subject: [PATCH] feat: support multiple input files - Accept multiple positional arguments instead of a single required file - Extract `read_file` helper to separate file reading from parsing - Collect all valid reminders across files before printing - Handle per-file errors gracefully without aborting the whole run --- bin/commandLine.ml | 11 ++++--- bin/main.ml | 75 +++++++++++++++++++++++++++++----------------- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/bin/commandLine.ml b/bin/commandLine.ml index a68b368..896d49c 100644 --- a/bin/commandLine.ml +++ b/bin/commandLine.ml @@ -1,17 +1,16 @@ open Cmdliner open Cmdliner.Term.Syntax -let ical_file = - let doc = "TODO" in - let docv = "ICAL" in - Arg.(required & pos ~rev:true 0 (some string) None & info [] ~docv ~doc) +let files = + let doc = "Files to process" in + Arg.(non_empty & pos_all string [] & info [] ~docv:"FILE" ~doc) let main_command f = let doc = "Convert iCalendar files to remind format" in let man = [] in Cmd.make (Cmd.info "ical2rem" ~version:"%%VERSION%%" ~doc ~man) @@ - let+ ical_file = ical_file in - f ical_file + let+ files = files in + f files let main f = Cmd.eval @@ main_command f diff --git a/bin/main.ml b/bin/main.ml index 75cedfd..17ec544 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -7,38 +7,57 @@ module Map = MoreLabels.Map.Make (String) (e.g., due to updates or recurring events or cancellations). *) -let ical2rem ical_file = - let ic = open_in ical_file in +let read_file filename = + let ic = open_in filename in let n = in_channel_length ic in let s = Bytes.create n in really_input ic s 0 n; close_in ic; - let cal_or_error = Icalendar.parse (Bytes.unsafe_to_string s) in - match cal_or_error with - | Error e -> - if e = ": not enough input" then - exit 0 (* This is a common error when the file is empty, so we treat it as a non-error case *) - else prerr_endline ("Error parsing iCalendar file: " ^ e) - | Ok (_, components) -> begin - let events_map : Icalendar.event list Map.t = - ListLabels.fold_left ~init:Map.empty components ~f:(fun acc comp -> - match comp with - | `Event ev -> - let uid = Utils.get_uid ev in - let event_list = Map.find_opt uid acc |> Option.value ~default:[] in - Map.add ~key:uid ~data:(ev :: event_list) acc - | _ -> acc (* Ignore non-event components *)) - in - (* Now revert all the lists *) - let events_map = Map.map ~f:List.rev events_map in - (* Printf.printf "Events: %d\n\n" (Map.cardinal events_map); *) + Bytes.unsafe_to_string s - Map.iter events_map ~f:(fun ~key:uid ~data:events -> - let rem_or_error = EventPredicates.remind_of_event events in - match rem_or_error with - | Ok rem -> begin Printf.printf "%s\n" (Remind.string_of_rem rem) end - | Error (EventPredicates.Invalid_date s) -> Printf.eprintf "UID: %s Invalid date: %s\n" uid s - | Error Skip -> Printf.eprintf "UID: %s Skipped\n" uid) - end +let ical2rem ical_files = + let good_rems = + ListLabels.fold_left ~init:[] ical_files ~f:(fun good_rems_acc filename -> + try + Printf.eprintf "\nProcessing file: %s\n" filename; + let file_content = read_file filename in + match Icalendar.parse file_content with + | Error e -> + if e <> ": not enough input" then prerr_endline ("Error parsing iCalendar file: " ^ e); + good_rems_acc + | Ok (_, components) -> begin + let events_map : Icalendar.event list Map.t = + ListLabels.fold_left ~init:Map.empty components ~f:(fun acc comp -> + match comp with + | `Event ev -> + let uid = Utils.get_uid ev in + let event_list = Map.find_opt uid acc |> Option.value ~default:[] in + Map.add ~key:uid ~data:(ev :: event_list) acc + | _ -> acc (* Ignore non-event components *)) + in + + let events_map = Map.map ~f:List.rev events_map in + + let good_rems = + Map.fold ~init:[] events_map ~f:(fun ~key:uid ~data:events good_rems -> + let rem_or_error = EventPredicates.remind_of_event events in + match rem_or_error with + | Ok rem -> rem :: good_rems + | Error (EventPredicates.Invalid_date s) -> + Printf.eprintf "UID: %s Invalid date: %s\n" uid s; + good_rems + | Error Skip -> + Printf.eprintf "UID: %s Skipped\n" uid; + good_rems) + in + let good_rems = List.rev good_rems in + good_rems @ good_rems_acc + end + with e -> + prerr_endline ("Error reading file " ^ filename ^ ": " ^ Printexc.to_string e); + good_rems_acc) + in + + ListLabels.iter good_rems ~f:(fun rem -> Printf.printf "%s\n" (Remind.string_of_rem rem)) let () = if !Sys.interactive then () else exit (CommandLine.main ical2rem)