diff --git a/.gitignore b/.gitignore index 4ce6fdd..e0b77ae 100644 --- a/.gitignore +++ b/.gitignore @@ -143,6 +143,7 @@ _TeamCity* _NCrunch_* .*crunch*.local.xml nCrunchTemp_* +.ncrunchsolution # MightyMoose *.mm.* diff --git a/CliFx.Tests/CliApplicationBuilderTests.cs b/CliFx.Tests/CliApplicationBuilderTests.cs index c04e15a..171875c 100644 --- a/CliFx.Tests/CliApplicationBuilderTests.cs +++ b/CliFx.Tests/CliApplicationBuilderTests.cs @@ -1,8 +1,9 @@ -using System; +using NUnit.Framework; +using System; using System.IO; using CliFx.Services; +using CliFx.Tests.Stubs; using CliFx.Tests.TestCommands; -using NUnit.Framework; namespace CliFx.Tests { @@ -20,8 +21,8 @@ namespace CliFx.Tests builder .AddCommand(typeof(HelloWorldDefaultCommand)) .AddCommandsFrom(typeof(HelloWorldDefaultCommand).Assembly) - .AddCommands(new[] {typeof(HelloWorldDefaultCommand)}) - .AddCommandsFrom(new[] {typeof(HelloWorldDefaultCommand).Assembly}) + .AddCommands(new[] { typeof(HelloWorldDefaultCommand) }) + .AddCommandsFrom(new[] { typeof(HelloWorldDefaultCommand).Assembly }) .AddCommandsFromThisAssembly() .AllowDebugMode() .AllowPreviewMode() @@ -30,8 +31,9 @@ namespace CliFx.Tests .UseVersionText("test") .UseDescription("test") .UseConsole(new VirtualConsole(TextWriter.Null)) - .UseCommandFactory(schema => (ICommand) Activator.CreateInstance(schema.Type)) + .UseCommandFactory(schema => (ICommand)Activator.CreateInstance(schema.Type)) .UseCommandOptionInputConverter(new CommandOptionInputConverter()) + .UseEnvironmentVariablesProvider(new EnvironmentVariablesProviderStub()) .Build(); } diff --git a/CliFx.Tests/CliApplicationTests.cs b/CliFx.Tests/CliApplicationTests.cs index b193bc4..c9c2b1b 100644 --- a/CliFx.Tests/CliApplicationTests.cs +++ b/CliFx.Tests/CliApplicationTests.cs @@ -1,11 +1,12 @@ -using System; +using FluentAssertions; +using NUnit.Framework; +using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using CliFx.Services; +using CliFx.Tests.Stubs; using CliFx.Tests.TestCommands; -using FluentAssertions; -using NUnit.Framework; namespace CliFx.Tests { @@ -17,104 +18,104 @@ namespace CliFx.Tests private static IEnumerable GetTestCases_RunAsync() { yield return new TestCaseData( - new[] {typeof(HelloWorldDefaultCommand)}, + new[] { typeof(HelloWorldDefaultCommand) }, new string[0], "Hello world." ); - + yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "}, + new[] { typeof(ConcatCommand) }, + new[] { "concat", "-i", "foo", "-i", "bar", "-s", " " }, "foo bar" ); - + yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"concat", "-i", "one", "two", "three", "-s", ", "}, + new[] { typeof(ConcatCommand) }, + new[] { "concat", "-i", "one", "two", "three", "-s", ", " }, "one, two, three" ); yield return new TestCaseData( - new[] {typeof(DivideCommand)}, - new[] {"div", "-D", "24", "-d", "8"}, + new[] { typeof(DivideCommand) }, + new[] { "div", "-D", "24", "-d", "8" }, "3" ); yield return new TestCaseData( - new[] {typeof(HelloWorldDefaultCommand)}, - new[] {"--version"}, + new[] { typeof(HelloWorldDefaultCommand) }, + new[] { "--version" }, TestVersionText ); yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"--version"}, + new[] { typeof(ConcatCommand) }, + new[] { "--version" }, TestVersionText ); - + yield return new TestCaseData( - new[] {typeof(HelloWorldDefaultCommand)}, - new[] {"-h"}, + new[] { typeof(HelloWorldDefaultCommand) }, + new[] { "-h" }, null ); yield return new TestCaseData( - new[] {typeof(HelloWorldDefaultCommand)}, - new[] {"--help"}, + new[] { typeof(HelloWorldDefaultCommand) }, + new[] { "--help" }, null ); - + yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, + new[] { typeof(ConcatCommand) }, new string[0], null ); - + yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"-h"}, + new[] { typeof(ConcatCommand) }, + new[] { "-h" }, null ); yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"--help"}, - null - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"concat", "-h"}, + new[] { typeof(ConcatCommand) }, + new[] { "--help" }, null ); yield return new TestCaseData( - new[] {typeof(ExceptionCommand)}, - new[] {"exc", "-h"}, + new[] { typeof(ConcatCommand) }, + new[] { "concat", "-h" }, null ); yield return new TestCaseData( - new[] {typeof(CommandExceptionCommand)}, - new[] {"exc", "-h"}, + new[] { typeof(ExceptionCommand) }, + new[] { "exc", "-h" }, null ); yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"[preview]"}, + new[] { typeof(CommandExceptionCommand) }, + new[] { "exc", "-h" }, null ); yield return new TestCaseData( - new[] {typeof(ExceptionCommand)}, - new[] {"exc", "[preview]"}, + new[] { typeof(ConcatCommand) }, + new[] { "[preview]" }, null ); yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"concat", "[preview]", "-o", "value"}, + new[] { typeof(ExceptionCommand) }, + new[] { "exc", "[preview]" }, + null + ); + + yield return new TestCaseData( + new[] { typeof(ConcatCommand) }, + new[] { "concat", "[preview]", "-o", "value" }, null ); } @@ -128,38 +129,38 @@ namespace CliFx.Tests ); yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"non-existing"}, + new[] { typeof(ConcatCommand) }, + new[] { "non-existing" }, null, null ); yield return new TestCaseData( - new[] {typeof(ExceptionCommand)}, - new[] {"exc"}, + new[] { typeof(ExceptionCommand) }, + new[] { "exc" }, null, null ); yield return new TestCaseData( - new[] {typeof(CommandExceptionCommand)}, - new[] {"exc"}, + new[] { typeof(CommandExceptionCommand) }, + new[] { "exc" }, null, null ); yield return new TestCaseData( - new[] {typeof(CommandExceptionCommand)}, - new[] {"exc"}, + new[] { typeof(CommandExceptionCommand) }, + new[] { "exc" }, null, null ); - + yield return new TestCaseData( - new[] {typeof(CommandExceptionCommand)}, - new[] {"exc", "-m", "foo bar"}, + new[] { typeof(CommandExceptionCommand) }, + new[] { "exc", "-m", "foo bar" }, "foo bar", null ); - + yield return new TestCaseData( - new[] {typeof(CommandExceptionCommand)}, - new[] {"exc", "-m", "foo bar", "-c", "666"}, + new[] { typeof(CommandExceptionCommand) }, + new[] { "exc", "-m", "foo bar", "-c", "666" }, "foo bar", 666 ); } @@ -173,11 +174,13 @@ namespace CliFx.Tests using (var stdoutStream = new StringWriter()) { var console = new VirtualConsole(stdoutStream); + var environmentVariablesProvider = new EnvironmentVariablesProviderStub(); var application = new CliApplicationBuilder() .AddCommands(commandTypes) .UseVersionText(TestVersionText) .UseConsole(console) + .UseEnvironmentVariablesProvider(environmentVariablesProvider) .Build(); // Act @@ -203,10 +206,12 @@ namespace CliFx.Tests using (var stderrStream = new StringWriter()) { var console = new VirtualConsole(TextWriter.Null, stderrStream); + var environmentVariablesProvider = new EnvironmentVariablesProviderStub(); var application = new CliApplicationBuilder() .AddCommands(commandTypes) .UseVersionText(TestVersionText) + .UseEnvironmentVariablesProvider(environmentVariablesProvider) .UseConsole(console) .Build(); @@ -219,7 +224,7 @@ namespace CliFx.Tests exitCode.Should().Be(expectedExitCode); else exitCode.Should().NotBe(0); - + if (expectedStdErr != null) stderr.Should().Be(expectedStdErr); else diff --git a/CliFx.Tests/Services/CommandInitializerTests.cs b/CliFx.Tests/Services/CommandInitializerTests.cs index 30923d9..354883a 100644 --- a/CliFx.Tests/Services/CommandInitializerTests.cs +++ b/CliFx.Tests/Services/CommandInitializerTests.cs @@ -1,12 +1,13 @@ -using System; +using FluentAssertions; +using NUnit.Framework; +using System; using System.Collections.Generic; using System.Linq; using CliFx.Exceptions; using CliFx.Models; using CliFx.Services; using CliFx.Tests.TestCommands; -using FluentAssertions; -using NUnit.Framework; +using CliFx.Tests.Stubs; namespace CliFx.Tests.Services { @@ -14,7 +15,7 @@ namespace CliFx.Tests.Services public class CommandInitializerTests { private static CommandSchema GetCommandSchema(Type commandType) => - new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single(); + new CommandSchemaResolver().GetCommandSchemas(new[] { commandType }).Single(); private static IEnumerable GetTestCases_InitializeCommand() { @@ -26,7 +27,7 @@ namespace CliFx.Tests.Services new CommandOptionInput("dividend", "13"), new CommandOptionInput("divisor", "8") }), - new DivideCommand {Dividend = 13, Divisor = 8} + new DivideCommand { Dividend = 13, Divisor = 8 } ); yield return new TestCaseData( @@ -37,7 +38,7 @@ namespace CliFx.Tests.Services new CommandOptionInput("dividend", "13"), new CommandOptionInput("d", "8") }), - new DivideCommand {Dividend = 13, Divisor = 8} + new DivideCommand { Dividend = 13, Divisor = 8 } ); yield return new TestCaseData( @@ -48,7 +49,7 @@ namespace CliFx.Tests.Services new CommandOptionInput("D", "13"), new CommandOptionInput("d", "8") }), - new DivideCommand {Dividend = 13, Divisor = 8} + new DivideCommand { Dividend = 13, Divisor = 8 } ); yield return new TestCaseData( @@ -58,7 +59,7 @@ namespace CliFx.Tests.Services { new CommandOptionInput("i", new[] {"foo", " ", "bar"}) }), - new ConcatCommand {Inputs = new[] {"foo", " ", "bar"}} + new ConcatCommand { Inputs = new[] { "foo", " ", "bar" } } ); yield return new TestCaseData( @@ -69,7 +70,43 @@ namespace CliFx.Tests.Services new CommandOptionInput("i", new[] {"foo", "bar"}), new CommandOptionInput("s", " ") }), - new ConcatCommand {Inputs = new[] {"foo", "bar"}, Separator = " "} + new ConcatCommand { Inputs = new[] { "foo", "bar" }, Separator = " " } + ); + + //Will read a value from environment variables because none is supplied via CommandInput + yield return new TestCaseData( + new EnvironmentVariableCommand(), + GetCommandSchema(typeof(EnvironmentVariableCommand)), + new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables), + new EnvironmentVariableCommand { Option = "A" } + ); + + //Will read multiple values from environment variables because none is supplied via CommandInput + yield return new TestCaseData( + new EnvironmentVariableWithMultipleValuesCommand(), + GetCommandSchema(typeof(EnvironmentVariableWithMultipleValuesCommand)), + new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables), + new EnvironmentVariableWithMultipleValuesCommand { Option = new[] { "A", "B", "C" } } + ); + + //Will not read a value from environment variables because one is supplied via CommandInput + yield return new TestCaseData( + new EnvironmentVariableCommand(), + GetCommandSchema(typeof(EnvironmentVariableCommand)), + new CommandInput(null, new[] + { + new CommandOptionInput("opt", new[] { "X" }) + }, + EnvironmentVariablesProviderStub.EnvironmentVariables), + new EnvironmentVariableCommand { Option = "X" } + ); + + //Will not split environment variable values because underlying property is not a collection + yield return new TestCaseData( + new EnvironmentVariableWithoutCollectionPropertyCommand(), + GetCommandSchema(typeof(EnvironmentVariableWithoutCollectionPropertyCommand)), + new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables), + new EnvironmentVariableWithoutCollectionPropertyCommand { Option = "A;B;C;" } ); } diff --git a/CliFx.Tests/Services/CommandInputParserTests.cs b/CliFx.Tests/Services/CommandInputParserTests.cs index c9549b4..c1a47dd 100644 --- a/CliFx.Tests/Services/CommandInputParserTests.cs +++ b/CliFx.Tests/Services/CommandInputParserTests.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using System.Collections.Generic; using CliFx.Models; using CliFx.Services; -using FluentAssertions; -using NUnit.Framework; +using CliFx.Tests.Stubs; namespace CliFx.Tests.Services { @@ -11,203 +12,238 @@ namespace CliFx.Tests.Services { private static IEnumerable GetTestCases_ParseCommandInput() { - yield return new TestCaseData(new string[0], CommandInput.Empty); + yield return new TestCaseData(new string[0], CommandInput.Empty, new EmptyEnvironmentVariablesProviderStub()); yield return new TestCaseData( - new[] {"--option", "value"}, + new[] { "--option", "value" }, new CommandInput(new[] { new CommandOptionInput("option", "value") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"--option1", "value1", "--option2", "value2"}, + new[] { "--option1", "value1", "--option2", "value2" }, new CommandInput(new[] { new CommandOptionInput("option1", "value1"), new CommandOptionInput("option2", "value2") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"--option", "value1", "value2"}, + new[] { "--option", "value1", "value2" }, new CommandInput(new[] { new CommandOptionInput("option", new[] {"value1", "value2"}) - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"--option", "value1", "--option", "value2"}, + new[] { "--option", "value1", "--option", "value2" }, new CommandInput(new[] { new CommandOptionInput("option", new[] {"value1", "value2"}) - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"-a", "value"}, + new[] { "-a", "value" }, new CommandInput(new[] { new CommandOptionInput("a", "value") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"-a", "value1", "-b", "value2"}, + new[] { "-a", "value1", "-b", "value2" }, new CommandInput(new[] { new CommandOptionInput("a", "value1"), new CommandOptionInput("b", "value2") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"-a", "value1", "value2"}, + new[] { "-a", "value1", "value2" }, new CommandInput(new[] { new CommandOptionInput("a", new[] {"value1", "value2"}) - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"-a", "value1", "-a", "value2"}, + new[] { "-a", "value1", "-a", "value2" }, new CommandInput(new[] { new CommandOptionInput("a", new[] {"value1", "value2"}) - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"--option1", "value1", "-b", "value2"}, + new[] { "--option1", "value1", "-b", "value2" }, new CommandInput(new[] { new CommandOptionInput("option1", "value1"), new CommandOptionInput("b", "value2") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"--switch"}, + new[] { "--switch" }, new CommandInput(new[] { new CommandOptionInput("switch") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"--switch1", "--switch2"}, + new[] { "--switch1", "--switch2" }, new CommandInput(new[] { new CommandOptionInput("switch1"), new CommandOptionInput("switch2") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"-s"}, + new[] { "-s" }, new CommandInput(new[] { new CommandOptionInput("s") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"-a", "-b"}, + new[] { "-a", "-b" }, new CommandInput(new[] { new CommandOptionInput("a"), new CommandOptionInput("b") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"-ab"}, + new[] { "-ab" }, new CommandInput(new[] { new CommandOptionInput("a"), new CommandOptionInput("b") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"-ab", "value"}, + new[] { "-ab", "value" }, new CommandInput(new[] { new CommandOptionInput("a"), new CommandOptionInput("b", "value") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"command"}, - new CommandInput("command") + new[] { "command" }, + new CommandInput("command"), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"command", "--option", "value"}, + new[] { "command", "--option", "value" }, new CommandInput("command", new[] { new CommandOptionInput("option", "value") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"long", "command", "name"}, - new CommandInput("long command name") + new[] { "long", "command", "name" }, + new CommandInput("long command name"), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"long", "command", "name", "--option", "value"}, + new[] { "long", "command", "name", "--option", "value" }, new CommandInput("long command name", new[] { new CommandOptionInput("option", "value") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"[debug]"}, + new[] { "[debug]" }, new CommandInput(null, - new[] {"debug"}, - new CommandOptionInput[0]) + new[] { "debug" }, + new CommandOptionInput[0]), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"[debug]", "[preview]"}, + new[] { "[debug]", "[preview]" }, new CommandInput(null, - new[] {"debug", "preview"}, - new CommandOptionInput[0]) + new[] { "debug", "preview" }, + new CommandOptionInput[0]), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"[debug]", "[preview]", "-o", "value"}, + new[] { "[debug]", "[preview]", "-o", "value" }, new CommandInput(null, - new[] {"debug", "preview"}, + new[] { "debug", "preview" }, new[] { new CommandOptionInput("o", "value") - }) + }), + new EmptyEnvironmentVariablesProviderStub() ); yield return new TestCaseData( - new[] {"command", "[debug]", "[preview]", "-o", "value"}, + new[] { "command", "[debug]", "[preview]", "-o", "value" }, new CommandInput("command", - new[] {"debug", "preview"}, + new[] { "debug", "preview" }, new[] { new CommandOptionInput("o", "value") - }) + }), + new EmptyEnvironmentVariablesProviderStub() + ); + + yield return new TestCaseData( + new[] { "command", "[debug]", "[preview]", "-o", "value" }, + new CommandInput("command", + new[] { "debug", "preview" }, + new[] + { + new CommandOptionInput("o", "value") + }, + EnvironmentVariablesProviderStub.EnvironmentVariables), + new EnvironmentVariablesProviderStub() ); } [Test] [TestCaseSource(nameof(GetTestCases_ParseCommandInput))] public void ParseCommandInput_Test(IReadOnlyList commandLineArguments, - CommandInput expectedCommandInput) + CommandInput expectedCommandInput, IEnvironmentVariablesProvider environmentVariablesProvider) { // Arrange - var parser = new CommandInputParser(); + var parser = new CommandInputParser(environmentVariablesProvider); // Act var commandInput = parser.ParseCommandInput(commandLineArguments); diff --git a/CliFx.Tests/Services/CommandSchemaResolverTests.cs b/CliFx.Tests/Services/CommandSchemaResolverTests.cs index eab7d6c..d74bd3e 100644 --- a/CliFx.Tests/Services/CommandSchemaResolverTests.cs +++ b/CliFx.Tests/Services/CommandSchemaResolverTests.cs @@ -1,11 +1,11 @@ -using System; +using FluentAssertions; +using NUnit.Framework; +using System; using System.Collections.Generic; using CliFx.Exceptions; using CliFx.Models; using CliFx.Services; using CliFx.Tests.TestCommands; -using FluentAssertions; -using NUnit.Framework; namespace CliFx.Tests.Services { @@ -15,30 +15,37 @@ namespace CliFx.Tests.Services private static IEnumerable GetTestCases_GetCommandSchemas() { yield return new TestCaseData( - new[] {typeof(DivideCommand), typeof(ConcatCommand)}, + new[] { typeof(DivideCommand), typeof(ConcatCommand), typeof(EnvironmentVariableCommand) }, new[] { new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.", new[] { new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Dividend)), - "dividend", 'D', true, "The number to divide."), + "dividend", 'D', true, "The number to divide.", null), new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Divisor)), - "divisor", 'd', true, "The number to divide by.") + "divisor", 'd', true, "The number to divide by.", null) }), new CommandSchema(typeof(ConcatCommand), "concat", "Concatenate strings.", new[] { new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Inputs)), - null, 'i', true, "Input strings."), + null, 'i', true, "Input strings.", null), new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Separator)), - null, 's', false, "String separator.") - }) + null, 's', false, "String separator.", null) + }), + new CommandSchema(typeof(EnvironmentVariableCommand), null, "Reads option values from environment variables.", + new[] + { + new CommandOptionSchema(typeof(EnvironmentVariableCommand).GetProperty(nameof(EnvironmentVariableCommand.Option)), + "opt", null, false, null, "ENV_SINGLE_VALUE") + } + ) } ); yield return new TestCaseData( - new[] {typeof(HelloWorldDefaultCommand)}, + new[] { typeof(HelloWorldDefaultCommand) }, new[] { new CommandSchema(typeof(HelloWorldDefaultCommand), null, null, new CommandOptionSchema[0]) @@ -62,7 +69,7 @@ namespace CliFx.Tests.Services { new[] {typeof(NonAnnotatedCommand)} }); - + yield return new TestCaseData(new object[] { new[] {typeof(DuplicateOptionNamesCommand)} @@ -72,7 +79,7 @@ namespace CliFx.Tests.Services { new[] {typeof(DuplicateOptionShortNamesCommand)} }); - + yield return new TestCaseData(new object[] { new[] {typeof(ExceptionCommand), typeof(CommandExceptionCommand)} diff --git a/CliFx.Tests/Stubs/EmptyEnvironmentVariablesProviderStub.cs b/CliFx.Tests/Stubs/EmptyEnvironmentVariablesProviderStub.cs new file mode 100644 index 0000000..6a30403 --- /dev/null +++ b/CliFx.Tests/Stubs/EmptyEnvironmentVariablesProviderStub.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using CliFx.Services; + +namespace CliFx.Tests.Stubs +{ + public class EmptyEnvironmentVariablesProviderStub : IEnvironmentVariablesProvider + { + public IReadOnlyDictionary GetEnvironmentVariables() => new Dictionary(); + } +} diff --git a/CliFx.Tests/Stubs/EnvironmentVariablesProviderStub.cs b/CliFx.Tests/Stubs/EnvironmentVariablesProviderStub.cs new file mode 100644 index 0000000..26e82df --- /dev/null +++ b/CliFx.Tests/Stubs/EnvironmentVariablesProviderStub.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.IO; +using CliFx.Services; + +namespace CliFx.Tests.Stubs +{ + public class EnvironmentVariablesProviderStub : IEnvironmentVariablesProvider + { + public static readonly Dictionary EnvironmentVariables = new Dictionary + { + ["ENV_SINGLE_VALUE"] = "A", + ["ENV_MULTIPLE_VALUES"] = $"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}", + ["ENV_ESCAPED_MULTIPLE_VALUES"] = $"\"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}\"" + }; + + public IReadOnlyDictionary GetEnvironmentVariables() => EnvironmentVariables; + } +} diff --git a/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs b/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs new file mode 100644 index 0000000..11f6d38 --- /dev/null +++ b/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Services; + +namespace CliFx.Tests.TestCommands +{ + [Command(Description = "Reads option values from environment variables.")] + public class EnvironmentVariableCommand : ICommand + { + [CommandOption("opt", EnvironmentVariableName = "ENV_SINGLE_VALUE")] + public string Option { get; set; } + + public Task ExecuteAsync(IConsole console) => Task.CompletedTask; + } +} diff --git a/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs b/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs new file mode 100644 index 0000000..92a93f7 --- /dev/null +++ b/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Services; + +namespace CliFx.Tests.TestCommands +{ + [Command(Description = "Reads multiple option values from environment variables.")] + public class EnvironmentVariableWithMultipleValuesCommand : ICommand + { + [CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")] + public IEnumerable Option { get; set; } + + public Task ExecuteAsync(IConsole console) => Task.CompletedTask; + } +} diff --git a/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs b/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs new file mode 100644 index 0000000..8b61b8e --- /dev/null +++ b/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Services; + +namespace CliFx.Tests.TestCommands +{ + [Command(Description = "Reads one option value from environment variables because target property is not a collection.")] + public class EnvironmentVariableWithoutCollectionPropertyCommand : ICommand + { + [CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")] + public string Option { get; set; } + + public Task ExecuteAsync(IConsole console) => Task.CompletedTask; + } +} diff --git a/CliFx/Attributes/CommandOptionAttribute.cs b/CliFx/Attributes/CommandOptionAttribute.cs index 0aa6e46..c0f34e8 100644 --- a/CliFx/Attributes/CommandOptionAttribute.cs +++ b/CliFx/Attributes/CommandOptionAttribute.cs @@ -28,6 +28,11 @@ namespace CliFx.Attributes /// public string Description { get; set; } + /// + /// Optional environment variable name that will be used as fallback value if no option value is specified. + /// + public string EnvironmentVariableName { get; set; } + /// /// Initializes an instance of . /// @@ -41,7 +46,7 @@ namespace CliFx.Attributes /// Initializes an instance of . /// public CommandOptionAttribute(string name, char shortName) - : this(name, (char?) shortName) + : this(name, (char?)shortName) { } diff --git a/CliFx/CliApplicationBuilder.cs b/CliFx/CliApplicationBuilder.cs index a619d67..e0be026 100644 --- a/CliFx/CliApplicationBuilder.cs +++ b/CliFx/CliApplicationBuilder.cs @@ -26,6 +26,7 @@ namespace CliFx private IConsole _console; private ICommandFactory _commandFactory; private ICommandOptionInputConverter _commandOptionInputConverter; + private IEnvironmentVariablesProvider _environmentVariablesProvider; /// public ICliApplicationBuilder AddCommand(Type commandType) @@ -116,6 +117,13 @@ namespace CliFx return this; } + /// + public ICliApplicationBuilder UseEnvironmentVariablesProvider(IEnvironmentVariablesProvider environmentVariablesProvider) + { + _environmentVariablesProvider = environmentVariablesProvider.GuardNotNull(nameof(environmentVariablesProvider)); + return this; + } + /// public ICliApplication Build() { @@ -126,14 +134,15 @@ namespace CliFx _console = _console ?? new SystemConsole(); _commandFactory = _commandFactory ?? new CommandFactory(); _commandOptionInputConverter = _commandOptionInputConverter ?? new CommandOptionInputConverter(); + _environmentVariablesProvider = _environmentVariablesProvider ?? new EnvironmentVariablesProvider(); // Project parameters to expected types var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description); var configuration = new ApplicationConfiguration(_commandTypes.ToArray(), _isDebugModeAllowed, _isPreviewModeAllowed); return new CliApplication(metadata, configuration, - _console, new CommandInputParser(), new CommandSchemaResolver(), - _commandFactory, new CommandInitializer(_commandOptionInputConverter), new HelpTextRenderer()); + _console, new CommandInputParser(_environmentVariablesProvider), new CommandSchemaResolver(), + _commandFactory, new CommandInitializer(_commandOptionInputConverter, new EnvironmentVariablesParser()), new HelpTextRenderer()); } } diff --git a/CliFx/ICliApplicationBuilder.cs b/CliFx/ICliApplicationBuilder.cs index 57ff389..374a96d 100644 --- a/CliFx/ICliApplicationBuilder.cs +++ b/CliFx/ICliApplicationBuilder.cs @@ -64,6 +64,11 @@ namespace CliFx /// ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter); + /// + /// Configures application to use specified implementation of . + /// + ICliApplicationBuilder UseEnvironmentVariablesProvider(IEnvironmentVariablesProvider environmentVariablesProvider); + /// /// Creates an instance of using configured parameters. /// Default values are used in place of parameters that were not specified. diff --git a/CliFx/Models/CommandInput.cs b/CliFx/Models/CommandInput.cs index 9945ebe..b06bd1a 100644 --- a/CliFx/Models/CommandInput.cs +++ b/CliFx/Models/CommandInput.cs @@ -25,14 +25,36 @@ namespace CliFx.Models /// public IReadOnlyList Options { get; } + /// + /// Environment variables available when the command was parsed + /// + public IReadOnlyDictionary EnvironmentVariables { get; } + /// /// Initializes an instance of . /// - public CommandInput(string commandName, IReadOnlyList directives, IReadOnlyList options) + public CommandInput(string commandName, IReadOnlyList directives, IReadOnlyList options, IReadOnlyDictionary environmentVariables) { CommandName = commandName; // can be null Directives = directives.GuardNotNull(nameof(directives)); Options = options.GuardNotNull(nameof(options)); + EnvironmentVariables = environmentVariables.GuardNotNull(nameof(environmentVariables)); + } + + /// + /// Initializes an instance of . + /// + public CommandInput(string commandName, IReadOnlyList directives, IReadOnlyList options) + : this(commandName, directives, options, EmptyEnvironmentVariables) + { + } + + /// + /// Initializes an instance of . + /// + public CommandInput(string commandName, IReadOnlyList options, IReadOnlyDictionary environmentVariables) + : this(commandName, EmptyDirectives, options, environmentVariables) + { } /// @@ -87,6 +109,7 @@ namespace CliFx.Models { private static readonly IReadOnlyList EmptyDirectives = new string[0]; private static readonly IReadOnlyList EmptyOptions = new CommandOptionInput[0]; + private static readonly IReadOnlyDictionary EmptyEnvironmentVariables = new Dictionary(); /// /// Empty input. diff --git a/CliFx/Models/CommandOptionSchema.cs b/CliFx/Models/CommandOptionSchema.cs index 7e30e9c..7e47e33 100644 --- a/CliFx/Models/CommandOptionSchema.cs +++ b/CliFx/Models/CommandOptionSchema.cs @@ -34,16 +34,22 @@ namespace CliFx.Models /// public string Description { get; } + /// + /// Optional environment variable name that will be used as fallback value if no option value is specified. + /// + public string EnvironmentVariableName { get; } + /// /// Initializes an instance of . /// - public CommandOptionSchema(PropertyInfo property, string name, char? shortName, bool isRequired, string description) + public CommandOptionSchema(PropertyInfo property, string name, char? shortName, bool isRequired, string description, string environmentVariableName) { Property = property; // can be null Name = name; // can be null ShortName = shortName; // can be null IsRequired = isRequired; Description = description; // can be null + EnvironmentVariableName = environmentVariableName; //can be null } /// @@ -75,9 +81,9 @@ namespace CliFx.Models // ...in CliApplication (when reading) and HelpTextRenderer (when writing). internal static CommandOptionSchema HelpOption { get; } = - new CommandOptionSchema(null, "help", 'h', false, "Shows help text."); + new CommandOptionSchema(null, "help", 'h', false, "Shows help text.", null); internal static CommandOptionSchema VersionOption { get; } = - new CommandOptionSchema(null, "version", null, false, "Shows version information."); + new CommandOptionSchema(null, "version", null, false, "Shows version information.", null); } } \ No newline at end of file diff --git a/CliFx/Models/Extensions.cs b/CliFx/Models/Extensions.cs index 2dc5e35..c9e0406 100644 --- a/CliFx/Models/Extensions.cs +++ b/CliFx/Models/Extensions.cs @@ -1,7 +1,7 @@ -using System; +using CliFx.Internal; +using System; using System.Collections.Generic; using System.Linq; -using CliFx.Internal; namespace CliFx.Models { @@ -71,16 +71,16 @@ namespace CliFx.Models return matchesByName || matchesByShortName; } - - /// - /// Finds an option that matches specified alias, or null if not found. - /// - public static CommandOptionSchema FindByAlias(this IReadOnlyList optionSchemas, string alias) - { - optionSchemas.GuardNotNull(nameof(optionSchemas)); - alias.GuardNotNull(nameof(alias)); - return optionSchemas.FirstOrDefault(o => o.MatchesAlias(alias)); + /// + /// Finds an option input that matches the option schema specified, or null if not found. + /// + public static CommandOptionInput FindByOptionSchema(this IReadOnlyList optionInputs, CommandOptionSchema optionSchema) + { + optionInputs.GuardNotNull(nameof(optionInputs)); + optionSchema.GuardNotNull(nameof(optionSchema)); + + return optionInputs.FirstOrDefault(o => optionSchema.MatchesAlias(o.Alias)); } /// diff --git a/CliFx/Services/CommandInitializer.cs b/CliFx/Services/CommandInitializer.cs index 7866604..046b071 100644 --- a/CliFx/Services/CommandInitializer.cs +++ b/CliFx/Services/CommandInitializer.cs @@ -11,20 +11,30 @@ namespace CliFx.Services public class CommandInitializer : ICommandInitializer { private readonly ICommandOptionInputConverter _commandOptionInputConverter; + private readonly IEnvironmentVariablesParser _environmentVariablesParser; /// /// Initializes an instance of . /// - public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter) + public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter, IEnvironmentVariablesParser environmentVariablesParser) { _commandOptionInputConverter = commandOptionInputConverter.GuardNotNull(nameof(commandOptionInputConverter)); + _environmentVariablesParser = environmentVariablesParser.GuardNotNull(nameof(environmentVariablesParser)); + } + + /// + /// Initializes an instance of . + /// + public CommandInitializer(IEnvironmentVariablesParser environmentVariablesParser) + : this(new CommandOptionInputConverter(), environmentVariablesParser) + { } /// /// Initializes an instance of . /// public CommandInitializer() - : this(new CommandOptionInputConverter()) + : this(new CommandOptionInputConverter(), new EnvironmentVariablesParser()) { } @@ -38,15 +48,28 @@ namespace CliFx.Services // Keep track of unset required options to report an error at a later stage var unsetRequiredOptions = commandSchema.Options.Where(o => o.IsRequired).ToList(); - // Set command options - foreach (var optionInput in commandInput.Options) + //Set command options + foreach (var optionSchema in commandSchema.Options) { - // Find matching option schema for this option input - var optionSchema = commandSchema.Options.FindByAlias(optionInput.Alias); - if (optionSchema == null) + //Find matching option input + var optionInput = commandInput.Options.FindByOptionSchema(optionSchema); + + //If no option input is available fall back to environment variable values + if (optionInput == null && !optionSchema.EnvironmentVariableName.IsNullOrWhiteSpace()) + { + var fallbackEnvironmentVariableExists = commandInput.EnvironmentVariables.ContainsKey(optionSchema.EnvironmentVariableName); + + //If no environment variable is found or there is no valid value for this option skip it + if (!fallbackEnvironmentVariableExists || commandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName].IsNullOrWhiteSpace()) + continue; + + optionInput = _environmentVariablesParser.GetCommandOptionInputFromEnvironmentVariable(commandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName], optionSchema); + } + + //No fallback available and no option input was specified, skip option + if (optionInput == null) continue; - // Convert option to the type of the underlying property var convertedValue = _commandOptionInputConverter.ConvertOptionInput(optionInput, optionSchema.Property.PropertyType); // Set value of the underlying property diff --git a/CliFx/Services/CommandInputParser.cs b/CliFx/Services/CommandInputParser.cs index 938f955..ac78cd4 100644 --- a/CliFx/Services/CommandInputParser.cs +++ b/CliFx/Services/CommandInputParser.cs @@ -12,6 +12,26 @@ namespace CliFx.Services /// public class CommandInputParser : ICommandInputParser { + private readonly IEnvironmentVariablesProvider _environmentVariablesProvider; + + /// + /// Initializes an instance of + /// + public CommandInputParser(IEnvironmentVariablesProvider environmentVariablesProvider) + { + environmentVariablesProvider.GuardNotNull(nameof(environmentVariablesProvider)); + + _environmentVariablesProvider = environmentVariablesProvider; + } + + /// + /// Initializes an instance of + /// + public CommandInputParser() + : this(new EnvironmentVariablesProvider()) + { + } + /// public CommandInput ParseCommandInput(IReadOnlyList commandLineArguments) { @@ -78,7 +98,9 @@ namespace CliFx.Services var commandName = commandNameBuilder.Length > 0 ? commandNameBuilder.ToString() : null; var options = optionsDic.Select(p => new CommandOptionInput(p.Key, p.Value)).ToArray(); - return new CommandInput(commandName, directives, options); + var environmentVariables = _environmentVariablesProvider.GetEnvironmentVariables(); + + return new CommandInput(commandName, directives, options, environmentVariables); } } } \ No newline at end of file diff --git a/CliFx/Services/CommandSchemaResolver.cs b/CliFx/Services/CommandSchemaResolver.cs index fe61790..7820980 100644 --- a/CliFx/Services/CommandSchemaResolver.cs +++ b/CliFx/Services/CommandSchemaResolver.cs @@ -31,7 +31,8 @@ namespace CliFx.Services attribute.Name, attribute.ShortName, attribute.IsRequired, - attribute.Description); + attribute.Description, + attribute.EnvironmentVariableName); // Make sure there are no other options with the same name var existingOptionWithSameName = result diff --git a/CliFx/Services/EnvironmentVariablesParser.cs b/CliFx/Services/EnvironmentVariablesParser.cs new file mode 100644 index 0000000..23aad29 --- /dev/null +++ b/CliFx/Services/EnvironmentVariablesParser.cs @@ -0,0 +1,30 @@ +using System.IO; +using System.Linq; +using CliFx.Internal; +using CliFx.Models; + +namespace CliFx.Services +{ + /// + public class EnvironmentVariablesParser : IEnvironmentVariablesParser + { + /// + public CommandOptionInput GetCommandOptionInputFromEnvironmentVariable(string environmentVariableValue, CommandOptionSchema targetOptionSchema) + { + environmentVariableValue.GuardNotNull(nameof(environmentVariableValue)); + targetOptionSchema.GuardNotNull(nameof(targetOptionSchema)); + + //If the option is not a collection do not split environment variable values + var optionIsCollection = targetOptionSchema.Property.PropertyType.IsCollection(); + + if (!optionIsCollection) return new CommandOptionInput(targetOptionSchema.EnvironmentVariableName, environmentVariableValue); + + //If the option is a collection split the values using System separator, empty values are discarded + var environmentVariableValues = environmentVariableValue.Split(Path.PathSeparator) + .Where(v => !v.IsNullOrWhiteSpace()) + .ToList(); + + return new CommandOptionInput(targetOptionSchema.EnvironmentVariableName, environmentVariableValues); + } + } +} diff --git a/CliFx/Services/EnvironmentVariablesProvider.cs b/CliFx/Services/EnvironmentVariablesProvider.cs new file mode 100644 index 0000000..95481fc --- /dev/null +++ b/CliFx/Services/EnvironmentVariablesProvider.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Security; + +namespace CliFx.Services +{ + /// + public class EnvironmentVariablesProvider : IEnvironmentVariablesProvider + { + /// + public IReadOnlyDictionary GetEnvironmentVariables() + { + try + { + var environmentVariables = Environment.GetEnvironmentVariables(); + + //Constructing the dictionary manually allows to specify a key comparer that ignores case + //This allows to ignore casing when looking for a fallback environment variable of an option + var environmentVariablesAsDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + //Type DictionaryEntry must be explicitly used otherwise it will enumerate as a collection of objects + foreach (DictionaryEntry environmentVariable in environmentVariables) + { + environmentVariablesAsDictionary.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString()); + } + + return environmentVariablesAsDictionary; + } + catch (SecurityException) + { + return new Dictionary(); + } + } + } +} diff --git a/CliFx/Services/Extensions.cs b/CliFx/Services/Extensions.cs index 078b538..4828dd5 100644 --- a/CliFx/Services/Extensions.cs +++ b/CliFx/Services/Extensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using CliFx.Internal; namespace CliFx.Services @@ -50,5 +51,25 @@ namespace CliFx.Services console.WithForegroundColor(foregroundColor, () => console.WithBackgroundColor(backgroundColor, action)); } + + /// + /// Gets wether a string representing an environment variable value is escaped (i.e.: surrounded by double quotation marks) + /// + public static bool IsEnvironmentVariableEscaped(this string environmentVariableValue) + { + environmentVariableValue.GuardNotNull(nameof(environmentVariableValue)); + + return environmentVariableValue.StartsWith("\"") && environmentVariableValue.EndsWith("\""); + } + + /// + /// Gets wether the supplied is a collection implementing + /// + public static bool IsCollection(this Type type) + { + type.GuardNotNull(nameof(type)); + + return type != typeof(string) && type.GetEnumerableUnderlyingType() != null; + } } } \ No newline at end of file diff --git a/CliFx/Services/IEnvironmentVariablesParser.cs b/CliFx/Services/IEnvironmentVariablesParser.cs new file mode 100644 index 0000000..cf13b06 --- /dev/null +++ b/CliFx/Services/IEnvironmentVariablesParser.cs @@ -0,0 +1,15 @@ +using CliFx.Models; + +namespace CliFx.Services +{ + /// + /// Parses environment variable values + /// + public interface IEnvironmentVariablesParser + { + /// + /// Parse an environment variable value and converts it to a + /// + CommandOptionInput GetCommandOptionInputFromEnvironmentVariable(string environmentVariableValue, CommandOptionSchema targetOptionSchema); + } +} diff --git a/CliFx/Services/IEnvironmentVariablesProvider.cs b/CliFx/Services/IEnvironmentVariablesProvider.cs new file mode 100644 index 0000000..0eddd05 --- /dev/null +++ b/CliFx/Services/IEnvironmentVariablesProvider.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace CliFx.Services +{ + /// + /// Provides environment variable values + /// + public interface IEnvironmentVariablesProvider + { + /// + /// Returns all the environment variables available. + /// + /// If the User is not allowed to read environment variables it will return an empty dictionary. + IReadOnlyDictionary GetEnvironmentVariables(); + } +}