using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
namespace ZipTools {
class CommandArgument : IEquatable<CommandArgument> {
public string Name { get; set; }
public string LongName { get; set; }
public string Description { get; set; }
public uint Flags { get; set; }
public string ParameterName { get; set; }
public Action<CommandParser, string> Action { get; set; }
public bool Equals(CommandArgument other) {
bool equals = false;
if (this.Name.Equals(other.Name) &&
this.LongName.Equals(other.LongName) &&
this.Description.Equals(other.Description) &&
this.Flags == other.Flags &&
this.ParameterName.Equals(other.ParameterName)) {
equals = true;
}
return equals;
}
}
class CommandAtom {
public CommandArgument Argument { get; set; }
public string Parameter { get; set; }
}
public class CommandArgumentFlags {
public const uint None = 0x00000000;
public const uint TakesParameter = 0x00000001;
public const uint Required = 0x00000002;
public const uint HideInUsage = 0x00000004;
public static bool FlagEnabled(uint f0, uint f1) {
return (f0 & f1) != 0;
}
public static bool FlagDisabled(uint f0, uint f1) {
return (f0 & f1) == 0;
}
}
public class CommandParser {
private IList<CommandArgument> arguments =
new List<CommandArgument>();
private IList<string> unknownCommands =
new List<string>();
private IList<string> missingRequired =
new List<string>();
private IList<CommandAtom> dispatchCandidates =
new List<CommandAtom>();
//
// the argument prefix list is used to designate the set of values
// that are used to denote the start of an argument. Long versions
// are always assumed to be two instances of the string. Thus,
// if the short version is specified via "-", the long version
// would be specified via "--".
//
public char[] ArgumentPrefixList { get; set; }
//
// shown on the help screen
//
public string ApplicationDescription { get; set; }
//
// this gets populated during a Parse() operation accumulating the list
// of commands that were supplied that are not understood by the parser.
//
public IList<string> UnknownCommands { get { return this.unknownCommands; } }
public IList<string> MissingRequiredCommands { get { return this.missingRequired; } }
public CommandParser() {
this.ArgumentPrefixList = new char[] { '-', '/' };
}
public CommandParser(string appDescription) {
this.ApplicationDescription = appDescription;
this.ArgumentPrefixList = new char[] { '-', '/' };
}
//
// specifying a longName for a command argument is not optional
// on purpose. it takes very little effort to specify one when building
// a tool, and it enhances the understandability of the tool greatly
// if good long names are chosen when someone reads the tool's help screen.
//
public void Argument(string name,
string longName,
string description,
Action<CommandParser, string> action) {
Argument(name,
longName,
description,
String.Empty,
CommandArgumentFlags.None,
action);
}
public void Argument(string name,
string longName,
string description,
uint flags,
Action<CommandParser, string> action) {
Argument(name,
longName,
description,
String.Empty,
flags,
action);
}
public void Argument(string name,
string longName,
string description,
string paramName,
uint flags,
Action<CommandParser, string> action) {
if (!ValidateArgument(name)) {
throw new ArgumentException("Invalid command argument 'name' = " + name);
}
if (!ValidateArgument(longName)) {
throw new ArgumentException("Invalid command argument 'longName' = " + longName);
}
this.arguments.Add(new CommandArgument() {
Name = name,
LongName = longName,
Description = description,
ParameterName = paramName,
Flags = flags,
Action = action,
});
}
public virtual void Parse() {
this.Parse(Environment.GetCommandLineArgs());
}
public virtual void Parse(string[] args) {
//
// This parser attempts to emulate, roughly, the behavior
// of the POSIX getopt C runtime function for parsing
// command line arguments. This mechanism is fairly
// easy to use as it is quite flexible in how it
// lets you submit arguments for parsing.
//
// For example, all of these would be valid and equivalent
// command line arguments if you had flags
// p, q, and z where z takes an argument.
//
// -p -q -z7
// -p -q -z 7
// -pqz7
// -p -qz7
//
// -p -qz "7"
// -p -qz"7"
//
// The main difference between this parser and getopt, however,
// is that with getopt you have to do command handling dispatch
// yourself in a big switch statement. This parser does
// the dispatching automatically leveraging C#'s Action<> convention.
//
// This parser also provides a slightly more cumbersome syntax for
// specifying arguments, but by paying this syntax tax, you get the
// benefit of a help screen that can be generated automatically
// for you based on the list of command arguments you supply to the
// parser. This reduces the common burden a writer of a command line
// tool has. It also ensures that the help screen for the application
// is always up to date whenever new flags or arguments are added
// to the tool.
//
//
// reset the tracking collections for unknown and missing
// required commands
//
ResetTrackingCollections();
//
// first, we merge the whole command line into a single string
// since we're going to have to parse char by char
//
var joined = String.Join(" ", args.Skip(1).ToArray());
//
// we keep track of all commands dispatched to determine if
// any commands that are required were not supplied
//
var dispatchedCommands = new List<CommandArgument>();
//
// these are the state variables that are used to track what's
// going on in the command line as we walk character by character
// through it.
//
bool isLongArg = false;
var argBuffer = String.Empty;
CommandArgument currentCommand = null;
//
// now we walk through the characters of the array until
// we determine if we've found a matching switch
//
for (int i = 0; i < joined.Length; i++) {
if (IsArgStart(joined, i)) {
//
// if we've reached a new arg, but there is a current
// command, that means we've been gathering a parameter
// for it and it needs to be queued now.
//
if (currentCommand != null) {
dispatchedCommands.Add(currentCommand);
currentCommand = QueueCommand(currentCommand, argBuffer);
} else if ((currentCommand == null) && !String.IsNullOrEmpty(argBuffer.Trim())) {
HandleUnknownCommandEnding(argBuffer);
}
//
// now that we're moving on to something new, we clear out
// the argument buffer.
//
argBuffer = joined[i].ToString();
//
// we check if we're about to deal with a long argument
//
isLongArg = IsLongArg(joined, i);
if (isLongArg) {
argBuffer += joined[i + 1];
i++;
}
} else if (currentCommand == null) {
argBuffer += joined[i];
currentCommand = GetCommand(argBuffer, isLongArg);
if (currentCommand != null) {
argBuffer = String.Empty;
//
// if the current command doesn't take a parameter,
// then we just dispatch it to it's handler
//
if (CommandArgumentFlags.FlagDisabled(currentCommand.Flags,
CommandArgumentFlags.TakesParameter)) {
dispatchedCommands.Add(currentCommand);
currentCommand = QueueCommand(currentCommand, String.Empty);
}
}
} else if (currentCommand != null) {
argBuffer += joined[i];
}
}
//
// if we exit the loop, and there's still a command waiting to
// be dispatched, then we've been gathering the parameter to the
// end of the string, so we need to dispatch it now
//
if (currentCommand != null) {
dispatchedCommands.Add(currentCommand);
currentCommand = QueueCommand(currentCommand, argBuffer);
} else if ((currentCommand == null) && !String.IsNullOrEmpty(argBuffer.Trim())) {
HandleUnknownCommandEnding(argBuffer);
}
//
// now that we're done with all the dispatching, we need to determine
// if there were any required commands that didn't get supplied
// and store that set for the caller to use
//
this.missingRequired = DetermineMissingRequiredCommands(dispatchedCommands);
//
// finally, actually dispatch everything that was accumulated
//
foreach (var atom in this.dispatchCandidates) {
atom.Argument.Action(this, atom.Parameter);
}
}
public string GetHelp() {
StringBuilder text = new StringBuilder();
var appName = System.AppDomain.CurrentDomain.FriendlyName.ToLower();
WriteLine(text, String.Empty);
//
// write out the application header
//
if (!String.IsNullOrEmpty(this.ApplicationDescription)) {
WriteLine(text, appName + " - " + this.ApplicationDescription);
} else {
WriteLine(text, appName);
}
//
// write out the usage string
//
WriteLine(text, String.Empty);
WriteLine(text, GetUsageString(appName));
WriteLine(text, String.Empty);
//
// write out the commands
//
WriteLine(text, "Available commands:");
WriteLine(text, "-------------------");
//
// figure out the longest command expression
//
var exprLength = this.arguments.Select(c => (GetCommandDisplayName(c.Name).Length +
GetCommandDisplayLongName(c.LongName).Length)).Max();
foreach (var command in this.arguments) {
WriteLine(text,
GetCommandHelpDisplay(command.Name, command.LongName).PadRight(exprLength + 5, ' ') +
command.Description);
}
return text.ToString();
}
private void HandleUnknownCommandEnding(string argBuffer) {
//
// if we're being told the passed in argBuffer wasn't mapped to a command,
// then if the previously handled command takes an argument, this is part of that
// argument, so we append the input argBuffer to that previous command's argument,
// along with the flag that triggered this to happen in the first place.
//
if ((this.dispatchCandidates.Count > 0) &&
(CommandArgumentFlags.FlagEnabled(this.dispatchCandidates[this.dispatchCandidates.Count - 1].Argument.Flags,
CommandArgumentFlags.TakesParameter))) {
dispatchCandidates[this.dispatchCandidates.Count - 1].Parameter += argBuffer.Trim();
} else {
//
// otherwise, we don't know what the heck this command is
//
this.unknownCommands.Add(argBuffer.Trim());
}
}
private bool IsArgStart(string joined, int index) {
return this.ArgumentPrefixList.Contains(joined[index]);
}
private bool IsLongArg(string joined, int index) {
bool isLong = false;
if (((index + 1) < joined.Length) &&
this.ArgumentPrefixList.Contains(joined[index + 1])) {
isLong = true;
}
return isLong;
}
private CommandArgument QueueCommand(CommandArgument ca,
string param) {
this.dispatchCandidates.Add(new CommandAtom() {
Argument = ca,
Parameter = param.Trim()
});
// we return null on purpose here
return null;
}
private CommandArgument GetCommand(string argBuffer, bool useLong) {
CommandArgument ca = null;
string strippedArg = argBuffer.Replace("-", String.Empty);
if (!useLong) {
ca = this.arguments.Where(a => a.Name.Equals(strippedArg)).FirstOrDefault();
} else {
ca = this.arguments.Where(a => a.LongName.Equals(strippedArg)).FirstOrDefault();
}
return ca;
}
private string GetCommandDisplayName(string c) {
return this.ArgumentPrefixList[0] + c;
}
private string GetCommandDisplayLongName(string c) {
var sb = new StringBuilder();
sb.Append(this.ArgumentPrefixList[0]);
sb.Append(this.ArgumentPrefixList[0]);
sb.Append(c);
return sb.ToString();
}
private string GetUsageString(string appName) {
var sb = new StringBuilder();
//
// usage start
//
sb.Append("Usage: ");
sb.Append(appName);
sb.Append(' ');
//
// required arguments
//
var required = this.arguments.Where(a =>
CommandArgumentFlags.FlagEnabled(a.Flags, CommandArgumentFlags.Required) &&
CommandArgumentFlags.FlagDisabled(a.Flags, CommandArgumentFlags.HideInUsage)).ToList();
if (required.Count > 0) {
AppendArgumentsToUsage(sb, required);
sb.Append(" ");
}
//
// optional arguments
//
var optional = this.arguments.Where(a =>
CommandArgumentFlags.FlagDisabled(a.Flags, CommandArgumentFlags.Required) &&
CommandArgumentFlags.FlagDisabled(a.Flags, CommandArgumentFlags.HideInUsage)).ToList();
if (optional.Count > 0) {
sb.Append("[");
AppendArgumentsToUsage(sb, optional);
sb.Append("]");
}
return sb.ToString();
}
private void AppendArgumentsToUsage(StringBuilder sb, IList<CommandArgument> arguments) {
foreach (var opt in arguments) {
sb.Append(GetCommandDisplayName(opt.Name));
if (CommandArgumentFlags.FlagEnabled(opt.Flags, CommandArgumentFlags.TakesParameter)) {
if (!String.IsNullOrEmpty(opt.ParameterName)) {
sb.Append(" <");
sb.Append(opt.ParameterName);
sb.Append(">");
} else {
sb.Append(" <arg>");
}
}
sb.Append(' ');
}
sb.Remove(sb.Length - 1, 1);
}
private string GetCommandHelpDisplay(string name, string longName) {
return GetCommandDisplayName(name) + ", " +
GetCommandDisplayLongName(longName);
}
private bool ValidateArgument(string arg) {
bool valid = true;
if (!String.IsNullOrEmpty(arg)) {
foreach (var prefix in this.ArgumentPrefixList) {
if (arg.Contains(prefix)) {
valid = false;
break;
}
}
} else {
valid = false;
}
return valid;
}
private void ResetTrackingCollections() {
this.unknownCommands = new List<string>();
this.missingRequired = new List<string>();
this.dispatchCandidates = new List<CommandAtom>();
}
private IList<string> DetermineMissingRequiredCommands(IList<CommandArgument> dispatchedCommands) {
IList<string> missing = new List<string>();
//
// figure out which arguments are required
//
var required =
this.arguments.Where(a =>
CommandArgumentFlags.FlagEnabled(a.Flags, CommandArgumentFlags.Required)).ToList();
if (required.Count > 0) {
//
// if we actually have some required arguments, then some might
// not have been dispatched, which means they're missing
//
foreach (var requiredCommand in required) {
if (!dispatchedCommands.Contains(requiredCommand)) {
missing.Add(requiredCommand.LongName);
}
}
}
return missing;
}
private static void WriteLine(StringBuilder sb, string s) {
sb.Append(s);
sb.Append(Environment.NewLine);
}
}
}