Last active
August 20, 2024 20:25
-
-
Save dacher996/51ad012d1676915a0864bd8b620f8eff to your computer and use it in GitHub Desktop.
C# Console Looper for writing simple command based applications
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace ConsoleLooper | |
{ | |
using System; | |
using System.Linq; | |
/// <summary> | |
/// An abstract console looper that handles commands and displays messages | |
/// </summary> | |
public abstract class ConsoleLooper | |
{ | |
#region Properties | |
/// <summary> | |
/// Optional welcome message to display when the console starts | |
/// </summary> | |
private readonly string? welcomeMessage; | |
/// <summary> | |
/// List of available commands | |
/// </summary> | |
protected readonly List<ConsoleCommand> AvailableCommands; | |
/// <summary> | |
/// Flag indicating whether to log errors to the console | |
/// </summary> | |
protected bool LogErrors { get; set; } = true; | |
/// <summary> | |
/// The message to display when an unknown command is entered | |
/// </summary> | |
protected string UnknownCommandMessage { get; set; } = "Unknown command: "; | |
/// <summary> | |
/// The prefix to display before an error message | |
/// </summary> | |
protected string HandleErrorMessagePrefix { get; set; } = "An error occurred: "; | |
#endregion | |
#region Constructors and Lifecycle events | |
/// <summary> | |
/// Default constructor for the console looper | |
/// </summary> | |
/// <param name="availableCommands">List of optional commands</param> | |
/// <param name="welcomeMessage">Optional welcome message displayed upon console start</param> | |
public ConsoleLooper(List<ConsoleCommand>? commands = null, string? welcomeMessage = null, bool includeInternalCommands = true) | |
{ | |
AvailableCommands = new() { }; | |
if (includeInternalCommands) | |
{ | |
AvailableCommands.AddRange(new List<ConsoleCommand>{ | |
new InternalConsoleCommand("help", "Display this message", _ => { Help(); return 0; }), | |
new InternalConsoleCommand("exit", "Quit the console", _ => -1), | |
new InternalConsoleCommand("clear", "Clears the console", _ => { Console.Clear(); return 0; }), | |
}); | |
} | |
if (commands != null) AvailableCommands.AddRange(commands); | |
this.welcomeMessage = welcomeMessage; | |
} | |
/// <summary> | |
/// Main update loop for the console | |
/// </summary> | |
public void Update() | |
{ | |
WelcomeMessage(); | |
var input = Console.ReadLine(); | |
int response = 0; | |
while (true) | |
{ | |
try | |
{ | |
response = HandleCommandInternally(input); | |
} | |
catch (Exception e) | |
{ | |
if (LogErrors) | |
{ | |
var msg = HandleErrorMessage(e); | |
Msg(); | |
Msg($"{HandleErrorMessagePrefix}{msg}"); | |
Msg(); | |
} | |
response = 0; | |
} | |
if (response == -1) break; | |
input = Console.ReadLine(); | |
} | |
} | |
/// <summary> | |
/// Handles a command from the console and returns an integer state | |
/// </summary> | |
/// <param name="commandArgs">The tokenized array of command arguments</param> | |
/// <returns>Integer state representing response type:<br/> | |
/// -1 - Exit<br/> | |
/// 0 - Continue<br/> | |
/// null - No response (to be handled internally)<br/> | |
/// </returns> | |
virtual protected int? HandleCommand(string[] commandArgs) => null; | |
/// <summary> | |
/// Handles an error message and returns a string to display. | |
/// Not called if LogErrors is set to false. | |
/// </summary> | |
/// <param name="e">The exception that has been thrown</param> | |
/// <returns>String to be presented to the console</returns> | |
virtual protected string HandleErrorMessage(Exception e) => e.Message; | |
#endregion | |
#region Messages | |
/// <summary> | |
/// Displays a welcome message to the console | |
/// </summary> | |
protected void WelcomeMessage() | |
{ | |
if (welcomeMessage == null) return; | |
Msg(); | |
Msg(welcomeMessage); | |
Msg(); | |
Msg(); | |
Help(); | |
} | |
/// <summary> | |
/// Displays a help message to the console | |
/// </summary> | |
protected void Help() | |
{ | |
Msg(); | |
Msg("Available commands:"); | |
foreach (var cmd in AvailableCommands) | |
Msg($"{cmd.CommandName} - {cmd.CommandDescription.ToLower()}"); | |
Msg(); | |
} | |
#endregion | |
#region Command Handling | |
/// <summary> | |
/// Logs a message to the console | |
/// </summary> | |
protected static void Msg(string? message = null) => Console.WriteLine(message ?? ""); | |
/// <summary> | |
/// Handles a command from the console and returns an integer state: | |
/// 0 - Continue | |
/// -1 - Exit | |
/// </summary> | |
/// <param name="command">The optional command to execute</param> | |
/// <returns>Integer state representing response type</returns> | |
private int HandleCommandInternally(string? command) | |
{ | |
if (string.IsNullOrWhiteSpace(command)) return 0; | |
string[] commandArgs = ParseCommand(command); | |
if (AvailableCommands.Any(cmd => cmd.CommandName.StartsWith(commandArgs[0]))) | |
{ | |
var resp = HandleCommand(commandArgs); | |
if (resp != null) return resp.Value; | |
// If not handled, handle internally | |
resp = AvailableCommands.FirstOrDefault(cmd => cmd.CommandName.StartsWith(commandArgs[0]))?.Execute(commandArgs); | |
if (resp != null) return resp.Value; | |
} | |
Msg($"{UnknownCommandMessage}{command}\n"); | |
return 0; | |
} | |
/// <summary> | |
/// Parses a command string into an array of arguments | |
/// </summary> | |
/// <param name="command">The input to parse</param> | |
/// <returns>Tokenized array of arguments</returns> | |
private string[] ParseCommand(string command) => command.Split(' ').Where(cmd => !string.IsNullOrWhiteSpace(cmd)).ToArray(); | |
#endregion | |
#region Internal Commands class | |
/// <summary> | |
/// Helper class used for defining internal console commands | |
/// </summary> | |
private class InternalConsoleCommand : ConsoleCommand | |
{ | |
private readonly Func<string[], int> callback; | |
public InternalConsoleCommand(string command, string description, Func<string[], int> callback) : base(command, description) | |
{ | |
this.callback = callback; | |
} | |
public override int Execute(string[] args) => callback.Invoke(args); | |
} | |
#endregion | |
} | |
/// <summary> | |
/// Abstract class representing a console command | |
/// </summary> | |
public abstract class ConsoleCommand | |
{ | |
/// <summary> | |
/// The name of the command | |
/// </summary> | |
public readonly string CommandName; | |
/// <summary> | |
/// The description of the command | |
/// </summary> | |
public readonly string CommandDescription; | |
/// <summary> | |
/// Default constructor for the console command | |
/// </summary> | |
/// <param name="command">Command name</param> | |
/// <param name="description">Command description</param> | |
public ConsoleCommand(string command, string description) | |
{ | |
CommandName = command; | |
CommandDescription = description; | |
} | |
/// <summary> | |
/// Executes the command with the given arguments | |
/// </summary> | |
/// <param name="args">Tokenized array of input arguments</param> | |
/// <returns>Status code representing state of execution</returns> | |
public abstract int Execute(string[] args); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment