Skip to content

Instantly share code, notes, and snippets.

@dacher996
Last active August 20, 2024 20:25
Show Gist options
  • Save dacher996/51ad012d1676915a0864bd8b620f8eff to your computer and use it in GitHub Desktop.
Save dacher996/51ad012d1676915a0864bd8b620f8eff to your computer and use it in GitHub Desktop.
C# Console Looper for writing simple command based applications
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