using GitConverter.Lib.Converters; using GitConverter.Lib.Factories; using GitConverter.Lib.Logging; using GitConverter.Lib.Models; namespace GitConverter.ConsoleApp { class Program { // Exit codes for the CLI private enum ExitCode : int { Success = 0, // Successful execution AppError = 1, // invalid args, wrong command ConversionFailed = 2, // converter returned Failure or null Unexpected = 3, // unexpected exception } // Main remains the true entry point and exits with the Run(...) result private static void Main(string[] args) { // Defensive debug helper: only returns defaults when running under DEBUG and args are missing. args = EnsureDebugArgs(args); Environment.Exit(Run(args)); } // Returns the original args when caller supplied enough arguments; otherwise returns debug defaults. // Guarded by #if DEBUG so production binaries are unaffected. // // Project debug settings (Visual Studio) — recommended: // 1. Right-click the ConsoleApp project → Properties → Debug. // 2. In Environment variables add: GITCONVERTER_DEBUG_TARGET=Shapefile1 for example // 3. Save and Start Debugging. // // The environment variable is only used in Debug builds so Release/CI are unaffected. private static string[] EnsureDebugArgs(string[] args) { #if DEBUG if (args != null && args.Length >= 6) return args; // Allow selecting a debug scenario via environment variable. // Supported values: Shapefile (default), Csv, Gml. var debugTarget = Environment.GetEnvironmentVariable("GITCONVERTER_DEBUG_TARGET"); switch (debugTarget.Trim().ToLowerInvariant()) { // Shapefile test cases case "shapefile1": return new[] { "gis_convert", @"D:\GisConverter\Tests\Shapefile\Input\ShapeFiles.7z", "ShapeFile", @"D:\GisConverter\Tests\Shapefile\Output\Shapefile1", @"D:\GisConverter\Tests\Shapefile\Temp\Shapefile1", "Log", @"D:\GisConverter\Tests\Shapefile\Log\shapefile_log1.txt" }; case "shapefile2": return new[] { "gis_convert", @"D:\GisConverter\Tests\Shapefile\Input\Vector.7z", "Shapefile", @"D:\GisConverter\Tests\Shapefile\Output\Shapefile2", @"D:\GisConverter\Tests\Shapefile\Temp\Shapefile2", "Log", @"D:\GisConverter\Tests\Shapefile\Log\shapefile_log2.txt" }; case "shapefile3": return new[] { "gis_convert", @"D:\GisConverter\Tests\Shapefile\Input\לא סטטוטורי - גבול מחוז מאוחד.7z", "Shapefile", @"D:\GisConverter\Tests\Shapefile\Output\Shapefile3", @"D:\GisConverter\Tests\Shapefile\Temp\Shapefile3", "Log", @"D:\GisConverter\Tests\Shapefile\Log\shapefile_log3.txt" }; case "shapefile4": return new[] { "gis_convert", @"D:\GisConverter\Tests\Shapefile\Input\שכבות מידע (Arc View).7z", "Shapefile", @"D:\GisConverter\Tests\Shapefile\Output\Shapefile4", @"D:\GisConverter\Tests\Shapefile\Temp\Shapefile4", "Log", @"D:\GisConverter\Tests\Shapefile\Log\shapefile_log4.txt" }; // Csv test cases case "csv1": return new[] { "gis_convert", @"D:\GisConverter\Tests\Csv\Input\features.7z", "Csv", @"D:\GisConverter\Tests\Csv\Output\Csv1", @"D:\GisConverter\Tests\Csv\Temp\Csv1", "Log", @"D:\GisConverter\Tests\Csv\Log\csv_log1.txt" }; case "csv2": return new[] { "gis_convert", @"D:\GisConverter\Tests\Csv\Input\features.csv", "Csv", @"D:\GisConverter\Tests\Csv\Output\Csv2", @"D:\GisConverter\Tests\Csv\Temp\Csv2", "Log", @"D:\GisConverter\Tests\Csv\Log\csv_log2.txt" }; case "csv3": return new[] { "gis_convert", @"D:\GisConverter\Tests\Csv\Input\features.zip", "Csv", @"D:\GisConverter\Tests\Csv\Output\Csv3", @"D:\GisConverter\Tests\Csv\Temp\Csv3", "Log", @"D:\GisConverter\Tests\Csv\Log\csv_log3.txt" }; // Gml test cases case "gml1": return new[] { "gis_convert", @"D:\GisConverter\Tests\Gml\Input\gml_with_attribute_collection_schema.7z", "Gml", @"D:\GisConverter\Tests\Gml\Output\Gml1", @"D:\GisConverter\Tests\Gml\Temp\Gml1", "Log", @"D:\GisConverter\Tests\Gml\Log\gml_log1.txt" }; case "gml2": return new[] { "gis_convert", @"D:\GisConverter\Tests\Gml\Input\gml_with_attribute_collection_schema.zip", "Gml", @"D:\GisConverter\Tests\Gml\Output\Gml2", @"D:\GisConverter\Tests\Gml\Temp\Gml2", "Log", @"D:\GisConverter\Tests\Gml\Log\gml_log2.txt" }; case "gml3": return new[] { "gis_convert", @"D:\GisConverter\Tests\Gml\Input\gml_without_attribute_collection_schema.7z", "Gml", @"D:\GisConverter\Tests\Gml\Output\Gml3", @"D:\GisConverter\Tests\Gml\Temp\Gml3", "Log", @"D:\GisConverter\Tests\Gml\Log\gml_log3.txt" }; case "gml4": return new[] { "gis_convert", @"D:\GisConverter\Tests\Gml\Input\gml_without_attribute_collection_schema.gml", "Gml", @"D:\GisConverter\Tests\Gml\Output\Gml4", @"D:\GisConverter\Tests\Gml\Temp\Gml4", "Log", @"D:\GisConverter\Tests\Gml\Log\gml_log4.txt" }; case "gml5": return new[] { "gis_convert", @"D:\GisConverter\Tests\Gml\Input\gml_without_attribute_collection_schema.zip", "Gml", @"D:\GisConverter\Tests\Gml\Output\Gml5", @"D:\GisConverter\Tests\Gml\Temp\Gml5", "Log", @"D:\GisConverter\Tests\Gml\Log\gml_log5.txt" }; default: return args ?? Array.Empty(); } #else return args ?? Array.Empty(); #endif } // Run encapsulates CLI logic and returns an exit code (0=success, 1=app error, 2=conversion failed, 3=unexpected). public static int Run(string[] args) { // If no arguments are provided or help is requested, show help and exit. if (args.Length == 0 || args[0] == "help" || args[0] == "--help" || args[0] == "-h") { ShowHelp(); return (int)ExitCode.AppError; } // Extract the command from the first argument. string command = args[0].ToLowerInvariant(); try { switch (command) { case "gis_convert": return RunGisConverter(args); default: Console.WriteLine($"❌ Unknown command: {command}\n"); ShowHelp(); return (int)ExitCode.AppError; } } catch (Exception ex) { // Top-level unexpected error Console.WriteLine("❌ Error:"); Console.WriteLine(ex.Message); return (int)ExitCode.Unexpected; } } // Run the gis_convert command and return an exit code private static int RunGisConverter(string[] args) { // Require at least: command (gis_convert), gis input file path, gis target format option, output folder path, out temp folder. if (args.Length < 6) { Console.WriteLine("Usage: gis_convert [Log ]"); return (int)ExitCode.AppError; } // attempt to repair args if user forgot to quote input path (may contain spaces) var factoryProbe = new ConverterFactory(); args = TryRepairUnquotedInputArgs(args, factoryProbe); var gisInputFilePath = args[1]; var gisTargetFormatOption = args[2]; var outputFolderPath = args[3]; var tempFolderPath = args[4]; var logFilePath = GetOptionalLogFolderPath(args, 5); SetLogger(logFilePath); var factory = new ConverterFactory(); string detectReason; if (!factory.TryCreateForInput(gisInputFilePath, out var converter, out detectReason) || converter == null) { // Detection failed or ambiguous; report detect reason and exit. Console.WriteLine("❌ Unable to auto-detect converter from input."); if (!string.IsNullOrWhiteSpace(detectReason)) Console.WriteLine($"Detector reason: {detectReason}"); Console.WriteLine("Please use 'help' for supported options."); return (int)ExitCode.AppError; } // Converter resolved by TryCreateForInput — invoke it. try { ConversionResult result = converter.Convert(gisInputFilePath, gisTargetFormatOption, outputFolderPath, tempFolderPath); if (result == null) { Console.WriteLine("❌ Conversion finished with no result returned."); return (int)ExitCode.ConversionFailed; } else { // Always present timestamps to the user in local time, keep UTC for diagnostics. var utc = result.TimestampUtc; var timeLine = ConverterUtils.FormatTimestampForDisplay(utc); if (result.IsSuccess) { Console.WriteLine("✅ Conversion finished successfully."); Console.WriteLine(timeLine); if (!string.IsNullOrWhiteSpace(result.Message)) Console.WriteLine(result.Message); return (int)ExitCode.Success; } else { Console.WriteLine("❌ Conversion failed."); Console.WriteLine(timeLine); if (!string.IsNullOrWhiteSpace(result.Message)) Console.WriteLine(result.Message); return (int)ExitCode.ConversionFailed; } } } catch (Exception ex) { // Defensive: unexpected exceptions from converters Console.WriteLine("❌ Conversion encountered an unexpected error:"); Console.WriteLine(ex.Message); return (int)ExitCode.Unexpected; } } // Attempt to repair args when input path contains spaces and wasn't quoted. // Strategy: // - Look for the first token after the command that matches a supported converter option. // - If found, join intervening tokens into the input path. // - Returns original args when no repair possible. private static string[] TryRepairUnquotedInputArgs(string[] args, ConverterFactory factoryProbe) { if (args == null || args.Length < 3) return args; var supported = new HashSet(factoryProbe.GetSupportedOptions().Select(s => s.ToLowerInvariant()), StringComparer.OrdinalIgnoreCase); // search for supported option token starting from index 2 (args[0]=command, args[1]=start of input) for (int i = 2; i < args.Length; i++) { var token = args[i].Trim().ToLowerInvariant(); if (supported.Contains(token)) { // found the format token at index i -> join args[1..i-1] into input path var inputParts = args.Skip(1).Take(i - 1).ToArray(); var joinedInput = string.Join(" ", inputParts); var rebuilt = new List { args[0], // command joinedInput, // joined input path args[i] // target option }; // append remaining args (output, temp, optional Log ...) for (int j = i + 1; j < args.Length; j++) rebuilt.Add(args[j]); return rebuilt.ToArray(); } } // No supported option token found — return original args. return args; } // Displays usage information and available commands to the user. static void ShowHelp() { Console.WriteLine("GIS Converter CLI Tool"); Console.WriteLine("----------------------"); Console.WriteLine(); Console.WriteLine("Usage:"); Console.WriteLine(" GISConverter.Cli.exe [options]"); Console.WriteLine(); Console.WriteLine("Commands:"); Console.WriteLine(" gis_convert "); Console.WriteLine(" [Log ]"); Console.WriteLine(" Convert GIS input file path, as a single file or an archive file, to output folder path."); Console.WriteLine(" The output file in output folder path created by the system in runtime"); Console.WriteLine(" according to a given target GIS format option."); Console.WriteLine(" The temp folder path is used for internal use. Optional parameter to specify an output log file path"); Console.WriteLine(); Console.WriteLine(" Explanations:"); Console.WriteLine(" gis_convert - is the command for converting GIS input to other GIS formats."); Console.WriteLine(); Console.WriteLine(" - The gis input file can be a single file or an archive file,"); Console.WriteLine(" if there are more than one files."); Console.WriteLine(); Console.WriteLine(" Available GIS target conversion format options:"); Console.WriteLine(" EsriJson:"); Console.WriteLine(" Purpose:a JSON format used by Esri ArcGis software to reprsent geographic features."); Console.WriteLine(" Similar to GeoJson but with Esri-specific attributes."); Console.WriteLine(" Files needed: a single .esrijson or .json file."); Console.WriteLine(); Console.WriteLine(" GeoJson:"); Console.WriteLine(" Purpose:a widely used open standard format for encoding variety of geographic data structures."); Console.WriteLine(" Files needed: a single .geojson or .json file."); Console.WriteLine(); Console.WriteLine(" GeoJsonSeq:"); Console.WriteLine(" Purpose:GeoJson sequence, where each line is an individual GeoJson feature(streaming format)."); Console.WriteLine(" It’s json format with special structure."); Console.WriteLine(" Files needed: a single .json file."); Console.WriteLine(); Console.WriteLine(" Kml:"); Console.WriteLine(" Purpose:XML based format used by Goggle Earth for geographic annotation and visualization."); Console.WriteLine(" Files needed: a single .kml file."); Console.WriteLine(); Console.WriteLine(" Kmz:"); Console.WriteLine(" Purpose:XML based format used by Goggle Earth for geographic annotation and visualization."); Console.WriteLine(" .kmz is a zipped of .kml."); Console.WriteLine(" Files needed: a single .kmz file."); Console.WriteLine(); Console.WriteLine(" Shapefile:"); Console.WriteLine(" Purpose:Traditional Esri vector format for storing geographic features."); Console.WriteLine(" Files needed:an archive file that contains .shp (geometry), .shx (shape index), .dbf (attribute table)."); Console.WriteLine(" Optinals:.prj, .cpg, .sbn, .sbx ."); Console.WriteLine(); Console.WriteLine(" Osm:"); Console.WriteLine(" Purpose:Xml format used by OpenStreetMap to store nodes, ways, and relations."); Console.WriteLine(" Files needed: a single .osm file."); Console.WriteLine(); Console.WriteLine(" Gpx:"); Console.WriteLine(" Purpose:Xml schema for storing GPs track, waypoint and route data."); Console.WriteLine(" Files needed: a single .gpx file."); Console.WriteLine(); Console.WriteLine(" Gml:"); Console.WriteLine(" Purpose:Xml based Geography markup Language, often used in government or standards-based data."); Console.WriteLine(" Files needed: a single .gml file or .gml file (sometimes with .xsd schema) as an archive file."); Console.WriteLine(); Console.WriteLine(" Gdb:"); Console.WriteLine(" Purpose:a folder-based database format from Esri storing multiple feature classes and tables."); Console.WriteLine(" Files needed: FileGdb can be represent as a dataset or just as one table."); Console.WriteLine(" In case of dataset - the entire .gdb folder as a single file."); Console.WriteLine(" In case of one table - .gdbtable and .gdbtablx as an archive file."); Console.WriteLine(); Console.WriteLine(" TopoJson:"); Console.WriteLine(" Purpose:Extension of GeoJson that encodes topology, reducing redundancy and file size."); Console.WriteLine(" Files needed: a single .json file."); Console.WriteLine(); Console.WriteLine(" MapInfoInterchange:"); Console.WriteLine(" Purpose:Text based format used by MapInfo system."); Console.WriteLine(" Files needed: .mif mandatory, .mid - contains attribue info and not mandatory."); Console.WriteLine(" If both files are supplied give it as an archive file, otherwise a single .mif file is needed."); Console.WriteLine(); Console.WriteLine(" MapInfoTab:"); Console.WriteLine(" Purpose:Native MapInfo vector format."); Console.WriteLine(" Files needed: an archive file that contains: .tab, .map, .id."); Console.WriteLine(" .tab - main file(link others)."); Console.WriteLine(); Console.WriteLine(" Csv:"); Console.WriteLine(" Purpose:Plain text format for tabular data."); Console.WriteLine(" Files needed: a single .csv file."); Console.WriteLine(); Console.WriteLine(" GeoPackage:"); Console.WriteLine(" Purpose:SQLite-based OGC standard for vector and raster data - modern, compact, and widely support."); Console.WriteLine(" Files needed: a single .gpkg file."); Console.WriteLine(); Console.WriteLine(); Console.WriteLine(" - The output folder for conversion results."); Console.WriteLine(" The system - Gis Cnverter App creates the file inside the ouput folder path"); Console.WriteLine(" with the right extension."); Console.WriteLine(); Console.WriteLine(" - Temporary folder used internally by the converter during processing."); Console.WriteLine(); Console.WriteLine(" [Log ] - Optional. To enable file logging include 'Log' followed by the desired log file path."); Console.WriteLine(); Console.WriteLine("Options:"); Console.WriteLine(" help, --help, -h Show this help screen"); } /// /// Finds an optional "Log <log_file_path>" argument pair in the command-line arguments, /// starting the search from the specified index. /// /// Command-line arguments passed to the application. May be null. /// Zero-based index in to begin the search. /// /// The resolved log file path when a 'Log' token is found and followed by one or more path segments; /// the returned string is the concatenation of all remaining arguments joined with spaces and trimmed. /// Returns null when: /// - is null or has fewer than searchFromIndex + 2 elements, /// - no 'Log' token is found at or after , /// - or the candidate path is empty or whitespace. /// /// /// - The method looks for a token equal to "Log" (case-insensitive). When found it joins all subsequent argv tokens /// into a single path string to support paths that contain spaces without requiring the caller to pre-quote them. /// - Example: /// args = { "gis_convert", "in.7z", "Shapefile", "out", "tmp", "Log", "D:\\My Logs\\log.txt" } /// => returns "D:\My Logs\log.txt" /// - If multiple "Log" tokens are present the first occurrence at or after is used. /// - This method does not validate the path's existence, permissions or syntax beyond trimming; callers should handle IO errors. /// static string GetOptionalLogFolderPath(string[] args, int searchFromIndex) { if (args == null || args.Length < searchFromIndex + 2) return null; for (int i = searchFromIndex; i < args.Length - 1; i++) { if (string.Equals(args[i], "Log", StringComparison.OrdinalIgnoreCase)) { // Join everything after "Log" into a single path (handles spaces) var parts = new List(); for (int j = i + 1; j < args.Length; j++) parts.Add(args[j]); var candidate = string.Join(" ", parts).Trim(); // optional: basic sanity check if (!string.IsNullOrWhiteSpace(candidate)) return candidate; } } return null; } /// /// Configure the library logger according to the provided log file path. /// /// /// Path to the log file to enable file logging. If null the library logging is disabled (no-op). /// Surrounding quotes are trimmed automatically. /// /// /// - Logging setup can fail (invalid path, missing permissions, locked files). We catch exceptions /// to ensure logging initialization never crashes the application. On failure logging is disabled /// and a concise warning is written to standard error. /// - Do not throw from this method: callers expect logging to be best-effort and non-fatal. /// - If you want the application to fail when logging cannot be configured, change the behavior here /// to rethrow or return a boolean result that the caller can act upon. /// private static void SetLogger(string logFilePath) { if (string.IsNullOrWhiteSpace(logFilePath)) { // No log file requested — use no-op logger. Log.Disable(); return; } // Trim surrounding quotes often present in CLI arguments. var path = logFilePath.Trim().Trim('"'); try { // Configure file logging; Log.SetFile may throw (invalid path, IO issues). Log.SetFile(path); // Activate the configured logger. Log.Enable(); } catch (Exception ex) { // Fail soft: disable logging and inform the user via stderr. // Do not rethrow — logging must be best-effort and must not terminate the conversion. Console.Error.WriteLine($"⚠️ Warning: logging disabled — could not initialize file logger: {ex.Message}"); Log.Disable(); } } } }