< Summary

Class:Towel.CommandLine
Assembly:Towel
File(s):File 1: /home/runner/work/Towel/Towel/Sources/Towel/CommandLine.cs
Covered lines:68
Uncovered lines:95
Coverable lines:163
Total lines:211
Line coverage:41.7% (68 of 163)
Covered branches:33
Total branches:78
Branch coverage:42.3% (33 of 78)

Metrics

MethodBranch coverage Cyclomatic complexity Line coverage
File 1: HandleArguments(...)56.89%5861.26%
File 1: DefaultVersion(...)0%20%
File 1: DefaultHelp(...)0%180%

File(s)

/home/runner/work/Towel/Towel/Sources/Towel/CommandLine.cs

#LineLine coverage
 1using System.Reflection;
 2
 3namespace Towel;
 4
 5/// <summary>Contains static helpers for handling command line input and output.</summary>
 6public static class CommandLine
 7{
 8  /// <summary>Handles the command line arguments by invoking the relative <see cref="CommandAttribute"/> method in the 
 9  /// <param name="args">The command line arguments.</param>
 10  public static void HandleArguments(string[]? args = null)
 2411  {
 2412    Assembly assembly = Assembly.GetCallingAssembly();
 2413    args ??= Environment.GetCommandLineArgs();
 2414    if (args.Length < 1)
 015    {
 016      Console.Error.WriteLine("No command provided.");
 017      return;
 18    }
 2419    ListArray<MethodInfo> commandMatches = new();
 36020    foreach (MethodInfo possibleCommand in assembly.GetMethodInfosWithAttribute<CommandAttribute>())
 14421    {
 14422      string command = possibleCommand.Name;
 14423      if (command == args[0])
 2424      {
 2425        commandMatches.Add(possibleCommand);
 2426      }
 14427    }
 2428    if (commandMatches.Count > 1)
 029    {
 030      throw new Exception("syntax error: multiple matching commands");
 31    }
 2432    else if (commandMatches.Count <= 0)
 033    {
 034      string arg = args[0].ToLower().Replace("-", string.Empty);
 035      if (arg is "help" || arg is "h")
 036      {
 037        if (args.Length is 2)
 038        {
 039          DefaultHelp(assembly, args[1]);
 040        }
 41        else
 042        {
 043          DefaultHelp(assembly);
 044        }
 045        return;
 46      }
 047      if (arg is "version" ||
 048        arg is "v")
 049      {
 050        DefaultVersion(assembly);
 051        return;
 52      }
 053      Console.Error.WriteLine("command not found");
 054      return;
 55    }
 2456    if ((args.Length - 1) % 2 != 0)
 057    {
 058      Console.Error.WriteLine($"Invalid argument count.");
 059      return;
 60    }
 2461    MethodInfo methodInfo = commandMatches[0];
 2462    if (!methodInfo.IsStatic)
 063    {
 064      throw new Exception("syntax error: relative command not static");
 65    }
 66
 2467    MapHashLinked<int, string, StringEquate, StringHash> parameterMap = new();
 2468    int parameterCount = 0;
 2469    ParameterInfo[] parameterInfos = methodInfo.GetParameters();
 14070    foreach (ParameterInfo parameterInfo in parameterInfos)
 3471    {
 3472      if (parameterInfo.Name is null) throw new Exception("encountered a null parameter name");
 3473      parameterMap.Add(parameterInfo.Name, parameterCount++);
 3474    }
 2475    object?[] parameters = new object[parameterCount];
 9076    for (int i = 1; i < args.Length; i += 2)
 2677    {
 2678      string arg = args[i];
 2679      if (arg.Length < 3 || arg[0] is not '-' || arg[1] is not '-')
 080      {
 081        Console.Error.WriteLine($"Invalid parameter {arg} in index {i}.");
 082        return;
 83      }
 2684      arg = arg[2..];
 2685      var (success, _, index) = parameterMap.TryGet(arg);
 2686      if (!success)
 587      {
 588        Console.Error.WriteLine($"Invalid parameter --{arg} in index {i}.");
 589        return;
 90      }
 2191      if (parameters[index] is not null)
 092      {
 093        Console.Error.WriteLine($"Duplicate parameters provided --{arg}.");
 094        return;
 95      }
 2196      Type parameterType = parameterInfos[index].ParameterType;
 2197      if (parameterType == typeof(string))
 098      {
 099        parameters[index] = args[i + 1];
 0100      }
 101      else
 21102      {
 103        MethodInfo? tryParse;
 104        ConstructorInfo? constuctor;
 21105        if ((tryParse = Meta.GetTryParseMethod(parameterType)) is not null)
 20106        {
 20107          object[] tryParseParameters = new object[2];
 20108          tryParseParameters[0] = args[i + 1];
 20109          object? result = tryParse.Invoke(null, tryParseParameters);
 20110          if (result is not bool resultBool || !resultBool)
 0111          {
 0112            Console.Error.WriteLine($"Could not parse parameter value --{arg} {args[i + 1]}.");
 0113            return;
 114          }
 20115          parameters[index] = tryParseParameters[1];
 20116        }
 1117        else if ((constuctor = parameterType.GetConstructor(Ɐ(typeof(string)))) is not null)
 1118        {
 1119          parameters[index] = constuctor.Invoke(Ɐ(args[i + 1]));
 1120        }
 121        else
 0122        {
 0123          throw new Exception("syntax error: invalid type used (no tryparse found)");
 124        }
 21125      }
 21126    }
 72127    for (int i = 0; i < parameters.Length; i++)
 24128    {
 24129      if (parameters[i] is null)
 9130      {
 9131        if (!parameterInfos[i].HasDefaultValue)
 7132        {
 7133          Console.Error.WriteLine($"Missing required parameter --{parameterInfos[i].Name} {parameterInfos[i].ParameterTy
 7134          return;
 135        }
 2136        parameters[i] = parameterInfos[i].DefaultValue;
 2137      }
 17138    }
 12139    methodInfo.Invoke(null, parameters);
 24140  }
 141
 142  /// <summary>Gets the default output for the version command.</summary>
 143  /// <param name="assembly">The application assembly.</param>
 144  public static void DefaultVersion(Assembly? assembly = null)
 0145  {
 0146    assembly ??= Assembly.GetCallingAssembly();
 0147    AssemblyName assemblyName = assembly.GetName();
 0148    Console.WriteLine($"Assembly: {assemblyName.Name}");
 0149    Console.WriteLine($"Version: {assemblyName.Version}");
 0150  }
 151
 152  /// <summary>Gets the default output for the help command.</summary>
 153  /// <param name="assembly">The application assembly.</param>
 154  /// <param name="command">The command to get the default help output for.</param>
 155  public static void DefaultHelp(Assembly? assembly = null, string? command = null)
 0156  {
 0157    assembly ??= Assembly.GetCallingAssembly();
 0158    if (command is null)
 0159    {
 0160      DefaultVersion(assembly);
 0161      Console.WriteLine("Commands:");
 0162      foreach (MethodInfo methodInfo in assembly.GetMethodInfosWithAttribute<CommandAttribute>())
 0163      {
 0164        Console.WriteLine("  " + methodInfo.Name);
 0165      }
 0166      Console.WriteLine(@"Use ""Help X"" for detailed info per ""X"" command.");
 0167    }
 168    else
 0169    {
 0170      ListArray<MethodInfo> commandMatches = new();
 0171      foreach (MethodInfo methodInfo in assembly.GetMethodInfosWithAttribute<CommandAttribute>())
 0172      {
 0173        string methodName = methodInfo.Name;
 0174        if (methodName == command)
 0175        {
 0176          commandMatches.Add(methodInfo);
 0177        }
 0178      }
 0179      if (commandMatches.Count > 1)
 0180      {
 0181        throw new Exception("syntax error: multiple matching commands");
 182      }
 0183      else if (commandMatches.Count <= 0)
 0184      {
 0185        Console.Error.WriteLine("command not found");
 0186        return;
 187      }
 188      else
 0189      {
 0190        MethodInfo methodInfo = commandMatches[0];
 0191        string? documentation = Meta.GetDocumentation(methodInfo);
 0192        if (documentation is null)
 0193        {
 0194          Console.WriteLine("Parameters:");
 0195          foreach (ParameterInfo parameterInfo in methodInfo.GetParameters())
 0196          {
 0197            Console.WriteLine("--" + parameterInfo.Name);
 0198          }
 0199        }
 200        else
 0201        {
 0202          Console.WriteLine(documentation);
 0203        }
 0204      }
 0205    }
 0206  }
 207
 208  /// <summary>Indicates that a method is invocable from the command line arguments.</summary>
 209  [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
 210  public class CommandAttribute : Attribute { }
 211}