GIS CLI commands

Dani_S 4,946 Reputation points
2025-12-12T11:02:22.9333333+00:00

Hi,

I would like to ask the following questions regarding my GIS CLI command in program file.

The GIS CLI commands used once by end-user.

1.Does the ShowHelp method is a good practice in CLI if not please fix.

I used in this case to show it:

// 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;

}

  1. This the command line:

<gis_input_file_path> <gis_target_format_option> <output_folder_path> <temp_folder_path> [Log <log_file_path>]"

[Log <log_file_path>]" is optional and handled in GetOptionalLogFolderPath method.

I want to add log level After Log or after <log_file_path> , in the appropriate place**.**

I use Log4Net. Can you please give me code to add this funcuality?

I added my logs files. In Log file -SetFile method there are these lines:

// Always enable all levels on the root logger for maximum diagnostic coverage

if (hierarchy != null)

{

 **hierarchy.Root.Level = hierarchy.LevelMap["ALL"];**

 **hierarchy.Configured = true;**

}

Take into account <log_file_path> can contains spaces and wasn't quoted that can broke the command line parser. Need to hndle this case also.

3.For the gis_input_file_path can contains spaces and wasn't quoted - is it ok?

// 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<string>(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<string>

{

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;

}

4.If you see more problem me please fix me me the code?

5.Please give a full code.

Thanks in advance.

ConversionResult.txt

ConverterFactory.txt

ConverterUtils.txt

IAppLogger.txt

Log.txt

Log4NetAdapter.txt

NullLogger.txt

Program.txt

ProgramIntegrationTest.txt

README.md.txt

Developer technologies | C#
Developer technologies | C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
{count} votes

1 answer

Sort by: Most helpful
  1. Michael Le (WICLOUD CORPORATION) 6,670 Reputation points Microsoft External Staff Moderator
    2025-12-15T07:45:14.9166667+00:00

    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
    

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.