File: HelpCommand.cs
Web Access
Project: src\src\Microsoft.ML.Maml\Microsoft.ML.Maml.csproj (Microsoft.ML.Maml)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Microsoft.ML;
using Microsoft.ML.Command;
using Microsoft.ML.CommandLine;
using Microsoft.ML.Internal.Utilities;
using Microsoft.ML.Runtime;
using Microsoft.ML.Tools;
 
[assembly: LoadableClass(HelpCommand.Summary, typeof(HelpCommand), typeof(HelpCommand.Arguments), typeof(SignatureCommand),
    "MAML Help Command", "Help", "?")]
 
[assembly: LoadableClass(typeof(XmlGenerator), typeof(XmlGenerator.Arguments), typeof(SignatureModuleGenerator),
    "Xml generator", "XmlGenerator", "Xml")]
 
namespace Microsoft.ML.Tools
{
    [BestFriend]
    internal interface IGenerator
    {
        void Generate(IEnumerable<HelpCommand.Component> infos);
    }
 
    public delegate void SignatureModuleGenerator(string regenerate);
 
    internal sealed class HelpCommand : ICommand
    {
        public sealed class Arguments
        {
#pragma warning disable 649 // never assigned
            [DefaultArgument(ArgumentType.AtMostOnce, HelpText = "The component name to get help for")]
            public string Component;
 
            [Argument(ArgumentType.AtMostOnce, HelpText = "The kind of component to look for", ShortName = "kind")]
            public string Kind;
 
            [Argument(ArgumentType.AtMostOnce, HelpText = "List the component kinds", ShortName = "list")]
            public bool ListKinds;
 
            [Argument(ArgumentType.AtMostOnce, ShortName = "all", Hide = true)]
            public bool AllComponents;
 
            // extra DLLs for dynamic loading
            [Argument(ArgumentType.Multiple, HelpText = "Extra DLLs", ShortName = "dll")]
            public string[] ExtraAssemblies;
 
            [Argument(ArgumentType.LastOccurrenceWins, Hide = true, SignatureType = typeof(SignatureModuleGenerator))]
            public IComponentFactory<string, IGenerator> Generator;
#pragma warning restore 649 // never assigned
        }
 
        internal const string Summary = "Prints command line help.";
 
        private readonly IHostEnvironment _env;
        private readonly string _component;
        private readonly string _kind;
        private readonly bool _listKinds;
        private readonly bool _allComponents;
        private readonly string[] _extraAssemblies;
        private readonly IGenerator _generator;
 
        // REVIEW: Need to change the help command to use the provided host environment for output,
        // instead of assuming the console.
        public HelpCommand(IHostEnvironment env, Arguments args)
        {
            Contracts.CheckValue(env, nameof(env));
            env.CheckValue(args, nameof(args));
 
            _env = env;
            _component = args.Component;
            if (string.IsNullOrWhiteSpace(_component))
                _component = null;
 
            _kind = args.Kind;
            if (string.IsNullOrWhiteSpace(_kind))
                _kind = null;
 
            _listKinds = args.ListKinds;
            _allComponents = args.AllComponents;
 
            _extraAssemblies = args.ExtraAssemblies;
 
            if (args.Generator != null)
            {
                _generator = args.Generator.CreateComponent(_env, "maml.exe ? " + CmdParser.GetSettings(env, args, new Arguments()));
            }
        }
 
        public void Run()
        {
            Run(null);
        }
 
        public void Run(int? columns)
        {
#pragma warning disable CS0618 // The help command should be entirely within the command line anyway.
            AssemblyLoadingUtils.LoadAndRegister(_env, _extraAssemblies);
#pragma warning restore CCS0618
 
            using (var ch = _env.Start("Help"))
            using (var sw = new StringWriter(CultureInfo.InvariantCulture))
            using (var writer = new IndentedTextWriter(sw, "  "))
            {
                if (_listKinds)
                {
                    if (_component != null)
                        writer.WriteLine("Listing component kinds so ignoring specified component");
                    else if (_kind != null)
                        writer.WriteLine("Listing component kinds so ignoring specified kind");
                    ListKinds(writer);
                }
                else if (_component != null)
                    ShowHelp(writer, columns);
                else if (_allComponents)
                    ShowAllHelp(writer, columns);
                else
                    ShowComponents(writer);
 
                ch.Info(sw.ToString());
            }
        }
 
