mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Pretty printing of exception messages (#79)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -22,6 +22,7 @@ namespace CliFx
|
||||
private readonly ITypeActivator _typeActivator;
|
||||
|
||||
private readonly HelpTextWriter _helpTextWriter;
|
||||
private readonly ErrorTextWriter _errorTextWriter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CliApplication"/>.
|
||||
@@ -36,6 +37,7 @@ namespace CliFx
|
||||
_typeActivator = typeActivator;
|
||||
|
||||
_helpTextWriter = new HelpTextWriter(metadata, console);
|
||||
_errorTextWriter = new ErrorTextWriter(console);
|
||||
}
|
||||
|
||||
private void WriteError(string message) => _console.WithForegroundColor(ConsoleColor.Red, () =>
|
||||
@@ -202,7 +204,7 @@ namespace CliFx
|
||||
// because we still want the IDE to show them to the developer.
|
||||
catch (Exception ex) when (!Debugger.IsAttached)
|
||||
{
|
||||
WriteError(ex.ToString());
|
||||
_errorTextWriter.WriteError(ex);
|
||||
return ExitCode.FromException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
226
CliFx/Domain/ErrorTextWriter.cs
Normal file
226
CliFx/Domain/ErrorTextWriter.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
internal class ErrorTextWriter
|
||||
{
|
||||
private const int indent = 4;
|
||||
|
||||
private static readonly ConsoleColor NameColor = ConsoleColor.DarkGray;
|
||||
private static readonly ConsoleColor SpecificNameColor = ConsoleColor.White;
|
||||
private static readonly ConsoleColor MessageColor = ConsoleColor.Red;
|
||||
private static readonly ConsoleColor MethodColor = ConsoleColor.Yellow;
|
||||
private static readonly ConsoleColor ParameterTypeColor = ConsoleColor.Blue;
|
||||
private static readonly ConsoleColor FileColor = ConsoleColor.Yellow;
|
||||
private static readonly ConsoleColor LineColor = ConsoleColor.Blue;
|
||||
|
||||
private static readonly Regex MethodMatcher = new Regex(@"(?<prefix>\S+) (?<name>.*?)(?<methodName>[^\.]+)\(");
|
||||
private static readonly Regex ParameterMatcher = new Regex(@"(?<type>.+? )(?<name>.+?)(?:(?<separator>, )|\))");
|
||||
private static readonly Regex FileMatcher = new Regex(@"(?<prefix>\S+?) (?<path>.*?)(?<file>[^\\/]+?(?:\.\w*)?):[^:]+? (?<line>\d+).*");
|
||||
|
||||
private readonly IConsole _console;
|
||||
|
||||
public ErrorTextWriter(IConsole console)
|
||||
{
|
||||
_console = console;
|
||||
}
|
||||
|
||||
public void WriteError(Exception ex) => WriteError(ex, 0);
|
||||
private void WriteError(Exception ex, int indentLevel)
|
||||
{
|
||||
var indentation = new string(' ', indent * indentLevel);
|
||||
var extraIndentation = new string(' ', indent / 2);
|
||||
|
||||
var exType = ex.GetType();
|
||||
|
||||
// (Fully qualified) type of the exception
|
||||
Write(NameColor, indentation + exType.Namespace + ".");
|
||||
Write(SpecificNameColor, exType.Name);
|
||||
_console.Error.Write(": ");
|
||||
|
||||
// Message
|
||||
Write(MessageColor, ex.Message);
|
||||
_console.Error.WriteLine();
|
||||
|
||||
// Prints the inner exception
|
||||
// with one higher indentation level
|
||||
if (ex.InnerException is Exception innerException)
|
||||
{
|
||||
WriteError(innerException, indentLevel + 1);
|
||||
}
|
||||
|
||||
// Print with formatting when successfully parsing all entries in the stack trace
|
||||
if (ParseStackTrace(ex.StackTrace) is IEnumerable<StackTraceEntry> parsedStackTrace)
|
||||
{
|
||||
// Each step in the stack trace is formated and printed
|
||||
foreach (var entry in parsedStackTrace)
|
||||
{
|
||||
_console.Error.Write(indentation + extraIndentation);
|
||||
|
||||
WriteMethodDescriptor(entry.MethodPrefix, entry.MethodName, entry.MethodSpecificName);
|
||||
|
||||
WriteParameters(entry.Parameters);
|
||||
|
||||
_console.Error.Write(entry.FilePrefix);
|
||||
_console.Error.Write("\n" + indentation + extraIndentation + extraIndentation);
|
||||
WriteFileDescriptor(entry.FilePath, entry.FileName, entry.FileLine);
|
||||
|
||||
_console.Error.WriteLine();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parsing failed. Print without formatting.
|
||||
foreach (var trace in ex.StackTrace.Split('\n'))
|
||||
{
|
||||
_console.Error.WriteLine(indentation + trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteMethodDescriptor(string prefix, string name, string methodName)
|
||||
{
|
||||
_console.Error.Write(prefix + " ");
|
||||
Write(NameColor, name);
|
||||
Write(MethodColor, methodName);
|
||||
}
|
||||
|
||||
private void WriteParameters(IEnumerable<ParameterEntry> parameters)
|
||||
{
|
||||
_console.Error.Write("(");
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
Write(ParameterTypeColor, parameter.Type);
|
||||
Write(SpecificNameColor, parameter.Name);
|
||||
|
||||
if (parameter.Separator is string separator)
|
||||
{
|
||||
_console.Error.Write(separator);
|
||||
}
|
||||
}
|
||||
_console.Error.Write(") ");
|
||||
}
|
||||
|
||||
private void WriteFileDescriptor(string path, string fileName, string lineNumber)
|
||||
{
|
||||
Write(NameColor, path);
|
||||
|
||||
Write(FileColor, fileName);
|
||||
_console.Error.Write(":");
|
||||
Write(LineColor, lineNumber);
|
||||
}
|
||||
|
||||
private void Write(ConsoleColor color, string value)
|
||||
{
|
||||
_console.WithForegroundColor(color, () => _console.Error.Write(value));
|
||||
}
|
||||
|
||||
private IEnumerable<StackTraceEntry>? ParseStackTrace(string stackTrace)
|
||||
{
|
||||
IList<StackTraceEntry> stackTraceEntries = new List<StackTraceEntry>();
|
||||
foreach (var trace in stackTrace.Split('\n'))
|
||||
{
|
||||
var methodMatch = MethodMatcher.Match(trace);
|
||||
var parameterMatches = ParameterMatcher.Matches(trace, methodMatch.Index + methodMatch.Length);
|
||||
var fileMatch = FileMatcher.Match(
|
||||
trace,
|
||||
parameterMatches.Count switch
|
||||
{
|
||||
0 => methodMatch.Index + methodMatch.Length + 1,
|
||||
int c => parameterMatches[c - 1].Index + parameterMatches[c - 1].Length
|
||||
}
|
||||
);
|
||||
|
||||
if (fileMatch.Index + fileMatch.Length != trace.Length)
|
||||
{
|
||||
// Didnt match the whole trace
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IList<ParameterEntry> parameters = new List<ParameterEntry>();
|
||||
foreach (Match match in parameterMatches)
|
||||
{
|
||||
parameters.Add(new ParameterEntry(
|
||||
match.Groups["type"].Success ? match.Groups["type"].Value : throw new Exception("type"),
|
||||
match.Groups["name"].Success ? match.Groups["name"].Value : throw new Exception("name"),
|
||||
match.Groups["separator"]?.Value // If this is null, it's just the last parameter
|
||||
));
|
||||
}
|
||||
|
||||
stackTraceEntries.Add(new StackTraceEntry(
|
||||
methodMatch.Groups["prefix"].Success ? methodMatch.Groups["prefix"].Value : throw new Exception("prefix"),
|
||||
methodMatch.Groups["name"].Success ? methodMatch.Groups["name"].Value : throw new Exception("name"),
|
||||
methodMatch.Groups["methodName"].Success ? methodMatch.Groups["methodName"].Value : throw new Exception("methodName"),
|
||||
parameters,
|
||||
fileMatch.Groups["prefix"].Success ? fileMatch.Groups["prefix"].Value : throw new Exception("prefix"),
|
||||
fileMatch.Groups["path"].Success ? fileMatch.Groups["path"].Value : throw new Exception("path"),
|
||||
fileMatch.Groups["file"].Success ? fileMatch.Groups["file"].Value : throw new Exception("file"),
|
||||
fileMatch.Groups["line"].Success ? fileMatch.Groups["line"].Value : throw new Exception("line")
|
||||
));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// One of the required groups failed to match
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return stackTraceEntries;
|
||||
}
|
||||
|
||||
private readonly struct StackTraceEntry
|
||||
{
|
||||
public string MethodPrefix { get; }
|
||||
public string MethodName { get; }
|
||||
public string MethodSpecificName { get; }
|
||||
|
||||
public IEnumerable<ParameterEntry> Parameters { get; }
|
||||
|
||||
public string FilePrefix { get; }
|
||||
public string FilePath { get; }
|
||||
public string FileName { get; }
|
||||
public string FileLine { get; }
|
||||
|
||||
public StackTraceEntry(
|
||||
string methodPrefix,
|
||||
string methodName,
|
||||
string methodSpecificName,
|
||||
IEnumerable<ParameterEntry> parameters,
|
||||
string filePrefix,
|
||||
string filePath,
|
||||
string fileName,
|
||||
string fileLine)
|
||||
{
|
||||
MethodPrefix = methodPrefix;
|
||||
MethodName = methodName;
|
||||
MethodSpecificName = methodSpecificName;
|
||||
Parameters = parameters;
|
||||
FilePrefix = filePrefix;
|
||||
FilePath = filePath;
|
||||
FileName = fileName;
|
||||
FileLine = fileLine;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct ParameterEntry
|
||||
{
|
||||
public string Type { get; }
|
||||
public string Name { get; }
|
||||
public string? Separator { get; }
|
||||
|
||||
public ParameterEntry(
|
||||
string type,
|
||||
string name,
|
||||
string? separator)
|
||||
{
|
||||
Type = type;
|
||||
Name = name;
|
||||
Separator = separator;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user