/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Xml;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using log4net;
using OpenSim.Framework;
using Nini.Config;
namespace OpenSim.Framework.Console
{
public class Commands : ICommands
{
// private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
///
/// Encapsulates a command that can be invoked from the console
///
private class CommandInfo
{
///
/// The module from which this command comes
///
public string module;
///
/// Whether the module is shared
///
public bool shared;
///
/// Very short BNF description
///
public string help_text;
///
/// Longer one line help text
///
public string long_help;
///
/// Full descriptive help for this command
///
public string descriptive_help;
///
/// The method to invoke for this command
///
public List fn;
}
public const string GeneralHelpText
= "To enter an argument that contains spaces, surround the argument with double quotes.\nFor example, show object name \"My long object name\"\n";
public const string ItemHelpText
= @"For more information, type 'help all' to get a list of all commands,
or type help - ' where
- is one of the following:";
///
/// Commands organized by keyword in a tree
///
private Dictionary tree =
new Dictionary();
///
/// Commands organized by module
///
private Dictionary> m_modulesCommands = new Dictionary>();
///
/// Get help for the given help string
///
/// Parsed parts of the help string. If empty then general help is returned.
///
public List GetHelp(string[] cmd)
{
List help = new List();
List helpParts = new List(cmd);
// Remove initial help keyword
helpParts.RemoveAt(0);
help.Add(""); // Will become a newline.
// General help
if (helpParts.Count == 0)
{
help.Add(GeneralHelpText);
help.Add(ItemHelpText);
help.AddRange(CollectModulesHelp(tree));
}
else if (helpParts.Count == 1 && helpParts[0] == "all")
{
help.AddRange(CollectAllCommandsHelp());
}
else
{
help.AddRange(CollectHelp(helpParts));
}
help.Add(""); // Will become a newline.
return help;
}
///
/// Collects the help from all commands and return in alphabetical order.
///
///
private List CollectAllCommandsHelp()
{
List help = new List();
lock (m_modulesCommands)
{
foreach (List commands in m_modulesCommands.Values)
{
var ourHelpText = commands.ConvertAll(c => string.Format("{0} - {1}", c.help_text, c.long_help));
help.AddRange(ourHelpText);
}
}
help.Sort();
return help;
}
///
/// See if we can find the requested command in order to display longer help
///
///
///
private List CollectHelp(List helpParts)
{
string originalHelpRequest = string.Join(" ", helpParts.ToArray());
List help = new List();
// Check modules first to see if we just need to display a list of those commands
if (TryCollectModuleHelp(originalHelpRequest, help))
{
help.Insert(0, ItemHelpText);
return help;
}
Dictionary dict = tree;
while (helpParts.Count > 0)
{
string helpPart = helpParts[0];
if (!dict.ContainsKey(helpPart))
break;
//m_log.Debug("Found {0}", helpParts[0]);
if (dict[helpPart] is Dictionary)
dict = (Dictionary)dict[helpPart];
helpParts.RemoveAt(0);
}
// There was a command for the given help string
if (dict.ContainsKey(String.Empty))
{
CommandInfo commandInfo = (CommandInfo)dict[String.Empty];
help.Add(commandInfo.help_text);
help.Add(commandInfo.long_help);
string descriptiveHelp = commandInfo.descriptive_help;
// If we do have some descriptive help then insert a spacing line before for readability.
if (descriptiveHelp != string.Empty)
help.Add(string.Empty);
help.Add(commandInfo.descriptive_help);
}
else
{
help.Add(string.Format("No help is available for {0}", originalHelpRequest));
}
return help;
}
///
/// Try to collect help for the given module if that module exists.
///
///
/// /param>
/// true if there was the module existed, false otherwise.
private bool TryCollectModuleHelp(string moduleName, List helpText)
{
lock (m_modulesCommands)
{
foreach (string key in m_modulesCommands.Keys)
{
// Allow topic help requests to succeed whether they are upper or lowercase.
if (moduleName.ToLower() == key.ToLower())
{
List commands = m_modulesCommands[key];
var ourHelpText = commands.ConvertAll(c => string.Format("{0} - {1}", c.help_text, c.long_help));
ourHelpText.Sort();
helpText.AddRange(ourHelpText);
return true;
}
}
return false;
}
}
private List CollectModulesHelp(Dictionary dict)
{
lock (m_modulesCommands)
{
List helpText = new List(m_modulesCommands.Keys);
helpText.Sort();
return helpText;
}
}
// private List CollectHelp(Dictionary dict)
// {
// List result = new List();
//
// foreach (KeyValuePair kvp in dict)
// {
// if (kvp.Value is Dictionary)
// {
// result.AddRange(CollectHelp((Dictionary)kvp.Value));
// }
// else
// {
// if (((CommandInfo)kvp.Value).long_help != String.Empty)
// result.Add(((CommandInfo)kvp.Value).help_text+" - "+
// ((CommandInfo)kvp.Value).long_help);
// }
// }
// return result;
// }
///
/// Add a command to those which can be invoked from the console.
///
///
///
///
///
///
public void AddCommand(string module, bool shared, string command,
string help, string longhelp, CommandDelegate fn)
{
AddCommand(module, shared, command, help, longhelp, String.Empty, fn);
}
///
/// Add a command to those which can be invoked from the console.
///
///
///
///
///
///
///
public void AddCommand(string module, bool shared, string command,
string help, string longhelp, string descriptivehelp,
CommandDelegate fn)
{
string[] parts = Parser.Parse(command);
Dictionary current = tree;
foreach (string part in parts)
{
if (current.ContainsKey(part))
{
if (current[part] is Dictionary)
current = (Dictionary)current[part];
else
return;
}
else
{
current[part] = new Dictionary();
current = (Dictionary)current[part];
}
}
CommandInfo info;
if (current.ContainsKey(String.Empty))
{
info = (CommandInfo)current[String.Empty];
if (!info.shared && !info.fn.Contains(fn))
info.fn.Add(fn);
return;
}
info = new CommandInfo();
info.module = module;
info.shared = shared;
info.help_text = help;
info.long_help = longhelp;
info.descriptive_help = descriptivehelp;
info.fn = new List();
info.fn.Add(fn);
current[String.Empty] = info;
// Now add command to modules dictionary
lock (m_modulesCommands)
{
List commands;
if (m_modulesCommands.ContainsKey(module))
{
commands = m_modulesCommands[module];
}
else
{
commands = new List();
m_modulesCommands[module] = commands;
}
// m_log.DebugFormat("[COMMAND CONSOLE]: Adding to category {0} command {1}", module, command);
commands.Add(info);
}
}
public string[] FindNextOption(string[] cmd, bool term)
{
Dictionary current = tree;
int remaining = cmd.Length;
foreach (string s in cmd)
{
remaining--;
List found = new List();
foreach (string opt in current.Keys)
{
if (remaining > 0 && opt == s)
{
found.Clear();
found.Add(opt);
break;
}
if (opt.StartsWith(s))
{
found.Add(opt);
}
}
if (found.Count == 1 && (remaining != 0 || term))
{
current = (Dictionary)current[found[0]];
}
else if (found.Count > 0)
{
return found.ToArray();
}
else
{
break;
// return new string[] {""};
}
}
if (current.Count > 1)
{
List choices = new List();
bool addcr = false;
foreach (string s in current.Keys)
{
if (s.Length == 0)
{
CommandInfo ci = (CommandInfo)current[String.Empty];
if (ci.fn.Count != 0)
addcr = true;
}
else
choices.Add(s);
}
if (addcr)
choices.Add("");
return choices.ToArray();
}
if (current.ContainsKey(String.Empty))
return new string[] { "Command help: "+((CommandInfo)current[String.Empty]).help_text};
return new string[] { new List(current.Keys)[0] };
}
private CommandInfo ResolveCommand(string[] cmd, out string[] result)
{
result = cmd;
int index = -1;
Dictionary current = tree;
foreach (string s in cmd)
{
index++;
List found = new List();
foreach (string opt in current.Keys)
{
if (opt == s)
{
found.Clear();
found.Add(opt);
break;
}
if (opt.StartsWith(s))
{
found.Add(opt);
}
}
if (found.Count == 1)
{
result[index] = found[0];
current = (Dictionary)current[found[0]];
}
else if (found.Count > 0)
{
return null;
}
else
{
break;
}
}
if (current.ContainsKey(String.Empty))
return (CommandInfo)current[String.Empty];
return null;
}
public bool HasCommand(string command)
{
string[] result;
return ResolveCommand(Parser.Parse(command), out result) != null;
}
public string[] Resolve(string[] cmd)
{
string[] result;
CommandInfo ci = ResolveCommand(cmd, out result);
if (ci == null)
return new string[0];
if (ci.fn.Count == 0)
return new string[0];
foreach (CommandDelegate fn in ci.fn)
{
if (fn != null)
fn(ci.module, result);
else
return new string[0];
}
return result;
}
public XmlElement GetXml(XmlDocument doc)
{
CommandInfo help = (CommandInfo)((Dictionary)tree["help"])[String.Empty];
((Dictionary)tree["help"]).Remove(string.Empty);
if (((Dictionary)tree["help"]).Count == 0)
tree.Remove("help");
CommandInfo quit = (CommandInfo)((Dictionary)tree["quit"])[String.Empty];
((Dictionary)tree["quit"]).Remove(string.Empty);
if (((Dictionary)tree["quit"]).Count == 0)
tree.Remove("quit");
XmlElement root = doc.CreateElement("", "HelpTree", "");
ProcessTreeLevel(tree, root, doc);
if (!tree.ContainsKey("help"))
tree["help"] = (object) new Dictionary();
((Dictionary)tree["help"])[String.Empty] = help;
if (!tree.ContainsKey("quit"))
tree["quit"] = (object) new Dictionary();
((Dictionary)tree["quit"])[String.Empty] = quit;
return root;
}
private void ProcessTreeLevel(Dictionary level, XmlElement xml, XmlDocument doc)
{
foreach (KeyValuePair kvp in level)
{
if (kvp.Value is Dictionary)
{
XmlElement next = doc.CreateElement("", "Level", "");
next.SetAttribute("Name", kvp.Key);
xml.AppendChild(next);
ProcessTreeLevel((Dictionary)kvp.Value, next, doc);
}
else
{
CommandInfo c = (CommandInfo)kvp.Value;
XmlElement cmd = doc.CreateElement("", "Command", "");
XmlElement e;
e = doc.CreateElement("", "Module", "");
cmd.AppendChild(e);
e.AppendChild(doc.CreateTextNode(c.module));
e = doc.CreateElement("", "Shared", "");
cmd.AppendChild(e);
e.AppendChild(doc.CreateTextNode(c.shared.ToString()));
e = doc.CreateElement("", "HelpText", "");
cmd.AppendChild(e);
e.AppendChild(doc.CreateTextNode(c.help_text));
e = doc.CreateElement("", "LongHelp", "");
cmd.AppendChild(e);
e.AppendChild(doc.CreateTextNode(c.long_help));
e = doc.CreateElement("", "Description", "");
cmd.AppendChild(e);
e.AppendChild(doc.CreateTextNode(c.descriptive_help));
xml.AppendChild(cmd);
}
}
}
public void FromXml(XmlElement root, CommandDelegate fn)
{
CommandInfo help = (CommandInfo)((Dictionary)tree["help"])[String.Empty];
((Dictionary)tree["help"]).Remove(string.Empty);
if (((Dictionary)tree["help"]).Count == 0)
tree.Remove("help");
CommandInfo quit = (CommandInfo)((Dictionary)tree["quit"])[String.Empty];
((Dictionary)tree["quit"]).Remove(string.Empty);
if (((Dictionary)tree["quit"]).Count == 0)
tree.Remove("quit");
tree.Clear();
ReadTreeLevel(tree, root, fn);
if (!tree.ContainsKey("help"))
tree["help"] = (object) new Dictionary();
((Dictionary)tree["help"])[String.Empty] = help;
if (!tree.ContainsKey("quit"))
tree["quit"] = (object) new Dictionary();
((Dictionary)tree["quit"])[String.Empty] = quit;
}
private void ReadTreeLevel(Dictionary level, XmlNode node, CommandDelegate fn)
{
Dictionary next;
string name;
XmlNodeList nodeL = node.ChildNodes;
XmlNodeList cmdL;
CommandInfo c;
foreach (XmlNode part in nodeL)
{
switch (part.Name)
{
case "Level":
name = ((XmlElement)part).GetAttribute("Name");
next = new Dictionary();
level[name] = next;
ReadTreeLevel(next, part, fn);
break;
case "Command":
cmdL = part.ChildNodes;
c = new CommandInfo();
foreach (XmlNode cmdPart in cmdL)
{
switch (cmdPart.Name)
{
case "Module":
c.module = cmdPart.InnerText;
break;
case "Shared":
c.shared = Convert.ToBoolean(cmdPart.InnerText);
break;
case "HelpText":
c.help_text = cmdPart.InnerText;
break;
case "LongHelp":
c.long_help = cmdPart.InnerText;
break;
case "Description":
c.descriptive_help = cmdPart.InnerText;
break;
}
}
c.fn = new List();
c.fn.Add(fn);
level[String.Empty] = c;
break;
}
}
}
}
public class Parser
{
// If an unquoted portion ends with an element matching this regex
// and the next element contains a space, then we have stripped
// embedded quotes that should not have been stripped
private static Regex optionRegex = new Regex("^--[a-zA-Z0-9-]+=$");
public static string[] Parse(string text)
{
List result = new List();
int index;
string[] unquoted = text.Split(new char[] {'"'});
for (index = 0 ; index < unquoted.Length ; index++)
{
if (index % 2 == 0)
{
string[] words = unquoted[index].Split();
bool option = false;
foreach (string w in words)
{
if (w != String.Empty)
{
if (optionRegex.Match(w) == Match.Empty)
option = false;
else
option = true;
result.Add(w);
}
}
// The last item matched the regex, put the quotes back
if (option)
{
// If the line ended with it, don't do anything
if (index < (unquoted.Length - 1))
{
// Get and remove the option name
string optionText = result[result.Count - 1];
result.RemoveAt(result.Count - 1);
// Add the quoted value back
optionText += "\"" + unquoted[index + 1] + "\"";
// Push the result into our return array
result.Add(optionText);
// Skip the already used value
index++;
}
}
}
else
{
result.Add(unquoted[index]);
}
}
return result.ToArray();
}
}
///
/// A console that processes commands internally
///
public class CommandConsole : ConsoleBase, ICommandConsole
{
// private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public event OnOutputDelegate OnOutput;
public static event OnCntrCCelegate OnCntrC;
public ICommands Commands { get; private set; }
public CommandConsole(string defaultPrompt) : base(defaultPrompt)
{
Commands = new Commands();
Commands.AddCommand(
"Help", false, "help", "help [
- ]",
"Display help on a particular command or on a list of commands in a category", Help);
}
private void Help(string module, string[] cmd)
{
List help = Commands.GetHelp(cmd);
foreach (string s in help)
Output(s);
}
protected void FireOnOutput(string text)
{
OnOutput?.Invoke(text);
}
///
/// Display a command prompt on the console and wait for user input
///
public void Prompt()
{
string line = ReadLine(DefaultPrompt + "# ", true, true);
if (line != String.Empty)
Output("Invalid command");
}
public void RunCommand(string cmd)
{
string[] parts = Parser.Parse(cmd);
Commands.Resolve(parts);
}
public override string ReadLine(string p, bool isCommand, bool e)
{
System.Console.Write("{0}", p);
string cmdinput = System.Console.ReadLine();
if (isCommand)
{
string[] cmd = Commands.Resolve(Parser.Parse(cmdinput));
if (cmd.Length != 0)
{
int i;
for (i=0 ; i < cmd.Length ; i++)
{
if (cmd[i].Contains(" "))
cmd[i] = "\"" + cmd[i] + "\"";
}
return String.Empty;
}
}
return cmdinput;
}
public virtual void ReadConfig(IConfigSource configSource)
{
}
public virtual void SetCntrCHandler(OnCntrCCelegate handler)
{
if(OnCntrC == null)
{
OnCntrC += handler;
System.Console.CancelKeyPress += CancelKeyPressed;
}
}
protected static void CancelKeyPressed(object sender, ConsoleCancelEventArgs args)
{
if (OnCntrC != null && args.SpecialKey == ConsoleSpecialKey.ControlC)
{
OnCntrC?.Invoke();
args.Cancel = false;
}
}
protected static void LocalCancelKeyPressed()
{
OnCntrC?.Invoke();
}
}
}