        private void ShowHelp(IndentedTextWriter writer, int? columns = null)
        {
            _env.AssertValue(_component);
 
            string name = _component.Trim();
 
            string sig = _kind?.ToLowerInvariant();
 
            // Note that we don't check IsHidden here. The current policy is when IsHidden is true, we don't
            // show the item in "list all" functionality, but will still show help when explicitly requested.
 
            var infos = _env.ComponentCatalog.FindLoadableClasses(name)
                .OrderBy(x => ComponentCatalog.SignatureToString(x.SignatureTypes[0]).ToLowerInvariant());
            var kinds = new StringBuilder();
            var components = new List<Component>();
            foreach (var info in infos)
            {
                _env.AssertValue(info.SignatureTypes);
                kinds.Clear();
                bool foundSig = false;
                foreach (var signature in info.SignatureTypes)
                {
                    _env.Assert(signature.BaseType == typeof(MulticastDelegate));
 
                    string kind;
                    if (signature == typeof(SignatureDefault))
                    {
                        kind = "Component";
                        if (sig == null || "default".StartsWithInvariantCulture(sig))
                            foundSig = true;
                    }
                    else
                    {
                        kind = ComponentCatalog.SignatureToString(signature);
                        if (sig == null || kind.StartsWithInvariantCultureIgnoreCase(sig))
                            foundSig = true;
                    }
 
                    if (kinds.Length > 0)
                        kinds.Append(", ");
                    kinds.Append(kind);
                }
                if (foundSig)
                {
                    string kindsStr = kinds.ToString();
                    var args = info.CreateArguments();
 
                    ShowUsage(writer, kindsStr, info.Summary, info.LoadNames[0], info.LoadNames, args, columns);
                    components.Add(new Component(kindsStr, info, args));
                }
            }
 
            if (components.Count == 0)
                writer.WriteLine("Unknown component: '{0}'", name);
            else
                Serialize(components);
        }
 
        private void ShowAllHelp(IndentedTextWriter writer, int? columns = null)
        {
            string sig = _kind?.ToLowerInvariant();
 
            var infos = _env.ComponentCatalog.GetAllClasses()
                .OrderBy(info => info.LoadNames[0].ToLowerInvariant())
                .ThenBy(info => ComponentCatalog.SignatureToString(info.SignatureTypes[0]).ToLowerInvariant());
            var components = new List<Component>();
            foreach (var info in infos)
            {
                // REVIEW: We should only be printing the usage once, not for every signature.
                _env.AssertValue(info.SignatureTypes);
                foreach (var signature in info.SignatureTypes)
                {
                    _env.Assert(signature.BaseType == typeof(MulticastDelegate));
 
                    string kind = ComponentCatalog.SignatureToString(signature);
                    if (sig != null && !kind.StartsWithInvariantCultureIgnoreCase(sig))
                        continue;
 
                    // Don't show classes that have no arguments.
                    var args = info.CreateArguments();
                    if (args == null)
                        continue;
 
                    ShowUsage(writer, kind, info.Summary, info.LoadNames[0], info.LoadNames, args, columns);
                    components.Add(new Component(kind, info, args));
                }
            }
 
            if (components.Count > 0)
                Serialize(components);
        }
 
        private void ShowUsage(IndentedTextWriter writer, string kind, string summary, string loadName,
            IReadOnlyList<string> loadNames, object args, int? columns)
        {
            _env.Assert(loadName == loadNames[0]);
 
            writer.WriteLine("Help for {0}: '{1}'", kind, loadName);
            using (writer.Nest())
                ShowAliases(writer, loadNames);
 
            writer.WriteLine();
            ShowFormattedSummary(writer, summary, columns);
 
            if (args == null)
            {
                writer.WriteLine("Component '{0}' is not configurable", loadName);
                writer.WriteLine();
            }
            else
                writer.WriteLine(CmdParser.ArgumentsUsage(_env, args.GetType(), args, false, columns));
        }
 
