Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup, Arguments #261

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,7 @@ protobuf/
cryptopp/

# misc
Thumbs.db
Thumbs.db

# IntelliJ Rider
.idea/
227 changes: 227 additions & 0 deletions DepotDownloader/ArgumentParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;

namespace DepotDownloader
{
/// <summary>
/// Quick argument parser
/// Need some optimisation, but it is functional ;)
/// </summary>
public static class ArgumentParser
{
/// <summary>
/// Parse command line arguments and set Properties in TContainer object
/// </summary>
/// <param name="args">Command line arguments</param>
/// <typeparam name="TContainer">Type implementing IArgumentContainer</typeparam>
/// <returns>TContainer object with properties set</returns>
/// <exception cref="ArgumentException"></exception>
public static TContainer Parse<TContainer>(string[] args) where TContainer : IArgumentContainer
{
var containerType = typeof(TContainer);
var container = (TContainer)Activator.CreateInstance(containerType);

if (container == null)
{
throw new ArgumentException($"Type {containerType} has no empty constructor");
}

var options = GetContainerOptions(containerType);

for (var i = 0; i < args.Length; i++)
{
if (!args[i].StartsWith("-"))
{
throw new ArgumentException($"Unknown argument: {args[i]}");
}

var arg = args[i].StartsWith("--") ? args[i].Substring(2) : args[i].Substring(1);

var (option, property) = (from op in options
where
arg.Length == 1 && op.Item1.ShortOption == arg[0]
|| op.Item1.LongOption.Equals(arg, StringComparison.Ordinal)
select op).FirstOrDefault();

if (option == null || property == null)
{
throw new ArgumentException($"Unknown argument: '{arg}'");
}

if (option.ParameterName != null || option.AllowMultiple)
{
if (i == args.Length - 1)
{
throw new ArgumentException($"No parameter for option '{arg}' found");
}

if (!option.AllowMultiple)
{
var parameter = args[++i];
if (parameter.StartsWith("-"))
{
throw new ArgumentException($"No parameter for option '{arg}' found");
}

property.SetValue(container,
property.PropertyType == typeof(string)
? parameter
: TypeDescriptor.GetConverter(property.PropertyType).ConvertFromString(parameter));
}
else
{
var converter = property.PropertyType.IsGenericType
? TypeDescriptor.GetConverter(property.PropertyType.GenericTypeArguments[0])
: null;

var list = (IList)property.GetValue(container);
if (list == null)
{
throw new ArgumentException("Initialize List properties first!");
}

while (i < args.Length && !args[i + 1].StartsWith("-"))
{
var parameter = converter != null ? converter.ConvertFromString(args[++i]) : args[++i];
list.Add(parameter);
}
}
}
}

return container;
}

// TODO wrap
/// <summary>
/// Creates a parameter table for given container type
/// </summary>
/// <param name="ident">number of whitespaces at the line beginning</param>
/// <param name="wrap">wrap long descriptions (not implemented yet)</param>
/// <typeparam name="T">Container type</typeparam>
/// <returns></returns>
public static string GetHelpList<T>(int ident = 4, bool wrap = false) where T : IArgumentContainer
{
var optionList = GetContainerOptions(typeof(T));
var sb = new StringBuilder();

var lines = new List<(string, string)>();
var maxOpLength = 0;
foreach (var (option, _) in optionList)
{
var opStr = option.ShortOption != '\0' ? $"-{option.ShortOption}" : "";
if (!string.IsNullOrEmpty(option.LongOption))
{
opStr += (option.ShortOption != '\0' ? ", " : " ") + $"--{option.LongOption}";
}

if (option.ParameterName != null)
{
opStr += $" <{option.ParameterName}>";
}

lines.Add((opStr, option.Description));
maxOpLength = Math.Max(maxOpLength, opStr.Length);
}

var identStr = "".PadRight(ident);
foreach (var (op, desc) in lines)
{
sb.AppendLine(identStr + op.PadRight(maxOpLength + 4) + desc);
}

return sb.ToString();
}

private static List<(OptionAttribute, PropertyInfo)> GetContainerOptions(Type containerType)
{
var resultList = new List<(OptionAttribute, PropertyInfo)>();

foreach (var prop in containerType.GetProperties())
{
// try to get OptionAttribute from property
var a = prop.GetCustomAttribute(typeof(OptionAttribute));

if (a == null)
{
continue;
}

// Check some things
var option = (OptionAttribute)a;
if (prop.SetMethod == null)
{
throw new ArgumentException($"No setter found for '{prop.Name}'");
}

// Only options with descriptions are allowed!
if (string.IsNullOrEmpty(option.Description))
{
throw new ArgumentException($"No description found for '{prop.Name}'");
}

// We need a short option or a long option
if (option.ShortOption == '\0' && string.IsNullOrEmpty(option.LongOption))
{
throw new ArgumentException(
$"You must at least permit ShortOption or LongOption. Property: '{prop.Name}");
}

// AllowMultiple only allowed on list properties
if (option.AllowMultiple && !prop.PropertyType.IsAssignableTo(typeof(IList)))
{
throw new ArgumentException(
$"Options with AllowMultiple must be assignable to IList type. Property: '{prop.Name}");
}

if (!option.AllowMultiple && option.ParameterName == null && prop.PropertyType != typeof(bool))
{
throw new ArgumentException(
$"Property must be bool if there is no parameter required. Property: '{prop.Name}");
}

// if everything is ok, add it to the result list
resultList.Add((option, prop));
}

return resultList;
}
}

[AttributeUsage(AttributeTargets.Property)]
public class OptionAttribute : Attribute
{
/// <summary>
/// Used for lists of parameters, seperated by whitespaces
/// </summary>
public bool AllowMultiple = false;

/// <summary>
/// </summary>
public string Description = null;

/// <summary>
/// long option name (e.g. --username)
/// </summary>
public string LongOption = null;

/// <summary>
/// Name of parameter if parameter is needed (e.g. <user>)
/// </summary>
public string ParameterName = null;

/// <summary>
/// single character option (e.g. -u)
/// </summary>
public char ShortOption = '\0';
}

public interface IArgumentContainer
{
}
}
1 change: 1 addition & 0 deletions DepotDownloader/DepotDownloader.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<Description>Steam Downloading Utility</Description>
<Authors>SteamRE Team</Authors>
<Copyright>Copyright © SteamRE Team 2021</Copyright>
<AssemblyVersion>2.4.3</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading