diff --git a/source/Nuke.Core.Tests/EnumerableExtensionsTest.cs b/source/Nuke.Core.Tests/EnumerableExtensionsTest.cs new file mode 100644 index 000000000..079431a08 --- /dev/null +++ b/source/Nuke.Core.Tests/EnumerableExtensionsTest.cs @@ -0,0 +1,31 @@ +// Copyright Matthias Koch, Sebastian Karasek 2018. +// Distributed under the MIT License. +// https://github.com/nuke-build/nuke/blob/master/LICENSE + +using System; +using System.Linq; +using FluentAssertions; +using Nuke.Core.Utilities.Collections; +using Xunit; + +namespace Nuke.Core.Tests +{ + public class EnumerableExtensionsTest + { + [Fact] + public void SingleOrDefaultOrError_ThrowsExceptionWithMessage() + { + var x = new[] { "a", "a" }; + Action a = () => x.SingleOrDefaultOrError("error"); + a.Should().Throw().WithMessage("error"); + } + + [Theory] + [InlineData(new[] { "a", "b", "c" }, "a", "a")] + [InlineData(new[] { "a", "b", "c" }, "d", null)] + public void SingleOrDefaultOrError(string[] enumerable, string equalsWith, string expectedValue) + { + enumerable.SingleOrDefaultOrError(x => x.Equals(equalsWith), "error").Should().Be(expectedValue); + } + } +} diff --git a/source/Nuke.Core.Tests/Nuke.Core.Tests.csproj b/source/Nuke.Core.Tests/Nuke.Core.Tests.csproj index f6c63ae41..4dd091b3c 100644 --- a/source/Nuke.Core.Tests/Nuke.Core.Tests.csproj +++ b/source/Nuke.Core.Tests/Nuke.Core.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/source/Nuke.Core.Tests/ParameterServiceTest.cs b/source/Nuke.Core.Tests/ParameterServiceTest.cs index 7813ad315..ebfa356f8 100644 --- a/source/Nuke.Core.Tests/ParameterServiceTest.cs +++ b/source/Nuke.Core.Tests/ParameterServiceTest.cs @@ -121,5 +121,16 @@ public void TestConversionCollections() service.GetParameter("files").Should().BeEquivalentTo("C:\\new folder\\file.txt", "C:\\file.txt"); service.GetParameter("values", separator: '+').Should().BeEquivalentTo("A", "B", "C"); } + + [Theory] + [InlineData(new[] { "arg1" }, 0, typeof(string), "arg1")] + [InlineData(new[] { "true" }, 0, typeof(bool), true)] + [InlineData(new[] { "arg1" }, 1, typeof(string), null)] + [InlineData(new[] { "arg1", "arg2" }, 1, typeof(string), "arg2")] + public void TestPositionalCommandLineArguments(string[] commandLineArgs, int position, Type destinationType, object expectedValue) + { + var service = GetService(commandLineArgs); + service.GetCommandLineArgument(position, destinationType).Should().Be(expectedValue); + } } } diff --git a/source/Nuke.Core/EnvironmentInfo.Internal.cs b/source/Nuke.Core/EnvironmentInfo.Internal.cs index 47dbec07f..2e03db1cf 100644 --- a/source/Nuke.Core/EnvironmentInfo.Internal.cs +++ b/source/Nuke.Core/EnvironmentInfo.Internal.cs @@ -40,9 +40,9 @@ internal static string[] InvokedTargets { get { - var argument = Environment.GetCommandLineArgs() - .Skip(count: 1).Take(count: 1) - .SingleOrDefault(x => !x.StartsWith("-")); + var argument = ParameterService.Instance.GetCommandLineArgument(position: 1); + argument = argument == null || argument.StartsWith("-") ? null : argument; + if (argument != null) return argument.Split(new[] { '+' }, StringSplitOptions.RemoveEmptyEntries); diff --git a/source/Nuke.Core/EnvironmentInfo.Parameters.cs b/source/Nuke.Core/EnvironmentInfo.Parameters.cs index ef3caa2ba..746b95d12 100644 --- a/source/Nuke.Core/EnvironmentInfo.Parameters.cs +++ b/source/Nuke.Core/EnvironmentInfo.Parameters.cs @@ -17,8 +17,6 @@ namespace Nuke.Core [DebuggerStepThrough] public static partial class EnvironmentInfo { - private static readonly ParameterService s_parameterService = new ParameterService(); - public static void SetVariable(string name, string value) { Environment.SetEnvironmentVariable(name, value); @@ -36,7 +34,7 @@ public static void SetVariable(string name, IEnumerable values, char separ /// public static bool ParameterSwitch(string name) { - return s_parameterService.GetParameter(name); + return ParameterService.Instance.GetParameter(name); } /// @@ -45,7 +43,7 @@ public static bool ParameterSwitch(string name) [CanBeNull] public static string Parameter(string name) { - return s_parameterService.GetParameter(name); + return ParameterService.Instance.GetParameter(name); } /// @@ -54,7 +52,7 @@ public static string Parameter(string name) [CanBeNull] public static T Parameter(string name) { - return s_parameterService.GetParameter(name); + return ParameterService.Instance.GetParameter(name); } /// @@ -63,7 +61,7 @@ public static T Parameter(string name) [CanBeNull] public static T[] ParameterSet(string name, char? separator = null) { - return s_parameterService.GetParameter(name, separator); + return ParameterService.Instance.GetParameter(name, separator); } /// @@ -99,7 +97,7 @@ public static T[] EnsureParameterSet(string name, char? separator = null) /// public static bool VariableSwitch(string name) { - return s_parameterService.GetEnvironmentVariable(name); + return ParameterService.Instance.GetEnvironmentVariable(name); } /// @@ -108,7 +106,7 @@ public static bool VariableSwitch(string name) [CanBeNull] public static string Variable(string name) { - return s_parameterService.GetEnvironmentVariable(name); + return ParameterService.Instance.GetEnvironmentVariable(name); } /// @@ -117,7 +115,7 @@ public static string Variable(string name) [CanBeNull] public static T Variable(string name) { - return s_parameterService.GetEnvironmentVariable(name); + return ParameterService.Instance.GetEnvironmentVariable(name); } /// @@ -126,7 +124,7 @@ public static T Variable(string name) [CanBeNull] public static T[] VariableSet(string name, char? separator = null) { - return s_parameterService.GetEnvironmentVariable(name, separator); + return ParameterService.Instance.GetEnvironmentVariable(name, separator); } /// @@ -162,7 +160,7 @@ public static T[] EnsureVariableSet(string name, char? separator = null) /// public static bool ArgumentSwitch(string name) { - return s_parameterService.GetCommandLineArgument(name); + return ParameterService.Instance.GetCommandLineArgument(name); } /// @@ -171,7 +169,7 @@ public static bool ArgumentSwitch(string name) [CanBeNull] public static string Argument(string name) { - return s_parameterService.GetCommandLineArgument(name); + return ParameterService.Instance.GetCommandLineArgument(name); } /// @@ -180,7 +178,7 @@ public static string Argument(string name) [CanBeNull] public static T Argument(string name) { - return s_parameterService.GetCommandLineArgument(name); + return ParameterService.Instance.GetCommandLineArgument(name); } /// @@ -189,7 +187,7 @@ public static T Argument(string name) [CanBeNull] public static T[] ArgumentSet(string name, char? separator = null) { - return s_parameterService.GetCommandLineArgument(name, separator); + return ParameterService.Instance.GetCommandLineArgument(name, separator); } /// diff --git a/source/Nuke.Core/Execution/NukeTempfileArgumentProvider.cs b/source/Nuke.Core/Execution/NukeTempfileArgumentProvider.cs new file mode 100644 index 000000000..2ab017ab4 --- /dev/null +++ b/source/Nuke.Core/Execution/NukeTempfileArgumentProvider.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using JetBrains.Annotations; +using Nuke.Core.Utilities.Collections; + +namespace Nuke.Core.Execution +{ + public static class NukeTempfileArgumentProvider + { + [CanBeNull] private static string[] s_arguments; + [CanBeNull] private static readonly FileInfo s_tempConfigFile; + + static NukeTempfileArgumentProvider() + { + try + { + s_tempConfigFile = new FileInfo(EnvironmentInfo.BuildDirectory / "nuke.tmp"); + } + catch (Exception) + { + s_tempConfigFile = null; + } + } + + public static bool IsAvailable + { + get + { + if (s_tempConfigFile == null || !s_tempConfigFile.Exists) return false; + if (s_tempConfigFile.LastWriteTime.AddMinutes(value: 1) >= DateTime.Now) return true; + + s_tempConfigFile.Delete(); + return false; + } + } + + public static string[] Execute() + { + if (s_arguments != null) return s_arguments; + + var args = new List { nameof(NukeTempfileArgumentProvider) }; + args.AddRange(ReadAndDeleteTempFile().Split(' ')); + return s_arguments = args.ToArray(); + } + + private static string ReadAndDeleteTempFile() + { + ControlFlow.Assert(IsAvailable, "nuke.tmp file was deleted"); + + var lines = File.ReadAllLines(s_tempConfigFile.NotNull().FullName); + + s_tempConfigFile.Delete(); + + var configLine = lines.ToList() + .SingleOrDefaultOrError(x => !string.IsNullOrWhiteSpace(x), "nuke.tmp must not contain more than one line."); + ControlFlow.Assert(configLine != null, "No configuration values found"); + return configLine; + } + } +} diff --git a/source/Nuke.Core/Execution/ParameterService.cs b/source/Nuke.Core/Execution/ParameterService.cs index 2ef9b45bc..899355fe7 100644 --- a/source/Nuke.Core/Execution/ParameterService.cs +++ b/source/Nuke.Core/Execution/ParameterService.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.IO; using System.Linq; using JetBrains.Annotations; using Nuke.Core.Utilities; @@ -15,6 +16,8 @@ namespace Nuke.Core.Execution { public class ParameterService { + private static ParameterService s_instance; + private readonly Func _commandLineArgumentsProvider; private readonly Func _environmentVariablesProvider; @@ -25,6 +28,18 @@ public ParameterService(Func commandLineArgumentsProvider = null, Func _commandLineArgumentsProvider = commandLineArgumentsProvider ?? Environment.GetCommandLineArgs; } + public static ParameterService Instance + { + get + { + if (s_instance != null) return s_instance; + Func commandLineArgumentsProvider = null; + if (NukeTempfileArgumentProvider.IsAvailable) commandLineArgumentsProvider = NukeTempfileArgumentProvider.Execute; + + return s_instance = new ParameterService(commandLineArgumentsProvider); + } + } + [CanBeNull] public T GetParameter(string parameterName, char? separator = null) { @@ -37,6 +52,12 @@ public T GetCommandLineArgument(string parameterName, char? separator = null) return (T) GetCommandLineArgument(parameterName, typeof(T), separator); } + [CanBeNull] + public T GetCommandLineArgument(int position, char? separator = null) + { + return (T) GetCommandLineArgument(position, typeof(T), separator); + } + [CanBeNull] public T GetEnvironmentVariable(string parameterName, char? separator = null) { @@ -60,6 +81,25 @@ public object GetCommandLineArgument(string argumentName, Type destinationType, return GetDefaultValue(destinationType); var values = args.Skip(index + 1).TakeWhile(x => !x.StartsWith("-")).ToArray(); + return ConvertCommandLineArguments(argumentName, values, destinationType, args, separator); + } + + [CanBeNull] + public object GetCommandLineArgument(int position, Type destinationType, char? separator = null) + { + var args = _commandLineArgumentsProvider.Invoke(); + if (args.Length <= position) return null; + return ConvertCommandLineArguments($"positional[{position}]", new[] { args[position] }, destinationType, args, separator); + } + + [CanBeNull] + public object ConvertCommandLineArguments( + string argumentName, + string[] values, + Type destinationType, + string[] commandLineArguments, + char? separator = null) + { ControlFlow.Assert(values.Length == 1 || !separator.HasValue || values.All(x => !x.Contains(separator.Value)), $"Command-line argument '{argumentName}' with value [ {values.JoinComma()} ] cannot be split with separator '{separator}'."); values = separator.HasValue && values.Any(x => x.Contains(separator.Value)) @@ -74,7 +114,7 @@ public object GetCommandLineArgument(string argumentName, Type destinationType, { ControlFlow.Fail( new[] { ex.Message, "Command-line arguments were:" } - .Concat(args.Select((x, i) => $" [{i}] = {x}")) + .Concat(commandLineArguments.Select((x, i) => $" [{i}] = {x}")) .JoinNewLine()); // ReSharper disable once HeuristicUnreachableCode return null; diff --git a/source/Nuke.Core/ParameterAttribute.cs b/source/Nuke.Core/ParameterAttribute.cs index ff7c28529..5f97fd0dc 100644 --- a/source/Nuke.Core/ParameterAttribute.cs +++ b/source/Nuke.Core/ParameterAttribute.cs @@ -35,8 +35,6 @@ namespace Nuke.Core [UsedImplicitly(ImplicitUseKindFlags.Assign)] public class ParameterAttribute : InjectionAttributeBase { - private static readonly ParameterService s_parameterService = new ParameterService(); - public ParameterAttribute(string description = null) { Description = description; @@ -56,7 +54,7 @@ public override object GetValue(string memberName, Type memberType) !memberType.IsArray ? typeof(Nullable<>).MakeGenericType(memberType) : memberType; - return s_parameterService.GetParameter(Name ?? memberName, memberType, (Separator ?? string.Empty).SingleOrDefault()); + return ParameterService.Instance.GetParameter(Name ?? memberName, memberType, (Separator ?? string.Empty).SingleOrDefault()); } } } diff --git a/source/Nuke.Core/Utilities/Collections/Enumerable.SingleOrDefaultOrError.cs b/source/Nuke.Core/Utilities/Collections/Enumerable.SingleOrDefaultOrError.cs new file mode 100644 index 000000000..a7f7a25df --- /dev/null +++ b/source/Nuke.Core/Utilities/Collections/Enumerable.SingleOrDefaultOrError.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Nuke.Core.Utilities.Collections +{ + + public static partial class EnumerableExtensions + { + public static T SingleOrDefaultOrError(this IEnumerable enumerable, Func predicate, string message) + { + if (enumerable == null) throw new ArgumentNullException(nameof(enumerable)); + if (predicate == null) throw new ArgumentNullException(nameof(predicate)); + if (string.IsNullOrEmpty(message)) throw new ArgumentException("message must not be null or empty", nameof(message)); + + + try + { + return enumerable.SingleOrDefault(predicate); + } + catch (InvalidOperationException ex) + { + throw new InvalidOperationException(message, ex); + } + } + + public static T SingleOrDefaultOrError(this IEnumerable enumerable, string message) + { + if (enumerable == null) throw new ArgumentNullException(nameof(enumerable)); + if (string.IsNullOrEmpty(message)) throw new ArgumentException("message must not be null or empty", nameof(message)); + + try + { + return enumerable.SingleOrDefault(); + } + catch (InvalidOperationException ex) + { + throw new InvalidOperationException(message, ex); + } + + } + } +}