diff --git a/README.md b/README.md index 54d8978..b0d5a7d 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,15 @@ Usage: spdx-tool [options] [arguments] Options: -h, --help Show this help message and exit -v, --version Show version information and exit + -l, --log Log output to file + -s, --silent Silence console output Commands: help Display extended help about a command add-package Add package to SPDX document (workflow only). add-relationship Add relationship between elements. copy-package Copy package between SPDX documents (workflow only). - diagram Generate mermaid diagram. + diagram [tools] Generate mermaid diagram. find-package Find package ID in SPDX document get-version Get the version of an SPDX package. hash Generate or verify hashes of files diff --git a/src/DemaConsulting.SpdxTool/Commands/AddPackage.cs b/src/DemaConsulting.SpdxTool/Commands/AddPackage.cs index 7de3c88..177604a 100644 --- a/src/DemaConsulting.SpdxTool/Commands/AddPackage.cs +++ b/src/DemaConsulting.SpdxTool/Commands/AddPackage.cs @@ -92,13 +92,13 @@ private AddPackage() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { throw new CommandUsageException("'add-package' command is only valid in a workflow"); } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); diff --git a/src/DemaConsulting.SpdxTool/Commands/AddRelationship.cs b/src/DemaConsulting.SpdxTool/Commands/AddRelationship.cs index 9ac70e7..eb528b6 100644 --- a/src/DemaConsulting.SpdxTool/Commands/AddRelationship.cs +++ b/src/DemaConsulting.SpdxTool/Commands/AddRelationship.cs @@ -82,7 +82,7 @@ private AddRelationship() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { // Report an error if the number of arguments is less than 4 if (args.Length < 4) @@ -102,7 +102,7 @@ public override void Run(string[] args) } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); diff --git a/src/DemaConsulting.SpdxTool/Commands/Command.cs b/src/DemaConsulting.SpdxTool/Commands/Command.cs index 9419cfb..6fae50d 100644 --- a/src/DemaConsulting.SpdxTool/Commands/Command.cs +++ b/src/DemaConsulting.SpdxTool/Commands/Command.cs @@ -30,15 +30,17 @@ public abstract class Command /// /// Run the command /// + /// Program context /// Command arguments - public abstract void Run(string[] args); + public abstract void Run(Context context, string[] args); /// /// Run the command /// + /// Program context /// Command step /// Workflow variables - public abstract void Run(YamlMappingNode step, Dictionary variables); + public abstract void Run(Context context, YamlMappingNode step, Dictionary variables); /// /// Expand variables in text diff --git a/src/DemaConsulting.SpdxTool/Commands/CopyPackage.cs b/src/DemaConsulting.SpdxTool/Commands/CopyPackage.cs index d424d54..3715bf5 100644 --- a/src/DemaConsulting.SpdxTool/Commands/CopyPackage.cs +++ b/src/DemaConsulting.SpdxTool/Commands/CopyPackage.cs @@ -86,7 +86,7 @@ private CopyPackage() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { // Report an error if the number of arguments is less than 3 if (args.Length < 3) @@ -122,7 +122,7 @@ public override void Run(string[] args) } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); diff --git a/src/DemaConsulting.SpdxTool/Commands/Diagram.cs b/src/DemaConsulting.SpdxTool/Commands/Diagram.cs index b707683..65832df 100644 --- a/src/DemaConsulting.SpdxTool/Commands/Diagram.cs +++ b/src/DemaConsulting.SpdxTool/Commands/Diagram.cs @@ -88,7 +88,7 @@ private Diagram() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { // Report an error if the number of arguments is less than 2 if (args.Length < 2) @@ -110,7 +110,7 @@ public override void Run(string[] args) } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); diff --git a/src/DemaConsulting.SpdxTool/Commands/FindPackage.cs b/src/DemaConsulting.SpdxTool/Commands/FindPackage.cs index 870014e..3cb6c19 100644 --- a/src/DemaConsulting.SpdxTool/Commands/FindPackage.cs +++ b/src/DemaConsulting.SpdxTool/Commands/FindPackage.cs @@ -82,7 +82,7 @@ private FindPackage() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { // Report an error if insufficient arguments if (args.Length < 2) @@ -97,11 +97,11 @@ public override void Run(string[] args) var packageId = FindPackageByCriteria(spdxFile, criteria).Id; // Write the package ID to the console - Console.WriteLine(packageId); + context.WriteLine(packageId); } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); diff --git a/src/DemaConsulting.SpdxTool/Commands/GetVersion.cs b/src/DemaConsulting.SpdxTool/Commands/GetVersion.cs index a8137f5..ce6781f 100644 --- a/src/DemaConsulting.SpdxTool/Commands/GetVersion.cs +++ b/src/DemaConsulting.SpdxTool/Commands/GetVersion.cs @@ -79,7 +79,7 @@ private GetVersion() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { // Report an error if insufficient arguments if (args.Length < 2) @@ -94,11 +94,11 @@ public override void Run(string[] args) var packageVersion = FindPackage.FindPackageByCriteria(spdxFile, criteria).Version; // Print the version - Console.WriteLine(packageVersion); + context.WriteLine(packageVersion ?? ""); } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); diff --git a/src/DemaConsulting.SpdxTool/Commands/Hash.cs b/src/DemaConsulting.SpdxTool/Commands/Hash.cs index fb50389..cdee501 100644 --- a/src/DemaConsulting.SpdxTool/Commands/Hash.cs +++ b/src/DemaConsulting.SpdxTool/Commands/Hash.cs @@ -69,7 +69,7 @@ private Hash() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { // Report an error if the number of arguments is not 3 if (args.Length != 3) @@ -79,11 +79,11 @@ public override void Run(string[] args) var operation = args[0]; var algorithm = args[1]; var file = args[2]; - DoHashOperation(operation, algorithm, file); + DoHashOperation(context, operation, algorithm, file); } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); @@ -101,17 +101,18 @@ public override void Run(YamlMappingNode step, Dictionary variab throw new YamlException(step.Start, step.End, "'hash' command missing 'file' input"); // Do the hash operation - DoHashOperation(operation, algorithm, file); + DoHashOperation(context, operation, algorithm, file); } /// /// Do the requested Sha256 operation /// + /// Program context /// Operation to perform (generate or verify) /// Hash algorithm /// File to perform operation on /// On usage error - public static void DoHashOperation(string operation, string algorithm, string file) + public static void DoHashOperation(Context context, string operation, string algorithm, string file) { // Check the algorithm if (algorithm != "sha256") @@ -125,7 +126,7 @@ public static void DoHashOperation(string operation, string algorithm, string fi break; case "verify": - VerifySha256(file); + VerifySha256(context, file); break; default: @@ -149,9 +150,10 @@ public static void GenerateSha256(string file) /// /// Verify a Sha256 hash for a file /// - /// + /// Program context + /// Name of the file to verify /// - public static void VerifySha256(string file) + public static void VerifySha256(Context context, string file) { // Check the hash file exists var hashFile = file + ".sha256"; @@ -169,7 +171,7 @@ public static void VerifySha256(string file) throw new CommandErrorException($"Sha256 hash mismatch for '{file}'"); // Report the digest is OK - Console.WriteLine($"Sha256 Digest OK for '{file}'"); + context.WriteLine($"Sha256 Digest OK for '{file}'"); } /// diff --git a/src/DemaConsulting.SpdxTool/Commands/Help.cs b/src/DemaConsulting.SpdxTool/Commands/Help.cs index 1cf4613..7a4e180 100644 --- a/src/DemaConsulting.SpdxTool/Commands/Help.cs +++ b/src/DemaConsulting.SpdxTool/Commands/Help.cs @@ -66,18 +66,18 @@ private Help() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { // Report an error if the number of arguments is not 1 if (args.Length != 1) throw new CommandUsageException("'help' command missing arguments"); // Generate the markdown - ShowUsage(args[0]); + ShowUsage(context, args[0]); } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); @@ -87,15 +87,16 @@ public override void Run(YamlMappingNode step, Dictionary variab throw new YamlException(step.Start, step.End, "'help' command missing 'about' input"); // Generate the markdown - ShowUsage(about); + ShowUsage(context, about); } /// /// Show the usage for the requested command /// - /// + /// Program context + /// Command to get help on /// On error - public static void ShowUsage(string command) + public static void ShowUsage(Context context, string command) { // Get the entry for the command if (!CommandsRegistry.Commands.TryGetValue(command, out var entry)) @@ -103,6 +104,6 @@ public static void ShowUsage(string command) // Display the command entry foreach (var line in entry.Details) - Console.WriteLine(line); + context.WriteLine(line); } } \ No newline at end of file diff --git a/src/DemaConsulting.SpdxTool/Commands/Print.cs b/src/DemaConsulting.SpdxTool/Commands/Print.cs index 4b46c70..f233fcf 100644 --- a/src/DemaConsulting.SpdxTool/Commands/Print.cs +++ b/src/DemaConsulting.SpdxTool/Commands/Print.cs @@ -68,14 +68,14 @@ private Print() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { foreach (var arg in args) - Console.WriteLine(arg); + context.WriteLine(arg); } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); @@ -88,7 +88,7 @@ public override void Run(YamlMappingNode step, Dictionary variab for (var i = 0; i < text.Children.Count; i++) { var line = GetSequenceString(text, i, variables) ?? string.Empty; - Console.WriteLine(line); + context.WriteLine(line); } } } \ No newline at end of file diff --git a/src/DemaConsulting.SpdxTool/Commands/Query.cs b/src/DemaConsulting.SpdxTool/Commands/Query.cs index aff1df8..2527d17 100644 --- a/src/DemaConsulting.SpdxTool/Commands/Query.cs +++ b/src/DemaConsulting.SpdxTool/Commands/Query.cs @@ -74,7 +74,7 @@ private Query() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { // Report an error if the number of arguments is not 1 if (args.Length < 2) @@ -83,12 +83,12 @@ public override void Run(string[] args) // Generate the markdown var found = QueryProgramOutput(args[0], args[1], args.Skip(2).ToArray()); - // Write the found value to the console - Console.WriteLine(found); + // Write the found value + context.WriteLine(found); } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); diff --git a/src/DemaConsulting.SpdxTool/Commands/RenameId.cs b/src/DemaConsulting.SpdxTool/Commands/RenameId.cs index f786d57..e50768a 100644 --- a/src/DemaConsulting.SpdxTool/Commands/RenameId.cs +++ b/src/DemaConsulting.SpdxTool/Commands/RenameId.cs @@ -70,7 +70,7 @@ private RenameId() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { // Report an error if the number of arguments is not 3 if (args.Length != 3) @@ -81,7 +81,7 @@ public override void Run(string[] args) } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); diff --git a/src/DemaConsulting.SpdxTool/Commands/RunWorkflow.cs b/src/DemaConsulting.SpdxTool/Commands/RunWorkflow.cs index 1243a76..493bb7b 100644 --- a/src/DemaConsulting.SpdxTool/Commands/RunWorkflow.cs +++ b/src/DemaConsulting.SpdxTool/Commands/RunWorkflow.cs @@ -75,7 +75,7 @@ private RunWorkflow() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { // Report an error if the number of arguments is less than 1 if (args.Length < 1) @@ -107,20 +107,20 @@ public override void Run(string[] args) } // Execute the workflow - var outputs = name.StartsWith("http") ? RunUrl(name, null, parameters) : RunFile(name, null, parameters); + var outputs = name.StartsWith("http") ? RunUrl(context, name, null, parameters) : RunFile(context, name, null, parameters); // Skip if not verbose if (!verbose) return; // Print the outputs - Console.WriteLine("Outputs:"); + context.WriteLine("Outputs:"); foreach (var (key, value) in outputs) - Console.WriteLine($" {key} = {value}"); + context.WriteLine($" {key} = {value}"); } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); @@ -144,7 +144,7 @@ public override void Run(YamlMappingNode step, Dictionary variab } // Run the workflow - var outputs = Run(step, file, url, integrity, parameters); + var outputs = Run(context, step, file, url, integrity, parameters); // Save any outputs if (GetMapMap(inputs, "outputs") is { } outputsMap) @@ -163,6 +163,7 @@ public override void Run(YamlMappingNode step, Dictionary variab /// /// Execute the workflow /// + /// Program context /// Step for reporting errors /// Optional file /// Optional URL @@ -170,7 +171,7 @@ public override void Run(YamlMappingNode step, Dictionary variab /// Workflow parameters /// Workflow outputs /// on error - public static Dictionary Run(YamlMappingNode step, string? file, string? url, string? integrity, Dictionary parameters) + public static Dictionary Run(Context context, YamlMappingNode step, string? file, string? url, string? integrity, Dictionary parameters) { // Fail if no source if (file != null && url != null) @@ -178,11 +179,11 @@ public static Dictionary Run(YamlMappingNode step, string? file, // Run the file if specified if (file != null) - return RunFile(file, integrity, parameters); + return RunFile(context, file, integrity, parameters); // Run the URL if specified if (url != null) - return RunUrl(url, integrity, parameters); + return RunUrl(context, url, integrity, parameters); // No source provided throw new YamlException(step.Start, step.End, "'run-workflow' command must specify either 'file' or 'url' input"); @@ -191,13 +192,14 @@ public static Dictionary Run(YamlMappingNode step, string? file, /// /// Execute the workflow /// + /// Program context /// Workflow file /// Optional integrity hash /// Workflow parameters /// Workflow outputs /// On usage error /// On workflow error - public static Dictionary RunFile(string workflowFile, string? integrity, Dictionary parameters) + public static Dictionary RunFile(Context context, string workflowFile, string? integrity, Dictionary parameters) { // Verify the file exists if (!File.Exists(workflowFile)) @@ -208,18 +210,19 @@ public static Dictionary RunFile(string workflowFile, string? in var bytes = File.ReadAllBytes(workflowFile); // Run the workflow - return RunBytes(workflowFile, bytes, integrity, parameters); + return RunBytes(context, workflowFile, bytes, integrity, parameters); } /// /// Run workflow from URL /// + /// Program context /// Workflow URL /// Optional integrity hash /// Workflow parameters /// Workflow outputs /// on error - public static Dictionary RunUrl(string url, string? integrity, Dictionary parameters) + public static Dictionary RunUrl(Context context, string url, string? integrity, Dictionary parameters) { // Construct the client handler to use the system proxy var handler = new HttpClientHandler @@ -245,18 +248,19 @@ public static Dictionary RunUrl(string url, string? integrity, D var bytes = bytesTask.Result; // Run the workflow - return RunBytes(url, bytes, integrity, parameters); + return RunBytes(context, url, bytes, integrity, parameters); } /// /// Execute the workflow from Yaml bytes (from file, url, etc.) /// + /// Program context /// Yaml source /// Yaml bytes /// Optional integrity hash /// Parameters /// Workflow outputs - public static Dictionary RunBytes(string source, byte[] bytes, string? integrity, Dictionary parameters) + public static Dictionary RunBytes(Context context, string source, byte[] bytes, string? integrity, Dictionary parameters) { // Optionally check the integrity before running if (integrity != null) @@ -319,7 +323,7 @@ public static Dictionary RunBytes(string source, byte[] bytes, s // Check for a displayName var displayName = GetMapString(step, "displayName", variables); if (displayName != null) - Console.WriteLine(displayName); + context.WriteLine(displayName); // Execute the step if (!CommandsRegistry.Commands.TryGetValue(command, out var entry)) @@ -327,7 +331,7 @@ public static Dictionary RunBytes(string source, byte[] bytes, s $"Unknown command: '{command}'"); // Run the command - entry.Instance.Run(step, variables); + entry.Instance.Run(context, step, variables); } // Return our variables as the output diff --git a/src/DemaConsulting.SpdxTool/Commands/SetVariable.cs b/src/DemaConsulting.SpdxTool/Commands/SetVariable.cs index a54e077..b8e11c7 100644 --- a/src/DemaConsulting.SpdxTool/Commands/SetVariable.cs +++ b/src/DemaConsulting.SpdxTool/Commands/SetVariable.cs @@ -63,13 +63,13 @@ private SetVariable() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { throw new CommandUsageException("'set-variable' command is only valid in a workflow"); } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); diff --git a/src/DemaConsulting.SpdxTool/Commands/ToMarkdown.cs b/src/DemaConsulting.SpdxTool/Commands/ToMarkdown.cs index 04ff285..d3d8c6d 100644 --- a/src/DemaConsulting.SpdxTool/Commands/ToMarkdown.cs +++ b/src/DemaConsulting.SpdxTool/Commands/ToMarkdown.cs @@ -72,7 +72,7 @@ private ToMarkdown() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { // Report an error if the number of arguments is less than 2 if (args.Length < 2) @@ -97,7 +97,7 @@ public override void Run(string[] args) } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); diff --git a/src/DemaConsulting.SpdxTool/Commands/UpdatePackage.cs b/src/DemaConsulting.SpdxTool/Commands/UpdatePackage.cs index a8e22e3..8a95e55 100644 --- a/src/DemaConsulting.SpdxTool/Commands/UpdatePackage.cs +++ b/src/DemaConsulting.SpdxTool/Commands/UpdatePackage.cs @@ -76,13 +76,13 @@ private UpdatePackage() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { throw new CommandUsageException("'update-package' command is only valid in a workflow"); } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); diff --git a/src/DemaConsulting.SpdxTool/Commands/Validate.cs b/src/DemaConsulting.SpdxTool/Commands/Validate.cs index 0b65514..d44318c 100644 --- a/src/DemaConsulting.SpdxTool/Commands/Validate.cs +++ b/src/DemaConsulting.SpdxTool/Commands/Validate.cs @@ -68,7 +68,7 @@ private Validate() } /// - public override void Run(string[] args) + public override void Run(Context context, string[] args) { // Report an error if for missing arguments if (args.Length == 0) @@ -79,11 +79,11 @@ public override void Run(string[] args) var ntia = args.Skip(1).Any(a => a == "ntia"); // Perform validation - DoValidate(spdxFile, ntia); + DoValidate(context, spdxFile, ntia); } /// - public override void Run(YamlMappingNode step, Dictionary variables) + public override void Run(Context context, YamlMappingNode step, Dictionary variables) { // Get the step inputs var inputs = GetMapMap(step, "inputs"); @@ -97,16 +97,17 @@ public override void Run(YamlMappingNode step, Dictionary variab var ntia = ntiaValue?.ToLowerInvariant() == "true"; // Perform validation - DoValidate(spdxFile, ntia); + DoValidate(context, spdxFile, ntia); } /// /// Validate SPDX document for issues /// + /// Program context /// SPDX document file name /// NTIA flag /// on issues - public static void DoValidate(string spdxFile, bool ntia) + public static void DoValidate(Context context, string spdxFile, bool ntia) { // Load the SPDX document var doc = SpdxHelpers.LoadJsonDocument(spdxFile); @@ -119,12 +120,10 @@ public static void DoValidate(string spdxFile, bool ntia) if (issues.Count == 0) return; - // Report issues to console - Console.ForegroundColor = ConsoleColor.DarkYellow; + // Report issues foreach (var issue in issues) - Console.WriteLine(issue); - Console.ResetColor(); - Console.WriteLine(); + context.WriteWarning(issue); + context.WriteLine(""); // Throw error throw new CommandErrorException($"Found {issues.Count} Issues in {spdxFile}"); diff --git a/src/DemaConsulting.SpdxTool/Context.cs b/src/DemaConsulting.SpdxTool/Context.cs new file mode 100644 index 0000000..2a5c90b --- /dev/null +++ b/src/DemaConsulting.SpdxTool/Context.cs @@ -0,0 +1,193 @@ +// Copyright (c) 2024 DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DemaConsulting.SpdxTool; + +/// +/// Program Context class +/// +public sealed class Context : IDisposable +{ + /// + /// Output log-file writer (when logging output to file) + /// + private readonly StreamWriter? _log; + + /// + /// Initializes a new instance of the Context class + /// + /// Optional log-file writer + /// Program arguments + private Context(StreamWriter? log, IReadOnlyCollection args) + { + _log = log; + Arguments = args; + } + + /// + /// Gets a value indicating the version has been requested + /// + public bool Version { get; private init; } + + /// + /// Gets a value indicating help has been requested + /// + public bool Help { get; private init; } + + /// + /// Gets a value indicating silent-output has been requested + /// + public bool Silent { get; private init; } + + /// + /// Gets the arguments + /// + public IReadOnlyCollection Arguments { get; private init; } + + /// + /// Gets the number of errors reported + /// + public int Errors { get; private set; } + + /// + /// Dispose of this context + /// + public void Dispose() + { + _log?.Dispose(); + } + + /// + /// Write text to output + /// + /// Text to write + public void WriteLine(string text) + { + // Write to the console unless silent + if (!Silent) + Console.WriteLine(text); + + // Write to the log if specified + _log?.WriteLine(text); + } + + /// + /// Write warning message to output + /// + /// Warning message to write + public void WriteWarning(string message) + { + // Write to the console unless silent + if (!Silent) + { + Console.ForegroundColor = ConsoleColor.DarkYellow; + Console.WriteLine(message); + Console.ResetColor(); + } + + // Write to the log if specified + _log?.WriteLine(message); + } + + /// + /// Write an error message to output + /// + /// Error message to write + public void WriteError(string message) + { + // Write to the console unless silent + if (!Silent) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ResetColor(); + } + + // Write to the log if specified + _log?.WriteLine(message); + + // Increment the number of errors + Errors++; + } + + /// + /// Create a program context + /// + /// Program arguments + /// Program context + /// Thrown on invalid arguments + public static Context Create(string[] args) + { + // Process arguments + var version = false; + var help = false; + var silent = false; + string? logFile = null; + var extra = new List(); + using var arg = args.AsEnumerable().GetEnumerator(); + while (arg.MoveNext()) + { + switch (arg.Current) + { + case "-v": + case "--version": + // Handle version query + version = true; + break; + + case "-h": + case "-?": + case "--help": + // Handle help query + help = true; + break; + + case "-s": + case "--silent": + silent = true; + break; + + case "-l": + case "--log": + // Handle logging output + if (!arg.MoveNext()) + throw new InvalidOperationException("Missing log output filename"); + logFile = arg.Current; + break; + + default: + // Handle unknown argument as start of extra parameters + do + { + extra.Add(arg.Current); + } while (arg.MoveNext()); + break; + } + } + + // Return the new context + return new Context(logFile != null ? new StreamWriter(logFile) : null, extra.AsReadOnly()) + { + Version = version, + Help = help, + Silent = silent + }; + } +} \ No newline at end of file diff --git a/src/DemaConsulting.SpdxTool/DemaConsulting.SpdxTool.csproj b/src/DemaConsulting.SpdxTool/DemaConsulting.SpdxTool.csproj index ec4f9c1..ea1c00a 100644 --- a/src/DemaConsulting.SpdxTool/DemaConsulting.SpdxTool.csproj +++ b/src/DemaConsulting.SpdxTool/DemaConsulting.SpdxTool.csproj @@ -52,7 +52,7 @@ - + diff --git a/src/DemaConsulting.SpdxTool/Program.cs b/src/DemaConsulting.SpdxTool/Program.cs index d7905ab..0b4ea52 100644 --- a/src/DemaConsulting.SpdxTool/Program.cs +++ b/src/DemaConsulting.SpdxTool/Program.cs @@ -38,97 +38,117 @@ public static class Program ?.InformationalVersion ?? "Unknown"; /// - /// Application entry point + /// Application entry point /// /// Program arguments public static void Main(string[] args) { - // Handle querying for version - if (args.Length == 1 && (args[0] == "-v" || args[0] == "--version")) + try + { + using var context = Context.Create(args); + Run(context); + Environment.ExitCode = context.Errors > 0 ? 1 : 0; + } + catch (InvalidOperationException e) + { + // Report standard failure + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {e.Message}"); + Console.ResetColor(); + Environment.Exit(1); + } + catch (Exception e) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {e}"); + Console.ResetColor(); + throw; + } + } + + /// + /// Run the program context + /// + /// Program context + public static void Run(Context context) + { + // Handle version query + if (context.Version) { - Console.WriteLine(Version); + context.WriteLine(Version); return; } // Print version banner - Console.WriteLine($"DemaConsulting.SpdxTool {Version}\n"); + context.WriteLine($"DemaConsulting.SpdxTool {Version}\n"); - // Fail if no arguments specified - if (args.Length == 0) + // Handle help query + if (context.Help) { - ReportError("No arguments specified"); - PrintUsage(); + PrintUsage(context); return; } - // Handle printing usage information - if (args[0] == "-h" || args[0] == "--help") + // Handle missing arguments + if (context.Arguments.Count == 0) { - PrintUsage(); + context.WriteError("Error: Missing arguments"); + PrintUsage(context); return; } try { - if (CommandsRegistry.Commands.TryGetValue(args[0], out var entry)) + var command = context.Arguments.First(); + if (CommandsRegistry.Commands.TryGetValue(command, out var entry)) { // Run the command - entry.Instance.Run(args.Skip(1).ToArray()); + entry.Instance.Run(context, context.Arguments.Skip(1).ToArray()); } else { // Report unknown command - ReportError($"Unknown command: '{args[0]}'"); - PrintUsage(); + context.WriteError($"Error: Unknown command '{command}'"); + PrintUsage(context); } } catch (CommandUsageException ex) { // Report usage exception and usage information - ReportError(ex.Message); - PrintUsage(); + context.WriteError($"Error: {ex.Message}"); + PrintUsage(context); } catch (CommandErrorException ex) { // Report error exception - ReportError(ex.Message); + context.WriteError($"Error: {ex.Message}"); } catch (Exception ex) { // Report unknown exception - ReportError(ex.ToString()); + context.WriteError(ex.ToString()); } } /// - /// Print usage information + /// Print usage information /// - public static void PrintUsage() + /// Program context + public static void PrintUsage(Context context) { - Console.WriteLine("Usage: spdx-tool [options] [arguments]"); - Console.WriteLine(); - Console.WriteLine("Options:"); - Console.WriteLine(" -h, --help Show this help message and exit"); - Console.WriteLine(" -v, --version Show version information and exit"); - Console.WriteLine(); - Console.WriteLine("Commands:"); + context.WriteLine( + """ + Usage: spdx-tool [options] [arguments] + + Options: + -h, --help Show this help message and exit + -v, --version Show version information and exit + -l, --log Log output to file + -s, --silent Silence console output + + Commands: + """); foreach (var command in CommandsRegistry.Commands.Values) - Console.WriteLine($" {command.CommandLine,-40} {command.Summary}"); - } - - /// - /// Report an error message to the console in red - /// - /// Error message - private static void ReportError(string message) - { - // Write an error message to the console - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error: {message}"); - Console.ResetColor(); - Console.WriteLine(); - - // Set the exit code to 1 as an error has occurred - Environment.ExitCode = 1; + context.WriteLine($" {command.CommandLine,-40} {command.Summary}"); } } \ No newline at end of file diff --git a/test/DemaConsulting.SpdxTool.Tests/DemaConsulting.SpdxTool.Tests.csproj b/test/DemaConsulting.SpdxTool.Tests/DemaConsulting.SpdxTool.Tests.csproj index 7242e3a..ed5200d 100644 --- a/test/DemaConsulting.SpdxTool.Tests/DemaConsulting.SpdxTool.Tests.csproj +++ b/test/DemaConsulting.SpdxTool.Tests/DemaConsulting.SpdxTool.Tests.csproj @@ -19,9 +19,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/test/DemaConsulting.SpdxTool.Tests/TestLog.cs b/test/DemaConsulting.SpdxTool.Tests/TestLog.cs new file mode 100644 index 0000000..9b3cc72 --- /dev/null +++ b/test/DemaConsulting.SpdxTool.Tests/TestLog.cs @@ -0,0 +1,94 @@ +// Copyright (c) 2024 DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DemaConsulting.SpdxTool.Tests; + +/// +/// Tests for logging output. +/// +[TestClass] +public class TestLog +{ + /// + /// Test that logging functions when '-l' is specified + /// + [TestMethod] + public void Log_Short() + { + try + { + // Run the command + var exitCode = Runner.Run( + out _, + "dotnet", + "DemaConsulting.SpdxTool.dll", + "-l", "output.log", + "-h"); + + // Verify success + Assert.AreEqual(0, exitCode); + + // Assert log file written + Assert.IsTrue(File.Exists("output.log")); + + // Verify the log contains the usage information + var log = File.ReadAllText("output.log"); + StringAssert.Contains(log, "Usage: spdx-tool"); + } + finally + { + // Delete output file + File.Delete("output.log"); + } + } + + /// + /// Test that logging functions when '--log' is specified + /// + [TestMethod] + public void Log_Long() + { + try + { + // Run the command + var exitCode = Runner.Run( + out _, + "dotnet", + "DemaConsulting.SpdxTool.dll", + "--log", "output.log", + "--help"); + + // Verify success + Assert.AreEqual(0, exitCode); + + // Assert log file written + Assert.IsTrue(File.Exists("output.log")); + + // Verify the log contains the usage information + var log = File.ReadAllText("output.log"); + StringAssert.Contains(log, "Usage: spdx-tool"); + } + finally + { + // Delete output file + File.Delete("output.log"); + } + } +} \ No newline at end of file diff --git a/test/DemaConsulting.SpdxTool.Tests/TestSilent.cs b/test/DemaConsulting.SpdxTool.Tests/TestSilent.cs new file mode 100644 index 0000000..5f8c737 --- /dev/null +++ b/test/DemaConsulting.SpdxTool.Tests/TestSilent.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2024 DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DemaConsulting.SpdxTool.Tests; + +/// +/// Tests for silencing output. +/// +[TestClass] +public class TestSilent +{ + /// + /// Test that silence functions when '-s' is specified + /// + [TestMethod] + public void Silent_Short() + { + // Run the command + var exitCode = Runner.Run( + out var output, + "dotnet", + "DemaConsulting.SpdxTool.dll", + "-s", + "-h"); + + // Verify success + Assert.AreEqual(0, exitCode); + + // Verify the output is empty + Assert.AreEqual(0, output.Length); + } + + /// + /// Test that silence functions when '--silent' is specified + /// + [TestMethod] + public void Silent_Long() + { + // Run the command + var exitCode = Runner.Run( + out var output, + "dotnet", + "DemaConsulting.SpdxTool.dll", + "--silent", + "--help"); + + // Verify success + Assert.AreEqual(0, exitCode); + + // Verify the output is empty + Assert.AreEqual(0, output.Length); + } +} \ No newline at end of file diff --git a/test/DemaConsulting.SpdxTool.Tests/TestUnknownCommand.cs b/test/DemaConsulting.SpdxTool.Tests/TestUnknownCommand.cs index d31f6b5..04cf6e5 100644 --- a/test/DemaConsulting.SpdxTool.Tests/TestUnknownCommand.cs +++ b/test/DemaConsulting.SpdxTool.Tests/TestUnknownCommand.cs @@ -41,6 +41,6 @@ public void UnknownCommand() // Verify error reported Assert.AreEqual(1, exitCode); - StringAssert.Contains(output, "Unknown command: 'unknown-command'"); + StringAssert.Contains(output, "Error: Unknown command 'unknown-command'"); } } \ No newline at end of file diff --git a/test/DemaConsulting.SpdxTool.Tests/TestUsage.cs b/test/DemaConsulting.SpdxTool.Tests/TestUsage.cs index d586cca..3f99d8e 100644 --- a/test/DemaConsulting.SpdxTool.Tests/TestUsage.cs +++ b/test/DemaConsulting.SpdxTool.Tests/TestUsage.cs @@ -42,7 +42,7 @@ public void Usage_NoArguments() Assert.AreEqual(1, exitCode); // Verify the output contains the usage information - StringAssert.Contains(output, "No arguments specified"); + StringAssert.Contains(output, "Error: Missing arguments"); StringAssert.Contains(output, "Usage: spdx-tool"); }