# MystBin ! - CLI.cs /* Two types of CLI arguments: --------------------------- 1. Full arguments - listed explicitly (eg. "--arg1") - usually a full word - can contain spaces represented as "--project-name" 2. Shortened arguments - listed implicitly version (eg. "-a") - usually a single letter, interpreted as such Two types of CLI values: ------------------------ 1. String inputs a) Single character (char) b) Single word (Word type) c) Multiple words (Sentence type) 2. Number inputs a) Single digit (Digit type) b) Digit string (Number type) */ using Builtins; namespace CommandLine { /// /// A struct representing a command-line argument. /// /// This contains two properties, a name and a list of aliases /// that can be used instead of the name, and serves as a way /// to represent a command-line argument in a centralised manner. /// public struct Argument { private readonly string _name; private readonly string[] _aliases; public string Name { get { return _name; } } public string[] Aliases { get { return _aliases; } } /// /// Create an Argument instance with only a name and no aliases. /// /// public Argument(string name) { _name = name; _aliases = []; } /// /// Create an Argument instance with a name and an array of aliases. /// /// These aliases will be used when comparing between two arguments /// and serves as a way to tell if arguments are repeated or not. /// /// /// public Argument(string name, string[] aliases) { _name = name; _aliases = aliases; } /// /// Create an Argument instance with a name and a single alias. /// /// Thhis alias will be used when comparing between two arguments /// and serves as a way to tell if arguments are repeated or not. /// /// /// public Argument(string name, string alias) { _name = name; _aliases = [alias]; } public override bool Equals(object? obj) { if (obj is null) return false; if (obj is string) return Name == obj.ToString(); Argument other = (Argument)obj; return Name == other.Name || other.Aliases.Contains(Name) || Aliases.Contains(other.Name); } public static implicit operator string(Argument arg) => arg.Name; public static implicit operator Argument(string str) => new(str); public override int GetHashCode() => Name.GetHashCode() ^ Aliases.GetHashCode(); public override string ToString() => $"Argument({_name}{(_aliases.Length > 0 ? $", aliases = [{string.Join(", ", _aliases)}]" : "")})"; } public abstract class Command { private readonly string _name; private readonly Argument[] _arguments; private readonly string? _doc; public string Name { get { return _name; } } public Argument[] Arguments { get { return _arguments; } } public string? Docstring { get { return _doc; } } public Argument[] ReturnedArguments { get; set; } public static List CreatedCommands { get { List names = []; foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) { foreach (var type in asm.GetTypes()) { if (type.BaseType == typeof(Command)) { var inst = Activator.CreateInstance(type); if (inst == null) throw new ArgumentNullException("Null error when searching for commands."); names.Add((Command)inst); } } } return names; } } public Command(string name) { _name = name; _arguments = [name]; _doc = null; ReturnedArguments = []; } public Command(string name, string docstring) { _name = name; _arguments = [name]; _doc = docstring.Trim().Replace("\n", "").Replace("\n\n", "\n"); ReturnedArguments = []; } public Command(string name, Argument[] arguments) { if (name == "help") throw new Exception("command names cannot be called 'help'."); _name = name; _arguments = [name, .. arguments]; _doc = null; ReturnedArguments = []; } public Command(string name, Argument[] arguments, string docstring) { if (name == "help") throw new Exception("command names cannot be called 'help'."); _name = name; _arguments = [name, .. arguments]; _doc = docstring.Trim().Replace("\n", "").Replace("\n\n", "\n"); ReturnedArguments = []; } public virtual void Callback() { throw new NotImplementedException("this method must be overrided in a subclass."); } } public struct CLI { public Command[] ValidCommands { get; set; } public CLI(Command[] validCommands) { ValidCommands = validCommands; } public Dictionary ArgParser(string[] argsFromCLI, Argument[] validArguments) { if (validArguments .Where(x => x.Name == "help") .ToList() .Count == 1 && validArguments.Length > 1) { validArguments = [new Argument("help_args")]; } List validArgIndexes = Enumerable .Range(0, argsFromCLI.Length) .Where(n => validArguments.Contains(argsFromCLI[n])) .ToList(); validArgIndexes.Add(argsFromCLI.Length); Dictionary convertedArgs = []; for (int x = 0; x < validArgIndexes.Count - 1; x++) { int firstIndex = validArgIndexes[x]; int secondIndex = validArgIndexes[x + 1]; string first = argsFromCLI[firstIndex]; string other = string.Join(" ", argsFromCLI[(firstIndex + 1) .. secondIndex]); convertedArgs.Add(first, other); } return convertedArgs; } public Command? SingleArgParser(string[] argsFromCLI) { string[] validArguments = ValidCommands.Select(x => x.Name).ToArray(); List validArgIndexes = Enumerable .Range(0, argsFromCLI.Length) .Where(n => validArguments.Contains(argsFromCLI[n])) .ToList(); if (validArgIndexes.Count != 2) return null; int argIndex = validArgIndexes[0]; string other = string.Join(' ', argsFromCLI[(argIndex + 1) .. (argsFromCLI.Length - argIndex)]); Command command = ValidCommands.Where(x => x.Name == argsFromCLI[argIndex]).First(); command.ReturnedArguments = [other]; return command; } } } # MystBin ! - Builtins.cs using CommandLine; namespace Builtins { // Echo command class EchoCommand : Command { public EchoCommand() : base("echo", docstring: """ A simple echo command that repeats back whatever the user sends. If the first argument is a number, it will be treated as the number of times to repeat the following sequence of characters, joined by a space. Usage ----- >>> echo hello world hello world >>> echo 3 hi hi hi hi """ ) { } public override void Callback() { string echoString = ReturnedArguments[0]; string[] echoStringParts = echoString.Split(' '); if (echoStringParts.Length == 1) { Console.WriteLine(echoString); return; } string firstEchoWord = echoStringParts[0]; string restOfEchoWord = string.Join(" ", echoStringParts[1 .. echoStringParts.Length]); if (int.TryParse(firstEchoWord, out int count)) { Console.WriteLine(string.Join(" ", Enumerable.Range(0, count + 1).Select(_ => restOfEchoWord))); } else { Console.WriteLine(echoString); } } } // Close command class CloseCommand : Command { public CloseCommand() : base("close", docstring: """ A simple close command that stops the process entirely and closes the window. Usage ----- >>> close """) { } public override void Callback() { Environment.Exit(0); } } // Help command class HelpCommand : Command { public HelpCommand() : base("help") { } public override void Callback() { // Get what the user queried with the help command string query = ReturnedArguments[0]; // Get all the commands subclassing command List search = CreatedCommands.Where(cmd => cmd.Name == query).ToList(); if (search.Count == 0) { Console.WriteLine($"The search for command '{query}' yielded no results."); return; } Command command = search[0]; string title = $"Help on command '{command}':"; Console.WriteLine($"{title}\n{new string('-', title.Length)}\n{command.Docstring}\n{new string('-', title.Length)}"); } } } # MystBin ! - Program.cs using CommandLine; using Builtins; class Program { static void RunCLI() { Console.Title = "Example CLI"; string? input; // Dictionary? args; Command? command; HelpCommand help = new(); EchoCommand echo = new(); CloseCommand close = new(); while (true) { Console.Write(">>> "); input = Console.ReadLine(); if (input == null) { Console.WriteLine(); continue; } CLI cli = new([help, echo, close]); command = cli.SingleArgParser(input.Split(' ')); if (command == null) { Console.WriteLine("Error occured when attempting to parse console input."); continue; } command.Callback(); } } static void Main() { // RunCLI(); Console.WriteLine(new EchoCommand().Docstring); } }