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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
@@ -22,6 +22,7 @@ namespace CliFx
|
|||||||
private readonly ITypeActivator _typeActivator;
|
private readonly ITypeActivator _typeActivator;
|
||||||
|
|
||||||
private readonly HelpTextWriter _helpTextWriter;
|
private readonly HelpTextWriter _helpTextWriter;
|
||||||
|
private readonly ErrorTextWriter _errorTextWriter;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CliApplication"/>.
|
/// Initializes an instance of <see cref="CliApplication"/>.
|
||||||
@@ -36,6 +37,7 @@ namespace CliFx
|
|||||||
_typeActivator = typeActivator;
|
_typeActivator = typeActivator;
|
||||||
|
|
||||||
_helpTextWriter = new HelpTextWriter(metadata, console);
|
_helpTextWriter = new HelpTextWriter(metadata, console);
|
||||||
|
_errorTextWriter = new ErrorTextWriter(console);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteError(string message) => _console.WithForegroundColor(ConsoleColor.Red, () =>
|
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.
|
// because we still want the IDE to show them to the developer.
|
||||||
catch (Exception ex) when (!Debugger.IsAttached)
|
catch (Exception ex) when (!Debugger.IsAttached)
|
||||||
{
|
{
|
||||||
WriteError(ex.ToString());
|
_errorTextWriter.WriteError(ex);
|
||||||
return ExitCode.FromException(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