// 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();
}
}
}