        private void ShowComponents(IndentedTextWriter writer)
        {
            Type typeSig;
            Type typeRes;
            string kind;
 
            if (_kind == null)
            {
                // Show commands.
                typeSig = typeof(SignatureCommand);
                typeRes = typeof(ICommand);
                kind = "Command";
                writer.WriteLine("Available commands:");
            }
            else
            {
                kind = _kind.ToLowerInvariant();
                var sigs = _env.ComponentCatalog.GetAllSignatureTypes();
                typeSig = sigs.FirstOrDefault(t => ComponentCatalog.SignatureToString(t).ToLowerInvariant() == kind);
                if (typeSig == null)
                {
                    typeSig = sigs.FirstOrDefault(t => ComponentCatalog.SignatureToString(t).StartsWithInvariantCultureIgnoreCase(kind));
                    if (typeSig == null)
                    {
                        writer.WriteLine("Couldn't find kind '{0}'", kind);
                        ListKinds(writer);
                        return;
                    }
                }
                typeRes = typeof(object);
                writer.WriteLine("Available components for kind '{0}':", ComponentCatalog.SignatureToString(typeSig));
            }
 
            var infos = _env.ComponentCatalog.GetAllDerivedClasses(typeRes, typeSig)
                .Where(x => !x.IsHidden)
                .OrderBy(x => x.LoadNames[0].ToLowerInvariant());
            using (writer.Nest())
            {
                var components = new List<Component>();
                foreach (var info in infos)
                {
                    _env.Assert(info.LoadNames.Count > 0);
 
                    writer.Write("{0}", info.LoadNames[0]);
                    if (!string.IsNullOrWhiteSpace(info.UserName))
                        writer.Write(": {0}", info.UserName);
                    writer.WriteLine();
 
                    using (writer.Nest())
                        ShowAliases(writer, info.LoadNames);
                    components.Add(new Component(kind, info, info.CreateArguments()));
                }
 
                if (components.Count > 0)
                    Serialize(components);
            }
        }
 
        private void Serialize(List<Component> components)
        {
            _env.AssertValue(components);
 
            if (_generator != null)
                GenerateModule(components);
        }
 
        private void ShowAliases(IndentedTextWriter writer, IReadOnlyList<string> names)
        {
            if (names.Count <= 1)
                return;
 
            string pre = "Aliases: ";
            for (int i = 1; i < names.Count; i++)
            {
                writer.Write(pre);
                pre = ", ";
                writer.Write(names[i]);
            }
            writer.WriteLine();
        }
 
        private void ListKinds(IndentedTextWriter writer)
        {
            var sigs = _env.ComponentCatalog.GetAllSignatureTypes()
                .Select(ComponentCatalog.SignatureToString)
                .OrderBy(x => x);
 
            writer.WriteLine("Available component kinds:");
            using (writer.Nest())
            {
                foreach (var sig in sigs)
                    writer.WriteLine(sig);
            }
        }
 
        private void ShowFormattedSummary(IndentedTextWriter writer, string summary, int? columns)
        {
            _env.AssertValue(writer);
 
            if (string.IsNullOrWhiteSpace(summary))
                return;
 
            // REVIEW: should we replace consecutive spaces with a single space as a preprocessing step?
            int screenWidth = (columns ?? Console.BufferWidth) - 1;
 
            // Console.BufferWidth returns 0 if command redirection operator is used
            if (screenWidth <= 0)
                screenWidth = 80;
 
            const int indentLen = 3;
            string indent = new string(' ', indentLen);
            var builder = new StringBuilder();
 
            // REVIEW: is using StringSplitOptions.RemoveEmptyEntries the right thing to do here?
            var blocks = summary.Split(new[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries);
            for (int i = 0; i < blocks.Length; i++)
                AppendFormattedText(builder, blocks[i], indent, screenWidth);
 
            writer.WriteLine("Summary:");
            writer.WriteLine(builder);
        }
 
        private void AppendFormattedText(StringBuilder builder, string text, string indent, int screenWidth)
        {
            _env.AssertValue(builder);
            _env.AssertNonEmpty(text);
            _env.AssertNonEmpty(indent);
            _env.Assert(screenWidth > 0);
 
            int textIdx = 0;
            while (textIdx < text.Length)
            {
                int screenLeft = screenWidth - indent.Length;
                int summaryLeft = text.Length - textIdx;
                if (summaryLeft <= screenLeft)
                {
                    builder.Append(indent).Append(text, textIdx, summaryLeft).AppendLine();
                    break;
                }
 
                int spaceIdx = text.LastIndexOf(' ', screenLeft + textIdx, screenLeft);
                if (spaceIdx < 0)
                {
                    // Print to the first space.
                    int startIdx = screenLeft + textIdx + 1;
                    spaceIdx = text.IndexOf(' ', startIdx, text.Length - startIdx);
                    if (spaceIdx < 0)
                    {
                        // Print to the end.
                        builder.Append(indent).Append(text, textIdx, summaryLeft).AppendLine();
                        break;
                    }
                }
 
                int appendCount = spaceIdx - textIdx;
                builder.Append(indent).Append(text, textIdx, appendCount).AppendLine();
                textIdx += appendCount + 1;
            }
        }
 
        public readonly struct Component
        {
            public readonly string Kind;
            public readonly ComponentCatalog.LoadableClassInfo Info;
            public readonly object Args;
 
            public Component(string kind, ComponentCatalog.LoadableClassInfo info, object args)
            {
                Contracts.AssertNonEmpty(kind);
                Contracts.AssertValue(info);
                Contracts.AssertValueOrNull(args);
 
                Kind = kind;
                Info = info;
                Args = args;
            }
        }
 
        private void GenerateModule(List<Component> components)
        {
            Contracts.AssertValue(components);
            _generator.Generate(components);
        }
    }
 
