From 68465fcef92368383be504d8e8a6c82cdb48454a Mon Sep 17 00:00:00 2001 From: vincent067 Date: Fri, 20 Mar 2026 18:22:44 +0000 Subject: [PATCH] feat: add CSV output format support This commit adds support for CSV output format, which is useful for: - Importing license data into Excel - Compliance and audit reporting - Feeding data into internal tools and CI/CD pipelines Changes: - Added Csv to OutputType enum - Created CsvOutputFormatter class with proper escaping - Updated CommandLineOptionsParser to handle CSV format - Updated help text to include CSV option Closes #423 --- src/NuGetLicense/CommandLineOptionsParser.cs | 1 + .../Output/Csv/CsvOutputFormatter.cs | 90 +++++++++++++++++++ src/NuGetLicense/Program.cs | 2 +- src/NuGetUtility/OutputType.cs | 5 +- 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 src/NuGetLicense/Output/Csv/CsvOutputFormatter.cs diff --git a/src/NuGetLicense/CommandLineOptionsParser.cs b/src/NuGetLicense/CommandLineOptionsParser.cs index 77445e36..a62d29b7 100644 --- a/src/NuGetLicense/CommandLineOptionsParser.cs +++ b/src/NuGetLicense/CommandLineOptionsParser.cs @@ -138,6 +138,7 @@ public IOutputFormatter GetOutputFormatter(OutputType outputType, bool returnErr OutputType.JsonPretty => new Output.Json.JsonOutputFormatter(true, returnErrorsOnly, !includeIgnoredPackages), OutputType.Table => new Output.Table.TableOutputFormatter(returnErrorsOnly, !includeIgnoredPackages), OutputType.Markdown => new Output.Table.TableOutputFormatter(returnErrorsOnly, !includeIgnoredPackages, printMarkdown: true), + OutputType.Csv => new Output.Csv.CsvOutputFormatter(returnErrorsOnly, !includeIgnoredPackages), _ => throw new ArgumentOutOfRangeException($"{outputType} not supported") }; } diff --git a/src/NuGetLicense/Output/Csv/CsvOutputFormatter.cs b/src/NuGetLicense/Output/Csv/CsvOutputFormatter.cs new file mode 100644 index 00000000..af70c9e4 --- /dev/null +++ b/src/NuGetLicense/Output/Csv/CsvOutputFormatter.cs @@ -0,0 +1,90 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System.Globalization; +using NuGetLicense.LicenseValidator; + +namespace NuGetLicense.Output.Csv +{ + /// + /// Outputs license validation results in CSV format. + /// + public class CsvOutputFormatter : IOutputFormatter + { + private readonly bool _printErrorsOnly; + private readonly bool _skipIgnoredPackages; + + public CsvOutputFormatter(bool printErrorsOnly, bool skipIgnoredPackages) + { + _printErrorsOnly = printErrorsOnly; + _skipIgnoredPackages = skipIgnoredPackages; + } + + public async Task Write(Stream stream, IList results) + { + if (_printErrorsOnly) + { + results = results.Where(r => r.ValidationErrors.Any()).ToList(); + } + else if (_skipIgnoredPackages) + { + results = results.Where(r => r.LicenseInformationOrigin != LicenseInformationOrigin.Ignored).ToList(); + } + + using var writer = new StreamWriter(stream, leaveOpen: true); + + // Write header + await writer.WriteLineAsync("Package,Version,License Information Origin,License Expression,License Url,Copyright,Authors,Description,Summary,Error,Error Context"); + + // Write rows + foreach (var result in results) + { + string errors = result.ValidationErrors.Any() + ? string.Join("; ", result.ValidationErrors.Select(e => EscapeCsvField(e.Error))) + : ""; + string errorContexts = result.ValidationErrors.Any() + ? string.Join("; ", result.ValidationErrors.Select(e => EscapeCsvField(e.Context))) + : ""; + + var line = string.Join(",", + EscapeCsvField(result.PackageId), + EscapeCsvField(result.PackageVersion.ToString()), + EscapeCsvField(result.LicenseInformationOrigin.ToString()), + EscapeCsvField(result.License), + EscapeCsvField(result.LicenseUrl), + EscapeCsvField(result.Copyright), + EscapeCsvField(result.Authors), + EscapeCsvField(result.Description), + EscapeCsvField(result.Summary), + EscapeCsvField(errors), + EscapeCsvField(errorContexts) + ); + + await writer.WriteLineAsync(line); + } + + await writer.FlushAsync(); + } + + /// + /// Escapes a field for CSV output. + /// Handles fields containing commas, quotes, or newlines. + /// + private static string EscapeCsvField(string? field) + { + if (string.IsNullOrEmpty(field)) + { + return ""; + } + + // If the field contains comma, quote, or newline, wrap it in quotes + if (field.Contains(',') || field.Contains('"') || field.Contains('\n') || field.Contains('\r')) + { + // Replace double quotes with two double quotes + return "\"" + field.Replace("\"", "\"\"") + "\""; + } + + return field; + } + } +} diff --git a/src/NuGetLicense/Program.cs b/src/NuGetLicense/Program.cs index 1829e34f..2c128bcf 100644 --- a/src/NuGetLicense/Program.cs +++ b/src/NuGetLicense/Program.cs @@ -41,7 +41,7 @@ public class Program : ICommandLineOptions [Option("-d|--license-information-download-location", Description = "Specifies a folder where the application will download all licenses provided via license URLs.")] public string? DownloadLicenseInformation { get; set; } - [Option("-o|--output", Description = "Specifies the output format. Valid values are Table, Markdown, Json, or JsonPretty (default: Table).")] + [Option("-o|--output", Description = "Specifies the output format. Valid values are Table, Markdown, Json, JsonPretty, or Csv (default: Table).")] public OutputType OutputType { get; set; } = OutputType.Table; [Option("-err|--error-only", Description = "When set, only validation errors are returned as result. Otherwise, all validation results are always returned.")] diff --git a/src/NuGetUtility/OutputType.cs b/src/NuGetUtility/OutputType.cs index 74525e51..3d66ac6d 100644 --- a/src/NuGetUtility/OutputType.cs +++ b/src/NuGetUtility/OutputType.cs @@ -1,4 +1,4 @@ -// Licensed to the projects contributors. +// Licensed to the projects contributors. // The license conditions are provided in the LICENSE file located in the project root namespace NuGetUtility @@ -8,6 +8,7 @@ public enum OutputType Table, Json, JsonPretty, - Markdown + Markdown, + Csv } }