diff --git a/src/Sign.Cli/AzureKeyVaultCommand.cs b/src/Sign.Cli/AzureKeyVaultCommand.cs index d6616635..8bbdeea3 100644 --- a/src/Sign.Cli/AzureKeyVaultCommand.cs +++ b/src/Sign.Cli/AzureKeyVaultCommand.cs @@ -17,7 +17,7 @@ internal sealed class AzureKeyVaultCommand : Command internal Option CertificateOption { get; } = new(["--azure-key-vault-certificate", "-kvc"], AzureKeyVaultResources.CertificateOptionDescription); internal AzureCredentialOptions AzureCredentialOptions { get; } = new(); - internal Argument FileArgument { get; } = new("file(s)", Resources.FilesArgumentDescription); + internal Argument?> FilesArgument { get; } = new("file(s)", Resources.FilesArgumentDescription) { Arity = ArgumentArity.OneOrMore }; internal AzureKeyVaultCommand(CodeCommand codeCommand, IServiceProviderFactory serviceProviderFactory) : base("azure-key-vault", AzureKeyVaultResources.CommandDescription) @@ -32,13 +32,13 @@ internal AzureKeyVaultCommand(CodeCommand codeCommand, IServiceProviderFactory s AddOption(CertificateOption); AzureCredentialOptions.AddOptionsToCommand(this); - AddArgument(FileArgument); + AddArgument(FilesArgument); this.SetHandler(async (InvocationContext context) => { - string? fileArgument = context.ParseResult.GetValueForArgument(FileArgument); + List? filesArgument = context.ParseResult.GetValueForArgument(FilesArgument); - if (string.IsNullOrEmpty(fileArgument)) + if (filesArgument is not { Count: > 0 }) { context.Console.Error.WriteLine(Resources.MissingFileValue); context.ExitCode = ExitCode.InvalidOptions; @@ -47,7 +47,7 @@ internal AzureKeyVaultCommand(CodeCommand codeCommand, IServiceProviderFactory s // this check exists as a courtesy to users who may have been signing .clickonce files via the old workaround. // at some point we should remove this check, probably once we hit v1.0 - if (fileArgument.EndsWith(".clickonce", StringComparison.OrdinalIgnoreCase)) + if (filesArgument.Any(x => x.EndsWith(".clickonce", StringComparison.OrdinalIgnoreCase))) { context.Console.Error.WriteLine(AzureKeyVaultResources.ClickOnceExtensionNotSupported); context.ExitCode = ExitCode.InvalidOptions; @@ -66,7 +66,7 @@ internal AzureKeyVaultCommand(CodeCommand codeCommand, IServiceProviderFactory s string certificateId = context.ParseResult.GetValueForOption(CertificateOption)!; KeyVaultServiceProvider keyVaultServiceProvider = new(credential, url, certificateId); - await codeCommand.HandleAsync(context, serviceProviderFactory, keyVaultServiceProvider, fileArgument); + await codeCommand.HandleAsync(context, serviceProviderFactory, keyVaultServiceProvider, filesArgument); }); } } diff --git a/src/Sign.Cli/CertificateStoreCommand.cs b/src/Sign.Cli/CertificateStoreCommand.cs index ca722ab1..4d7057df 100644 --- a/src/Sign.Cli/CertificateStoreCommand.cs +++ b/src/Sign.Cli/CertificateStoreCommand.cs @@ -23,7 +23,7 @@ internal sealed class CertificateStoreCommand : Command internal Option UseMachineKeyContainerOption { get; } = new(["--use-machine-key-container", "-km"], getDefaultValue: () => false, description: CertificateStoreResources.UseMachineKeyContainerOptionDescription); internal Option InteractiveOption { get; } = new(["--interactive", "-i"], getDefaultValue: () => false, description: CertificateStoreResources.InteractiveDescription); - internal Argument FileArgument { get; } = new("file(s)", Resources.FilesArgumentDescription); + internal Argument?> FilesArgument { get; } = new("file(s)", Resources.FilesArgumentDescription) { Arity = ArgumentArity.OneOrMore }; internal CertificateStoreCommand(CodeCommand codeCommand, IServiceProviderFactory serviceProviderFactory) : base("certificate-store", Resources.CertificateStoreCommandDescription) @@ -40,13 +40,13 @@ internal CertificateStoreCommand(CodeCommand codeCommand, IServiceProviderFactor AddOption(PrivateKeyContainerOption); AddOption(UseMachineKeyContainerOption); AddOption(InteractiveOption); - AddArgument(FileArgument); + AddArgument(FilesArgument); this.SetHandler(async (InvocationContext context) => { - string? fileArgument = context.ParseResult.GetValueForArgument(FileArgument); + List? filesArgument = context.ParseResult.GetValueForArgument(FilesArgument); - if (string.IsNullOrEmpty(fileArgument)) + if (filesArgument is not { Count: > 0 }) { context.Console.Error.WriteLine(Resources.MissingFileValue); context.ExitCode = ExitCode.InvalidOptions; @@ -111,7 +111,7 @@ internal CertificateStoreCommand(CodeCommand codeCommand, IServiceProviderFactor useMachineKeyContainer, isInteractive); - await codeCommand.HandleAsync(context, serviceProviderFactory, certificateStoreServiceProvider, fileArgument); + await codeCommand.HandleAsync(context, serviceProviderFactory, certificateStoreServiceProvider, filesArgument); }); } diff --git a/src/Sign.Cli/CodeCommand.cs b/src/Sign.Cli/CodeCommand.cs index a541d60d..ebc9f2e8 100644 --- a/src/Sign.Cli/CodeCommand.cs +++ b/src/Sign.Cli/CodeCommand.cs @@ -58,7 +58,7 @@ internal CodeCommand() AddGlobalOption(VerbosityOption); } - internal async Task HandleAsync(InvocationContext context, IServiceProviderFactory serviceProviderFactory, ISignatureProvider signatureProvider, string fileArgument) + internal async Task HandleAsync(InvocationContext context, IServiceProviderFactory serviceProviderFactory, ISignatureProvider signatureProvider, IEnumerable filesArgument) { // Some of the options have a default value and that is why we can safely use // the null-forgiving operator (!) to simplify the code. @@ -95,44 +95,47 @@ internal async Task HandleAsync(InvocationContext context, IServiceProviderFacto (IServiceProvider serviceProvider) => signatureProvider.GetCertificateProvider(serviceProvider)); }); - List inputFiles; + List inputFiles = []; - // If we're going to glob, we can't be fully rooted currently (fix me later) - bool isGlob = fileArgument.Contains('*'); - - if (isGlob) + foreach (var fileArgument in filesArgument) { - if (Path.IsPathRooted(fileArgument)) + // If we're going to glob, we can't be fully rooted currently (fix me later) + bool isGlob = fileArgument.Contains('*'); + + if (isGlob) { - context.Console.Error.WriteLine(Resources.InvalidFileValue); - context.ExitCode = ExitCode.InvalidOptions; - return; - } + if (Path.IsPathRooted(fileArgument)) + { + context.Console.Error.WriteLine(Resources.InvalidFileValue); + context.ExitCode = ExitCode.InvalidOptions; + return; + } - IFileListReader fileListReader = serviceProvider.GetRequiredService(); - IFileMatcher fileMatcher = serviceProvider.GetRequiredService(); + IFileListReader fileListReader = serviceProvider.GetRequiredService(); + IFileMatcher fileMatcher = serviceProvider.GetRequiredService(); - using (MemoryStream stream = new(Encoding.UTF8.GetBytes(fileArgument))) - using (StreamReader reader = new(stream)) - { - fileListReader.Read(reader, out Matcher? matcher, out Matcher? antiMatcher); + using (MemoryStream stream = new(Encoding.UTF8.GetBytes(fileArgument))) + using (StreamReader reader = new(stream)) + { + fileListReader.Read(reader, out Matcher? matcher, out Matcher? antiMatcher); - DirectoryInfoBase directory = new DirectoryInfoWrapper(baseDirectory); + DirectoryInfoBase directory = new DirectoryInfoWrapper(baseDirectory); - IEnumerable matches = fileMatcher.EnumerateMatches(directory, matcher); + IEnumerable matches = fileMatcher.EnumerateMatches(directory, matcher); - if (antiMatcher is not null) - { - IEnumerable antiMatches = fileMatcher.EnumerateMatches(directory, antiMatcher); - matches = matches.Except(antiMatches, FileInfoComparer.Instance); - } + if (antiMatcher is not null) + { + IEnumerable antiMatches = fileMatcher.EnumerateMatches(directory, antiMatcher); + matches = matches.Except(antiMatches, FileInfoComparer.Instance); + } - inputFiles = matches.ToList(); + inputFiles.AddRange(matches); + } + } + else + { + inputFiles.Add(new FileInfo(ExpandFilePath(baseDirectory, fileArgument))); } - } - else - { - inputFiles = [new FileInfo(ExpandFilePath(baseDirectory, fileArgument))]; } FileInfo? fileList = null; diff --git a/src/Sign.Cli/TrustedSigningCommand.cs b/src/Sign.Cli/TrustedSigningCommand.cs index 1247d5b2..e4c0967f 100644 --- a/src/Sign.Cli/TrustedSigningCommand.cs +++ b/src/Sign.Cli/TrustedSigningCommand.cs @@ -18,7 +18,7 @@ internal sealed class TrustedSigningCommand : Command internal Option CertificateProfileOption { get; } = new(["--trusted-signing-certificate-profile", "-tscp"], TrustedSigningResources.CertificateProfileOptionDescription); internal AzureCredentialOptions AzureCredentialOptions { get; } = new(); - internal Argument FileArgument { get; } = new("file(s)", Resources.FilesArgumentDescription); + internal Argument?> FilesArgument { get; } = new("file(s)", Resources.FilesArgumentDescription) { Arity = ArgumentArity.OneOrMore }; internal TrustedSigningCommand(CodeCommand codeCommand, IServiceProviderFactory serviceProviderFactory) : base("trusted-signing", TrustedSigningResources.CommandDescription) @@ -35,13 +35,13 @@ internal TrustedSigningCommand(CodeCommand codeCommand, IServiceProviderFactory AddOption(CertificateProfileOption); AzureCredentialOptions.AddOptionsToCommand(this); - AddArgument(FileArgument); + AddArgument(FilesArgument); this.SetHandler(async (InvocationContext context) => { - string? fileArgument = context.ParseResult.GetValueForArgument(FileArgument); + List? filesArgument = context.ParseResult.GetValueForArgument(FilesArgument); - if (string.IsNullOrEmpty(fileArgument)) + if (filesArgument is not { Count: > 0 }) { context.Console.Error.WriteLine(Resources.MissingFileValue); context.ExitCode = ExitCode.InvalidOptions; @@ -62,7 +62,7 @@ internal TrustedSigningCommand(CodeCommand codeCommand, IServiceProviderFactory TrustedSigningServiceProvider trustedSigningServiceProvider = new(credential, endpointUrl, accountName, certificateProfileName); - await codeCommand.HandleAsync(context, serviceProviderFactory, trustedSigningServiceProvider, fileArgument); + await codeCommand.HandleAsync(context, serviceProviderFactory, trustedSigningServiceProvider, filesArgument); }); } } diff --git a/test/Sign.Cli.Test/SignCommandTests.cs b/test/Sign.Cli.Test/SignCommandTests.cs index 88808b7c..e3ce8b36 100644 --- a/test/Sign.Cli.Test/SignCommandTests.cs +++ b/test/Sign.Cli.Test/SignCommandTests.cs @@ -93,7 +93,7 @@ public void Command_WhenAllOptionsAndArgumentAreValid_HasNoError() Assert.Equal(KeyVaultUrl, result.GetValueForOption(_azureKeyVaultCommand.UrlOption)!.OriginalString); Assert.Equal(CertificateName, result.GetValueForOption(_azureKeyVaultCommand.CertificateOption)); Assert.Equal(TimestampUrl, result.GetValueForOption(_codeCommand.TimestampUrlOption)!.OriginalString); - Assert.Equal(File, result.GetValueForArgument(_azureKeyVaultCommand.FileArgument)); + Assert.Equal([File], result.GetValueForArgument(_azureKeyVaultCommand.FilesArgument)); } } }