diff --git a/README.md b/README.md index cd59de2..d1b571c 100755 --- a/README.md +++ b/README.md @@ -193,6 +193,8 @@ Usage: Options: --input-files Input BOM filenames (separate filenames with a space). + --input-files-list One or more text file(s) with input BOM filenames (one per line). + --input-files-nul-list One or more text-like file(s) with input BOM filenames (separated by 0x00 characters). --output-file Output BOM filename, will write to stdout if no value provided. --input-format Specify input file format. --output-format Specify output file format. @@ -205,6 +207,24 @@ Options: Note: To perform a hierarchical merge all BOMs need the subject of the BOM described in the metadata component element. +The `--input-files-list` option can be useful if you have so many filenames to +merge that your shell interpreter command-line limit is exceeded if you list +them all as `--input-files`, or if your path names have spaces. + +The related `--input-files-nul-list` is intended for lists prepared by commands +like `find ... -print0` and makes sense on filesystems where carriage-return +and/or line-feed characters may validly be present in a path name component. +Note: behavior with multi-byte encodings (Unicode family) where a 0x00 byte +can be part of a character may be undefined. + +If you specify several of these options, the effective file lists will be +concatenated before the actual merge (first the individual `--input-files`, +then the contents of `--input-files-list`, and finally the contents of +`--input-files-nul-list`). If you have a document crafted to describe the +root of your product hierarchy tree, it is recommended to list it as the +first of individual `--input-files` (or otherwise on first line among used +lists). + ### Examples Merge two XML formatted BOMs: diff --git a/src/cyclonedx/Commands/MergeCommand.cs b/src/cyclonedx/Commands/MergeCommand.cs index 7a96404..8085d82 100644 --- a/src/cyclonedx/Commands/MergeCommand.cs +++ b/src/cyclonedx/Commands/MergeCommand.cs @@ -22,6 +22,8 @@ using System.Threading.Tasks; using CycloneDX.Models; using CycloneDX.Utils; +using System.IO; +using System.Collections.Immutable; namespace CycloneDX.Cli.Commands { @@ -32,6 +34,8 @@ public static void Configure(RootCommand rootCommand) Contract.Requires(rootCommand != null); var subCommand = new System.CommandLine.Command("merge", "Merge two or more BOMs"); subCommand.Add(new Option>("--input-files", "Input BOM filenames (separate filenames with a space).")); + subCommand.Add(new Option>("--input-files-list", "One or more text file(s) with input BOM filenames (one per line).")); + subCommand.Add(new Option>("--input-files-nul-list", "One or more text-like file(s) with input BOM filenames (separated by 0x00 characters).")); subCommand.Add(new Option("--output-file", "Output BOM filename, will write to stdout if no value provided.")); subCommand.Add(new Option("--input-format", "Specify input file format.")); subCommand.Add(new Option("--output-format", "Specify output file format.")); @@ -61,7 +65,7 @@ public static async Task Merge(MergeCommandOptions options) return (int)ExitCode.ParameterValidationError; } - var inputBoms = await InputBoms(options.InputFiles, options.InputFormat, outputToConsole).ConfigureAwait(false); + var inputBoms = await InputBoms(DetermineInputFiles(options), options.InputFormat, outputToConsole).ConfigureAwait(false); Component bomSubject = null; if (options.Group != null || options.Name != null || options.Version != null) @@ -92,7 +96,7 @@ public static async Task Merge(MergeCommandOptions options) // otherwise use the first non-null component from the input BOMs as the default foreach (var bom in inputBoms) { - if(bom.Metadata != null && bom.Metadata.Component != null) + if (bom.Metadata != null && bom.Metadata.Component != null) { outputBom.Metadata.Component = bom.Metadata.Component; break; @@ -113,6 +117,73 @@ public static async Task Merge(MergeCommandOptions options) return await CliUtils.OutputBomHelper(outputBom, options.OutputFormat, options.OutputFile).ConfigureAwait(false); } + private static List DetermineInputFiles(MergeCommandOptions options) + { + List InputFiles; + if (options.InputFiles != null) + { + InputFiles = (List)options.InputFiles; + } + else + { + InputFiles = new List(); + } + + Console.WriteLine($"Got " + InputFiles.Count + " individual input file name(s): ['" + string.Join("', '", InputFiles) + "']"); + if (options.InputFilesList != null) + { + // For some reason, without an immutable list this claims + // modifications of the iterable during iteration and fails: + ImmutableList InputFilesList = options.InputFilesList.ToImmutableList(); + Console.WriteLine($"Processing " + InputFilesList.Count + " file(s) with list of actual input file names: ['" + string.Join("', '", InputFilesList) + "']"); + foreach (string OneInputFileList in InputFilesList) + { + Console.WriteLine($"Adding to input file list from " + OneInputFileList); + string[] lines = File.ReadAllLines(OneInputFileList); + int count = 0; + foreach (string line in lines) + { + if (string.IsNullOrEmpty(line)) continue; + if (InputFiles.Contains(line)) continue; + InputFiles.Add(line); + count++; + } + Console.WriteLine($"Got " + count + " new entries from " + OneInputFileList); + } + } + + if (options.InputFilesNulList != null) + { + ImmutableList InputFilesNulList = options.InputFilesNulList.ToImmutableList(); + Console.WriteLine($"Processing " + InputFilesNulList.Count + " file(s) with NUL-separated list of actual input file names: ['" + string.Join("', '", InputFilesNulList) + "']"); + foreach (string OneInputFileList in InputFilesNulList) + { + Console.WriteLine($"Adding to input file list from " + OneInputFileList); + string[] lines = File.ReadAllText(OneInputFileList).Split('\0'); + int count = 0; + foreach (string line in lines) + { + if (string.IsNullOrEmpty(line)) continue; + if (InputFiles.Contains(line)) continue; + InputFiles.Add(line); + count++; + } + Console.WriteLine($"Got " + count + " new entries from " + OneInputFileList); + } + } + + if (InputFiles.Count == 0) + { + // Revert to legacy (error-handling) behavior below + // in case the parameter was not passed + InputFiles = null; + } else { + Console.WriteLine($"Determined " + InputFiles.Count + " input files to merge"); + } + + return InputFiles; + } + private static async Task> InputBoms(IEnumerable inputFilenames, CycloneDXBomFormat inputFormat, bool outputToConsole) { var boms = new List(); diff --git a/src/cyclonedx/Commands/MergeCommandOptions.cs b/src/cyclonedx/Commands/MergeCommandOptions.cs index f3078c4..b2c847c 100644 --- a/src/cyclonedx/Commands/MergeCommandOptions.cs +++ b/src/cyclonedx/Commands/MergeCommandOptions.cs @@ -21,6 +21,8 @@ namespace CycloneDX.Cli.Commands public class MergeCommandOptions { public IList InputFiles { get; set; } + public IList InputFilesList { get; set; } + public IList InputFilesNulList { get; set; } public string OutputFile { get; set; } public CycloneDXBomFormat InputFormat { get; set; } public CycloneDXBomFormat OutputFormat { get; set; } @@ -29,4 +31,4 @@ public class MergeCommandOptions public string Name { get; set; } public string Version { get; set; } } -} \ No newline at end of file +}