// Copyright Keysight Technologies 2012-2019 // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, you can obtain one at http://mozilla.org/MPL/2.0/. using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; namespace OpenTap.Cli { /// /// Helper class that enhances the ICliAction with extra extensions methods. /// internal static class ICliActionExtensions { /// /// Executes the action with the given parameters. /// /// The action to be executed. /// The parameters for the action. /// public static int PerformExecute(this ICliAction action, string[] parameters) { ArgumentsParser ap = new ArgumentsParser(); var td = TypeData.GetTypeData(action); if (td == null) throw new Exception("Type data is null for action: " + action.GetType().Name); var props = td.GetMembers(); ap.AllOptions.Add("help", 'h', false, "Write help information."); ap.AllOptions.Add("verbose", 'v', false, "Show verbose/debug-level log messages."); ap.AllOptions.Add("color", 'c', false, "Color messages according to their severity."); ap.AllOptions.Add("quiet", 'q', false, "Quiet console logging."); ap.AllOptions.Add("log", description: "Specify log file location. Default is ./SessionLogs."); var argToProp = new Dictionary(); var unnamedArgToProp = new List(); var overrides = new HashSet(); foreach (var prop in props) { if (prop.Readable == false || prop.Writable == false) continue; if (prop.HasAttribute()) { unnamedArgToProp.Add(prop); continue; } if (!prop.HasAttribute()) continue; var attr = prop.GetAttribute(); var needsArg = prop.TypeDescriptor != TypeData.FromType(typeof(bool)); string description = ""; if (prop.HasAttribute()) { var obsoleteAttribute = prop.GetAttribute(); description = $"[OBSOLETE: {obsoleteAttribute.Message}] {attr.Description}"; } else { description = attr.Description; } if (ap.AllOptions.Contains(attr.Name)) { overrides.Add(attr.Name); } if (!string.IsNullOrWhiteSpace(attr.ShortName)) { var overriden = ap.AllOptions.FirstOrDefault(opt => opt.Value?.ShortName == attr.ShortName[0]) .Value; if (overriden != null) { overrides.Add(overriden.ShortName.ToString()); // Set the ShortName of the overriden option to '\0' so the argument parser will resolve this ShortName to the overriding option // The overriden option can still be accessed by its LongName, provided that is not also overriden ap.AllOptions.Add(overriden.LongName, '\0', overriden.NeedsArgument, overriden.Description); } } var arg = ap.AllOptions.Add(attr.Name, attr.ShortName?.FirstOrDefault() ?? '\0', needsArg, description); // attr.Visible has been obsoleted but is still considered for backward compatibility. #pragma warning disable CS0618 arg.IsVisible = attr.Visible && (prop.GetAttribute()?.Browsable ?? true); #pragma warning restore CS0618 argToProp.Add(arg.LongName, prop); } var args = ap.Parse(parameters); if (args.MissingArguments.Any()) throw new Exception( $"Command line argument '{args.MissingArguments.FirstOrDefault().LongName}' is missing an argument."); foreach (var @override in overrides) { if (args.Contains(@override)) log.Debug( $"The CLI option '--{@override}' from '{action}' overrides a common CLI option from OpenTAP."); else if (@override.Length == 1) { var shortName = @override[0]; var isUsed = args.Any(a => a.Value?.ShortName == shortName); if (isUsed) log.Debug( $"The CLI option '-{@override}' from '{action}' overrides a common CLI option from OpenTAP."); } } if (!overrides.Contains("help") && args.Contains("help")) { printOptions(action.GetType().GetDisplayAttribute().Name, ap.AllOptions, unnamedArgToProp); return (int)ExitCodes.Success; } if (!overrides.Contains("log") && args.Contains("log")) { SessionLogs.Rename(args.Argument("log")); } foreach (var opts in args) { if (argToProp.ContainsKey(opts.Key) == false) continue; var prop = argToProp[opts.Key]; if (prop.TypeDescriptor is TypeData propTd) { Type propType2 = propTd.Load(); object getValue(string src, Type propType) { if (propType == typeof(string)) return src; if (propType == typeof(bool)) return true; if (propType.IsEnum) return parseArbitrary(opts.Key, src, propType); if (propType == typeof(int) || propType == typeof(long) || propType == typeof(uint) || propType == typeof(ushort) || propType == typeof(short) || propType == typeof(byte) || propType == typeof(sbyte) || propType == typeof(ulong) || propType == typeof(double) || propType == typeof(float)) return Convert.ChangeType(src, propType); throw new Exception( $"Command line option '{opts.Key}' is of an unsupported type '{propType.Name}'."); } if (propType2 != typeof(string) && propType2.IsArray) { var array = Array.CreateInstance(propType2.GetElementType(), opts.Value.Values.Count); var elemType = propType2.GetElementType(); for (int i = 0; i < array.Length; i++) array.SetValue(getValue(opts.Value.Values[i], elemType), i); prop.SetValue(action, array); } else { prop.SetValue(action, getValue(opts.Value.Value, propType2)); } } else throw new Exception( $"Command line option '{opts.Key}' is of an unsupported type '{prop.TypeDescriptor.Name}'."); } unnamedArgToProp = unnamedArgToProp.OrderBy(p => p.GetAttribute().Order) .ToList(); var requiredArgs = unnamedArgToProp.Where(x => x.GetAttribute().Required) .ToHashSet(); int idx = 0; for (int i = 0; i < unnamedArgToProp.Count; i++) { var p = unnamedArgToProp[i]; if (p.TypeDescriptor.IsA(typeof(string))) { if (idx < args.UnnamedArguments.Length) { p.SetValue(action, args.UnnamedArguments[idx++]); requiredArgs.Remove(p); } } else if (p.TypeDescriptor.IsA(typeof(string[]))) { if (idx < args.UnnamedArguments.Length) { p.SetValue(action, args.UnnamedArguments.Skip(idx).ToArray()); requiredArgs.Remove(p); } idx = args.UnnamedArguments.Length; } else if (p.TypeDescriptor is TypeData td2 && td2.Type.IsEnum) { if (idx < args.UnnamedArguments.Length) { var name = p.GetAttribute()?.Name ?? p.Name; p.SetValue(action, parseArbitrary($"<{name}>", args.UnnamedArguments[idx++], td2.Type)); requiredArgs.Remove(p); } } } if (args.UnknownsOptions.Any() || requiredArgs.Any()) { if (args.UnknownsOptions.Any()) Console.WriteLine("Unknown options: " + string.Join(" ", args.UnknownsOptions)); if (requiredArgs.Any()) Console.WriteLine("Missing argument: " + string.Join(" ", requiredArgs.Select(p => p.GetAttribute().Name))); printOptions(action.GetType().GetDisplayAttribute().Name, ap.AllOptions, unnamedArgToProp); return (int)ExitCodes.ArgumentParseError; } var actionFullName = td.GetDisplayAttribute().GetFullName(); bool isUpdate = "package \\ check-updates" == actionFullName; if (isUpdate) { // Avoid logging debug information for check-updates, since this is potentially confusing. return action.Execute(TapThread.Current.AbortToken); } log.Debug($"Executing CLI action: {actionFullName}"); var sw = Stopwatch.StartNew(); int exitCode = action.Execute(TapThread.Current.AbortToken); log.Debug(sw, "CLI action returned exit code: {0}", exitCode); return exitCode; } static TraceSource log = Log.CreateSource("CLI"); private static void printOptions(string passName, ArgumentCollection options, List unnamed) { var namedArguments = string.Join(" ", options.Values.Where(x => x.IsVisible) .Select(x => { var str = x.ShortName != 0 ? $"-{x.ShortName}" : "--" + x.LongName; if (x.NeedsArgument) str += " "; return '[' + str + ']'; })); var unnamedArguments = string.Join(" ", unnamed.Select(x => { var attr = x.GetAttribute(); var str = attr.Name; if (attr.Required == false) str = "[<" + str + ">]"; else str = "<" + str + ">"; return str; })); Console.WriteLine($"Usage: {passName} {namedArguments} {unnamedArguments}"); options.UnnamedArgumentData = unnamed; Console.Write(options); } private static object parseArbitrary(string name, string value, Type propertyType) { var obj = StringConvertProvider.FromString(value, TypeData.FromType(propertyType), null, System.Globalization.CultureInfo.InvariantCulture); if (obj == null) throw new Exception(string.Format( "Could not parse argument '{0}'. Argument given: '{1}'. Valid arguments: {2}", name, value, string.Join(", ", propertyType.GetEnumNames()))); return obj; } } }