Hello @Dani_S ,
I’ve reviewed your code and have some suggestions. It is going to be lengthy, so I’ll try to break them down into sections for clarity.
About your ShowHelp method
Your current approach works, but the help text is very long—over 150 lines. Users usually want quick guidance first, then deeper details if needed. Consider splitting this into a brief summary by default and offering something like --help-formats for the full format descriptions. You could also move detailed format explanations to external documentation.
For the help trigger, you’re returning ExitCode.AppError. Help isn’t actually an error—it’s informational. You should return ExitCode.Success instead:
if (args.Length == 0 || args[0] == "help" || args[0] == "--help" || args[0] == "-h")
{
ShowHelp();
return (int)ExitCode.Success; // changed from AppError
}
Adding log level support
You want to add an optional log level parameter after the log path.
Your updated CLI signature would look like:
gis_convert <input> <format> <output> <temp> [Log <log_path> [level]]
Step 1: Update your method to extract both log path and level. Rename GetOptionalLogFolderPath to something like GetOptionalLogArguments and return a tuple:
static (string logPath, string logLevel) GetOptionalLogArguments(string[] args, int searchFromIndex)
{
if (args == null || args.Length < searchFromIndex + 2) return (null, null);
for (int i = searchFromIndex; i < args.Length - 1; i++)
{
if (string.Equals(args[i], "Log", StringComparison.OrdinalIgnoreCase))
{
var knownLevels = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"DEBUG", "INFO", "WARN", "ERROR", "FATAL", "ALL", "OFF"
};
var pathTokens = new List<string>();
string detectedLevel = null;
for (int j = i + 1; j < args.Length; j++)
{
var token = args[j];
if (knownLevels.Contains(token))
{
detectedLevel = token.ToUpperInvariant();
break;
}
pathTokens.Add(token);
}
var logPath = string.Join(" ", pathTokens).Trim();
if (string.IsNullOrWhiteSpace(logPath)) return (null, null);
return (logPath, detectedLevel);
}
}
return (null, null);
}
This handles spaces in the log path automatically by joining tokens until it finds a recognized log level keyword or reaches the end of arguments.
Step 2: Update your SetLogger method to accept the log level:
private static void SetLogger(string logFilePath, string logLevel = null)
{
if (string.IsNullOrWhiteSpace(logFilePath))
{
Log.Disable();
return;
}
var path = logFilePath.Trim().Trim('"');
try
{
Log.SetFile(path, logLevel);
Log.Enable();
}
catch (Exception ex)
{
Console.Error.WriteLine($"⚠️ warning: logging disabled — could not initialize file logger: {ex.Message}");
Log.Disable();
}
}
Step 3: Update your Log.SetFile method in the logging library to accept and apply the level.
Step 4: Update RunGisConverter to use the new signature:
private static int RunGisConverter(string[] args)
{
if (args.Length < 6)
{
Console.WriteLine("usage: gis_convert <input> <format> <output> <temp> [log <log_path> [level]]");
return (int)ExitCode.AppError;
}
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, logLevel) = GetOptionalLogArguments(args, 5);
SetLogger(logFilePath, logLevel);
// rest of your conversion logic...
}
About handling spaces in paths
Your TryRepairUnquotedInputArgs approach for the input path is a reasonable workaround. It looks for the first token that matches a supported converter format and joins everything before it as the input path. This works but has edge cases—for example, if someone has a folder literally named “shapefile” in their path, it might misidentify that as the format option.
For the log path, the approach above handles spaces by collecting all tokens between “Log” and a recognized log level keyword (or end of args). This means users don’t need to quote the path unless they have a folder with a name that matches a log level keyword like “DEBUG” or “INFO”.
Ideally, you should document this behavior clearly in your help text and encourage users to quote paths when in doubt. For example:
note: paths with spaces can be unquoted, but quoting is recommended
to avoid ambiguity (e.g., "C:\My Logs\run.log" DEBUG)
Other things to consider
You’re catching and suppressing exceptions in SetLogger. That’s fine for production use, but during development it can make debugging harder. You might want to add a --strict or --verbose flag that causes logging setup failures to throw instead of just warning.
In your integration tests, the reflection-based InvokeRun helper is pretty complex. If you can directly reference the console app project from your test project, you could just call Program.Run directly without all that reflection probing. Reflection here feels like overengineering.
Putting it together
An example of the updated method:
public static int Run(string[] args)
{
if (args.Length == 0 || args[0] == "help" || args[0] == "--help" || args[0] == "-h")
{
ShowHelp();
return (int)ExitCode.Success; // not an error
}
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)
{
Console.WriteLine("❌ error:");
Console.WriteLine(ex.Message);
return (int)ExitCode.Unexpected;
}
}
And in RunGisConverter, replace the old log extraction with:
var (logFilePath, logLevel) = GetOptionalLogArguments(args, 5);
SetLogger(logFilePath, logLevel);
This gives users the flexibility to specify log levels like:
gis_convert input.shp geojson ./out ./tmp log C:\My Logs\run.log DEBUG
Or just use the default (ALL) by omitting the level:
gis_convert input.shp geojson ./out ./tmp log C:\My Logs\run.log