    internal sealed class XmlGenerator : IGenerator
    {
        public sealed class Arguments
        {
            [Argument(ArgumentType.AtMostOnce, IsInputFileName = true, HelpText = "The path of the XML documentation file",
                ShortName = "xml", Hide = true)]
            public string XmlFilename;
        }
 
        private readonly string _xmlFilename;
        private readonly IHost _host;
 
        public XmlGenerator(IHostEnvironment env, Arguments args, string regenerate)
        {
            Contracts.CheckValue(env, nameof(env));
            env.AssertValue(args, nameof(args));
            env.AssertNonEmpty(regenerate, nameof(regenerate));
 
            _xmlFilename = args.XmlFilename;
            if (!string.IsNullOrWhiteSpace(_xmlFilename))
                Utils.CheckOptionalUserDirectory(_xmlFilename, nameof(args.XmlFilename));
            else
                _xmlFilename = null;
            _host = env.Register("XML Generator");
        }
 
        public void Generate(IEnumerable<HelpCommand.Component> infos)
        {
            if (_xmlFilename == null)
                return;
            using (var ch = _host.Start("Generating XML"))
            {
                var content = new XElement("Components",
                    from c in infos
                    where !string.IsNullOrWhiteSpace(c.Info.UserName)
                    select new XElement("Component",
                        new XAttribute("Kind", c.Kind),
                        new XElement("Name", c.Info.UserName),
                        string.IsNullOrWhiteSpace(c.Info.Summary) ? null : new XElement("Summary", c.Info.Summary),
                        new XElement("LoadNames",
                            from l in c.Info.LoadNames
                            select new XElement("LoadName", l)),
                        new XElement("Type", c.Info.Type.ToString()),
                        new XElement("SignatureTypes",
                            from s in c.Info.SignatureTypes
                            select new XElement("SignatureType", s.ToString())),
                        c.Args == null
                            ? null
                            : new XElement("Arguments",
                                from a in CmdParser.GetArgInfo(c.Args.GetType(), c.Args).Args
                                select new XElement("Argument",
                                    new XElement("LongName", a.LongName),
                                    a.ShortNames == null
                                        ? null
                                        : new XElement("ShortNames",
                                            from sn in a.ShortNames
                                            select new XElement("ShortName", sn)),
                                    new XElement("HelpText", a.HelpText),
                                    CreateDefaultValueElement(ch, c.Kind, a)))));
                File.WriteAllText(_xmlFilename, content.ToString());
            }
        }
 
        private XElement CreateDefaultValueElement(IChannel ch, string name, CmdParser.ArgInfo.Arg a)
        {
            if (a.DefaultValue == null)
                return null;
            if (a.DefaultValue is char)
            {
                char val = (char)a.DefaultValue;
                if (!char.IsLetterOrDigit(val) && !char.IsPunctuation(val) && !char.IsSymbol(val))
                {
                    ch.Warning("Unprintable default value for component {0}, character valued field {1}: {2}", name,
                        a.LongName, Convert.ToUInt16(val).ToString("X4", CultureInfo.InvariantCulture));
 
                    return null;
                }
            }
            return new XElement("DefaultValue", a.DefaultValue);
        }
    }
}