module Map = MoreLabels.Map.Make (String) (* We use a list of events here because there can be multiple events with the same UID, and we want to preserve all of them. This is important for handling cases where there are multiple events with the same UID but different properties (e.g., due to updates or recurring events or cancellations). *) 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; Bytes.unsafe_to_string s let read_stdin () = let buf = Buffer.create 4096 in (try while true do Buffer.add_channel buf stdin 4096 done with End_of_file -> ()); Buffer.contents buf let ical2rem (args : CommandLine.cli_args) ical_files = (* Validate: '-' (stdin) may appear at most once *) let stdin_count = List.length (List.filter (( = ) "-") ical_files) in if stdin_count > 1 then begin Printf.eprintf "Error: '-' (stdin) may appear at most once.\n"; exit 1 end; (* Validate --source with multiple files *) (match args.source with | Some _ when List.length ical_files > 1 -> Printf.eprintf "Error: --source can only be used with a single input file.\n"; exit 1 | _ -> ()); Config.verbose := args.verbose; Config.no_uuid := args.no_uuid; Config.no_source := args.no_source; Config.no_location := args.no_location; Config.no_description := args.no_description; Config.no_conference_url := args.no_conference_url; Utils.init_target_tz args.tz; let good_rems = ListLabels.fold_left ~init:[] ical_files ~f:(fun good_rems_acc filename -> try let file_content, basename = if filename = "-" then ( read_stdin (), match args.source with | Some name -> name | None -> "stdin" ) else ( read_file filename, match args.source with | Some name -> name | None -> Filename.remove_extension (Filename.basename filename) ) in match Icalendar.parse file_content with | Error e -> if e <> ": not enough input" then Utils.warn "Error: could not parse %s: %s\n" filename 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 basename events in match rem_or_error with | Ok rem -> rem :: good_rems | Error (EventPredicates.Invalid_date s) -> Utils.warn "Warning: invalid date: %s (UID: %s)\n" s uid; good_rems | Error Skip -> Utils.warn "Warning: event skipped (UID: %s)\n" uid; good_rems) in let good_rems = List.rev good_rems in good_rems @ good_rems_acc end with e -> Printf.eprintf "Error: could not read file %s: %s\n%s" filename (Printexc.to_string e) (Printexc.get_backtrace ()); good_rems_acc) in let good_rems = match args.sort with | CommandLine.Desc -> List.sort (fun a b -> Timedesc.Date.compare b.Remind.date a.Remind.date) good_rems | CommandLine.Asc -> List.sort (fun a b -> Timedesc.Date.compare a.Remind.date b.Remind.date) good_rems | CommandLine.Original -> good_rems in try ListLabels.iter good_rems ~f:(fun rem -> Printf.printf "%s" (Remind.string_of_rem rem)) with e -> Printf.eprintf "Error processing reminders: %s\n%s" (Printexc.to_string e) (Printexc.get_backtrace ()); exit 1 let () = if !Sys.interactive then () else begin Printexc.record_backtrace true; exit (CommandLine.main ical2rem) end