// 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.Linq; using System.Text; namespace OpenTap.Cli { /// /// Parser for command line arguments. Supports --,-,/ based argument options /// as well as unnamed options mixed with named ones. /// internal class ArgumentsParser { public ArgumentCollection AllOptions = new ArgumentCollection(); struct optionFindResult { public Argument FoundOption; public bool IsUnknown; public string InlineArg; } optionFindResult findOption(string st, ArgumentCollection options) { optionFindResult opt = new optionFindResult { IsUnknown = false }; int len = 0; if (st.StartsWith("--")) { len = 2; } else if (st.StartsWith("-")) // Removed check for starting with '\'. It breaks installing a package from network drive because it starts with \\. { len = 1; } if (len == 0) { return opt; } st = st.Substring(len); var idx = st.IndexOf('='); if (idx > 0) { opt.InlineArg = st.Substring(idx + 1); st = st.Substring(0, idx); } opt.FoundOption = options .FirstOrDefault(o => o.Value.CompareTo(st)).Value; if (opt.FoundOption == null) { opt.FoundOption = AllOptions .FirstOrDefault(o => o.Value.CompareTo(st)).Value; } if (opt.FoundOption == null) { opt.IsUnknown = true; } else { opt.FoundOption = opt.FoundOption.Clone(); } return opt; } public ArgumentCollection Parse(string[] rawArgs) { ArgumentCollection options = AllOptions.CreateDefault(); List restList = options.UnnamedArguments.ToList(); for (int i = 0; i < rawArgs.Length; i++) { string arg = rawArgs[i]; optionFindResult optResult = findOption(arg, options); Argument opt = optResult.FoundOption; if (opt == null) { if (optResult.IsUnknown == false) { restList.Add(arg); } else { options.UnknownsOptions.Add(arg); } continue; } if (opt.NeedsArgument) { if (optResult.InlineArg != null) { opt.Values.Add(optResult.InlineArg); } else if (i + 1 < rawArgs.Length) { opt.Values.Add(rawArgs[++i]); } else { options.MissingArguments.Add(opt); continue; } } else { if (optResult.InlineArg != null) { // Add the value of the inline arg, even if NeedsArgument was not specified opt.Values.Add(optResult.InlineArg); } } options.Add(opt); } options.UnnamedArguments = restList.ToArray(); return options; } } internal class Argument { /// /// Optional. Used with one '-' or a '/'. /// public char ShortName { get; private set; } /// /// Non optional. used with '--' or '/'. Also used for argument lookup. /// public string LongName { get; private set; } /// /// If an argument is required for this option. /// public bool NeedsArgument { get; private set; } /// /// Argument given to this option. /// public string Value { get { return Values.FirstOrDefault(); } } /// /// Argument given to this option. Also used as a default. /// public List Values { get; private set; } /// /// Short description for this option. /// public string Description { get; set; } /// /// Indicates if an argument should be shown in "--help" output. /// public bool IsVisible { get; set; } = true; /// /// Initializes a new instance of the Option class. /// public Argument(string longName, char shortName = default(char), bool needsArgument = true, string description = "", string defaultArg = null) { ShortName = shortName; LongName = longName; NeedsArgument = needsArgument; Description = description; Values = new List(); if (String.IsNullOrWhiteSpace(defaultArg) == false) { Values.Add(defaultArg); } } /// /// Clones the option with a new argument /// /// /// public Argument Clone(string argument) { var opt = Clone(); opt.Values = new List { argument }; return opt; } public Argument Clone() { return new Argument(LongName, ShortName, NeedsArgument) { Values = new List(Values), Description = Description }; } public bool CompareTo(string arg) { if (arg.Length == 1 && ShortName != default(char)) { return arg[0] == ShortName; } return arg == LongName; } } /// /// A collection of options optionally with arguments. /// Also includes Unnamed arguments and in case of errors unknown options and missing arguments /// This class is used both as and input and output to option parsing. /// internal class ArgumentCollection : Dictionary { public string[] UnnamedArguments { get; set; } public List UnnamedArgumentData { get; set; } public List UnknownsOptions { get; set; } public List MissingArguments { get; set; } public ArgumentCollection() { UnnamedArguments = new string[0]; UnknownsOptions = new List(); MissingArguments = new List(); } public Argument Add(Argument option) { this[option.LongName] = option; return option; } public Argument Add(string longName, char shortName = default(char), bool needsArgument = true, string description = "", string defaultArgument = null) { var option = new Argument(longName, shortName, needsArgument, description, defaultArgument); return Add(option); } public bool Contains(string optionName) { return ContainsKey(optionName); } public Argument GetOrDefault(string optionLongName) { if (Contains(optionLongName)) { return this[optionLongName]; } return null; } public string Argument(string optionLongName) { return this[optionLongName].Value; } public string GetArgumentOrDefault(string optionLongName, string def = default(string)) { var opt = GetOrDefault(optionLongName); if (opt != null) { return opt.Value; } return def; } /// /// Transfers an option from one /// /// /// /// public Argument TakeOption(string optionName, ArgumentCollection from) { if (from.Contains(optionName)) { this[optionName] = from[optionName].Clone(); return this[optionName]; } return null; } public ArgumentCollection CreateDefault() { ArgumentCollection opt = new ArgumentCollection { UnnamedArguments = UnnamedArguments }; foreach (var key in Keys) { if (this[key].Value != null) { opt[key] = this[key]; } } return opt; } /// /// This is a simple abstraction over unnamed and named arguments to simplify output formatting. /// /// class OptionWrapper { public UnnamedCommandLineArgument Positional { get; } public Argument Argument { get; } public OptionWrapper(Argument argument) { Argument = argument; } public OptionWrapper(UnnamedCommandLineArgument positional) { Positional = positional; } public string Description() { return Positional != null ? Positional.Description : Argument.Description; } public override string ToString() { if (Positional != null) { return " " + (Positional.Required ? $"<{Positional.Name}>" : $"[{Positional.Name}]"); } else { var result = " "; if (Argument.ShortName != default(char)) { result += $"-{Argument.ShortName}, "; } result += $"--{Argument.LongName}"; return result; } } } /// /// Converts the ArgumentCollection to a help-string. /// /// public override string ToString() { StringBuilder output = new StringBuilder(); var wrappers = new List(); if (UnnamedArgumentData?.Any() == true) { foreach (var u in UnnamedArgumentData) { if (u.GetAttribute() is { } a) { wrappers.Add(new OptionWrapper(a)); } } } wrappers.AddRange(this.Values.Where(v => v.IsVisible).Select(k => new OptionWrapper(k))); // Compute the options' width int width = wrappers.Select(w => w.ToString().Length).Max() + 3; foreach (var wrapper in wrappers) { var arg = wrapper.ToString(); var description = wrapper.Description(); if (!string.IsNullOrEmpty(description)) { output.Append(arg); // Offst by the arument length in the first iteration var offset = arg.Length; foreach (var descSplit in description.Split('\n')) { output.AppendLine(descSplit.PadLeft(descSplit.Length + width - offset)); offset = 0; } } else { output.AppendLine(arg); } } return output.ToString(); } } }