From f90766573d8695efdaf9656b2402eaa736dad4bf Mon Sep 17 00:00:00 2001 From: Mogens Heller Grabe Date: Fri, 7 Aug 2020 09:43:24 +0200 Subject: [PATCH] porting... --- {Beverage => Beverage_old}/App.config | 0 {Beverage => Beverage_old}/Beverage.csproj | 6 - .../Commands/BlackRussian.cs | 0 .../Commands/Martini.cs | 0 .../Commands/WhiteRussian.cs | 0 {Beverage => Beverage_old}/Program.cs | 0 .../Properties/AssemblyInfo.cs | 0 GoCommando.Tests/GoCommando.Tests.csproj | 73 +-- GoCommando.Tests_old/GoCommando.Tests.csproj | 68 +++ .../Properties/AssemblyInfo.cs | 0 GoCommando.Tests_old/TestArgParser.cs | 91 ++++ GoCommando.Tests_old/TestCommand.cs | 158 ++++++ .../packages.config | 0 GoCommando.sln | 50 +- GoCommando/Go.cs | 23 +- GoCommando/GoCommando.csproj | 79 +-- GoCommando/Internals/InternalsVisibleTo.cs | 3 + GoCommando_old/BannerAttribute.cs | 24 + GoCommando_old/CommandAttribute.cs | 31 ++ GoCommando_old/DescriptionAttribute.cs | 25 + GoCommando_old/ExampleAttribute.cs | 24 + GoCommando_old/ExitCodeException.cs | 32 ++ GoCommando_old/Go.cs | 507 ++++++++++++++++++ GoCommando_old/GoCommando.csproj | 76 +++ GoCommando_old/GoCommandoException.cs | 27 + GoCommando_old/ICommand.cs | 13 + GoCommando_old/ICommandFactory.cs | 21 + GoCommando_old/Internals/Arguments.cs | 64 +++ GoCommando_old/Internals/CommandInvoker.cs | 298 ++++++++++ .../Internals/EnvironmentSettings.cs | 73 +++ GoCommando_old/Internals/Parameter.cs | 103 ++++ GoCommando_old/Internals/Settings.cs | 12 + GoCommando_old/Internals/StringExtensions.cs | 68 +++ GoCommando_old/Internals/Switch.cs | 55 ++ GoCommando_old/ParameterAttribute.cs | 79 +++ .../Properties/AssemblyInfo.cs | 0 .../SupportImpersonationAttribute.cs | 12 + {TestApp2 => TestApp2_old}/App.config | 0 {TestApp2 => TestApp2_old}/ElCommandante.cs | 0 {TestApp2 => TestApp2_old}/Program.cs | 0 .../Properties/AssemblyInfo.cs | 0 {TestApp2 => TestApp2_old}/TestApp2.csproj | 6 - {TestApp => TestApp_old}/App.config | 0 .../Commands/NewCommand.cs | 0 .../Commands/RunCommand.cs | 0 .../Commands/StopCommand.cs | 0 .../Commands/TestAppConfigBinding.cs | 0 {TestApp => TestApp_old}/Program.cs | 0 .../Properties/AssemblyInfo.cs | 0 {TestApp => TestApp_old}/TestApp.csproj | 6 - 50 files changed, 1906 insertions(+), 201 deletions(-) rename {Beverage => Beverage_old}/App.config (100%) rename {Beverage => Beverage_old}/Beverage.csproj (92%) rename {Beverage => Beverage_old}/Commands/BlackRussian.cs (100%) rename {Beverage => Beverage_old}/Commands/Martini.cs (100%) rename {Beverage => Beverage_old}/Commands/WhiteRussian.cs (100%) rename {Beverage => Beverage_old}/Program.cs (100%) rename {Beverage => Beverage_old}/Properties/AssemblyInfo.cs (100%) create mode 100644 GoCommando.Tests_old/GoCommando.Tests.csproj rename {GoCommando.Tests => GoCommando.Tests_old}/Properties/AssemblyInfo.cs (100%) create mode 100644 GoCommando.Tests_old/TestArgParser.cs create mode 100644 GoCommando.Tests_old/TestCommand.cs rename {GoCommando.Tests => GoCommando.Tests_old}/packages.config (100%) create mode 100644 GoCommando/Internals/InternalsVisibleTo.cs create mode 100644 GoCommando_old/BannerAttribute.cs create mode 100644 GoCommando_old/CommandAttribute.cs create mode 100644 GoCommando_old/DescriptionAttribute.cs create mode 100644 GoCommando_old/ExampleAttribute.cs create mode 100644 GoCommando_old/ExitCodeException.cs create mode 100644 GoCommando_old/Go.cs create mode 100644 GoCommando_old/GoCommando.csproj create mode 100644 GoCommando_old/GoCommandoException.cs create mode 100644 GoCommando_old/ICommand.cs create mode 100644 GoCommando_old/ICommandFactory.cs create mode 100644 GoCommando_old/Internals/Arguments.cs create mode 100644 GoCommando_old/Internals/CommandInvoker.cs create mode 100644 GoCommando_old/Internals/EnvironmentSettings.cs create mode 100644 GoCommando_old/Internals/Parameter.cs create mode 100644 GoCommando_old/Internals/Settings.cs create mode 100644 GoCommando_old/Internals/StringExtensions.cs create mode 100644 GoCommando_old/Internals/Switch.cs create mode 100644 GoCommando_old/ParameterAttribute.cs rename {GoCommando => GoCommando_old}/Properties/AssemblyInfo.cs (100%) create mode 100644 GoCommando_old/SupportImpersonationAttribute.cs rename {TestApp2 => TestApp2_old}/App.config (100%) rename {TestApp2 => TestApp2_old}/ElCommandante.cs (100%) rename {TestApp2 => TestApp2_old}/Program.cs (100%) rename {TestApp2 => TestApp2_old}/Properties/AssemblyInfo.cs (100%) rename {TestApp2 => TestApp2_old}/TestApp2.csproj (92%) rename {TestApp => TestApp_old}/App.config (100%) rename {TestApp => TestApp_old}/Commands/NewCommand.cs (100%) rename {TestApp => TestApp_old}/Commands/RunCommand.cs (100%) rename {TestApp => TestApp_old}/Commands/StopCommand.cs (100%) rename {TestApp => TestApp_old}/Commands/TestAppConfigBinding.cs (100%) rename {TestApp => TestApp_old}/Program.cs (100%) rename {TestApp => TestApp_old}/Properties/AssemblyInfo.cs (100%) rename {TestApp => TestApp_old}/TestApp.csproj (93%) diff --git a/Beverage/App.config b/Beverage_old/App.config similarity index 100% rename from Beverage/App.config rename to Beverage_old/App.config diff --git a/Beverage/Beverage.csproj b/Beverage_old/Beverage.csproj similarity index 92% rename from Beverage/Beverage.csproj rename to Beverage_old/Beverage.csproj index 8ce0968..b7214ad 100644 --- a/Beverage/Beverage.csproj +++ b/Beverage_old/Beverage.csproj @@ -52,12 +52,6 @@ - - - {2839AA55-B40A-4BB8-BDA0-C5057E5A683F} - GoCommando - - - \ No newline at end of file + + diff --git a/GoCommando.Tests_old/GoCommando.Tests.csproj b/GoCommando.Tests_old/GoCommando.Tests.csproj new file mode 100644 index 0000000..011b1e5 --- /dev/null +++ b/GoCommando.Tests_old/GoCommando.Tests.csproj @@ -0,0 +1,68 @@ + + + + + Debug + AnyCPU + {5B0BCF64-81B6-4A33-8D00-C673A0EF3551} + Library + Properties + GoCommando.Tests + GoCommando.Tests + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\NUnit.2.6.4\lib\nunit.framework.dll + True + + + + + + + + + + + + + + + + + + + + + {2839AA55-B40A-4BB8-BDA0-C5057E5A683F} + GoCommando + + + + + \ No newline at end of file diff --git a/GoCommando.Tests/Properties/AssemblyInfo.cs b/GoCommando.Tests_old/Properties/AssemblyInfo.cs similarity index 100% rename from GoCommando.Tests/Properties/AssemblyInfo.cs rename to GoCommando.Tests_old/Properties/AssemblyInfo.cs diff --git a/GoCommando.Tests_old/TestArgParser.cs b/GoCommando.Tests_old/TestArgParser.cs new file mode 100644 index 0000000..a334c81 --- /dev/null +++ b/GoCommando.Tests_old/TestArgParser.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GoCommando.Internals; +using NUnit.Framework; + +namespace GoCommando.Tests +{ + [TestFixture] + public class TestArgParser + { + [Test] + public void CanReturnSimpleCommand() + { + var arguments = Parse(new[] { "run" }); + + Assert.That(arguments.Command, Is.EqualTo("run")); + } + + [Test] + public void CommandIsNullWhenNoCommandIsGiven() + { + var arguments = Parse(new[] { "-file", @"""C:\temp\file.json""" }); + + Assert.That(arguments.Command, Is.Null); + } + + [Test, Ignore("arguments.Command should just be null")] + public void DoesNotAcceptSwitchAsCommand() + { + var ex = Assert.Throws(() => + { + Parse(new[] { "-file", @"""C:\temp\file.json""" }); + }); + + Console.WriteLine(ex); + } + + [Test] + public void CanParseOrdinaryArguments() + { + var args = @"run +-path +c:\Program Files +-dir +c:\Windows\Microsoft.NET\Framework +-flag +-moreflag".Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + + var arguments = Parse(args); + + Console.WriteLine(arguments); + + Assert.That(arguments.Command, Is.EqualTo("run")); + Assert.That(arguments.Switches.Count(), Is.EqualTo(4)); + Assert.That(arguments.Get("path"), Is.EqualTo(@"c:\Program Files")); + Assert.That(arguments.Get("dir"), Is.EqualTo(@"c:\Windows\Microsoft.NET\Framework")); + + Assert.That(arguments.Get("flag"), Is.True); + Assert.That(arguments.Get("moreflag"), Is.True); + Assert.That(arguments.Get("flag_not_specified_should_default_to_false"), Is.False); + } + + [TestCase(@"-path:""c:\temp""")] + [TestCase(@"-path=""c:\temp""")] + [TestCase(@"-path""c:\temp""")] + public void SupportsVariousSingleTokenAliases(string alias) + { + var arguments = Parse(new[] { alias }); + + Assert.That(arguments.Switches.Count(), Is.EqualTo(1)); + Assert.That(arguments.Switches.Single().Key, Is.EqualTo("path")); + Assert.That(arguments.Switches.Single().Value, Is.EqualTo(@"c:\temp")); + } + + [TestCase(@"-n23")] + public void SupportsShortFormWithNumber(string alias) + { + var arguments = Parse(new[] { alias }); + + Assert.That(arguments.Switches.Count(), Is.EqualTo(1)); + Assert.That(arguments.Switches.Single().Key, Is.EqualTo("n")); + Assert.That(arguments.Switches.Single().Value, Is.EqualTo(@"23")); + } + + static Arguments Parse(IEnumerable args) + { + return Go.Parse(args, new Settings()); + } + } +} diff --git a/GoCommando.Tests_old/TestCommand.cs b/GoCommando.Tests_old/TestCommand.cs new file mode 100644 index 0000000..6f3c3ab --- /dev/null +++ b/GoCommando.Tests_old/TestCommand.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GoCommando.Internals; +using NUnit.Framework; + +namespace GoCommando.Tests +{ + [TestFixture] + public class TestCommand + { + [TestCase("-switch:value2")] + [TestCase("-switch=value2")] + [TestCase(@"-switch""value2""")] + public void CanCorrectlyHandleDifferentAlternativeSwitchFormatsFoundInOneSingleTokenOnly(string switchText) + { + var settings = new Settings(); + var invoker = new CommandInvoker("bimse", settings, new Bimse()); + var arguments = Go.Parse(new[] { switchText }, settings); + + invoker.Invoke(arguments.Switches, EnvironmentSettings.Empty); + + var bimseInstance = (Bimse)invoker.CommandInstance; + + Assert.That(bimseInstance.Switch, Is.EqualTo("value2")); + } + + [TestCase("-s:value2")] + [TestCase("-s=value2")] + [TestCase(@"-s""value2""")] + public void CanCorrectlyHandleDifferentAlternativeSwitchFormatsFoundInOneSingleTokenOnly_Shortname(string switchText) + { + var settings = new Settings(); + var invoker = new CommandInvoker("bimse", settings, new Bimse()); + var arguments = Go.Parse(new[] { switchText }, settings); + + invoker.Invoke(arguments.Switches, EnvironmentSettings.Empty); + + var bimseInstance = (Bimse)invoker.CommandInstance; + + Assert.That(bimseInstance.Switch, Is.EqualTo("value2")); + } + + [Command("bimse")] + class Bimse : ICommand + { + [Parameter("switch", shortName: "s")] + public string Switch { get; set; } + + public void Run() + { + } + } + + [Test] + public void CanUseSuppliedCommandFactory() + { + var commandFactory = new CustomFactory(); + + var commandInvoker = new CommandInvoker("null", typeof(CreatedByFactory), new Settings(), commandFactory: commandFactory); + + commandInvoker.Invoke(Enumerable.Empty(), new EnvironmentSettings()); + + Assert.That(commandInvoker.CommandInstance, Is.TypeOf()); + + var createdByFactory = (CreatedByFactory)commandInvoker.CommandInstance; + Assert.That(createdByFactory.CtorInjectedValue, Is.EqualTo("ctor!!")); + + Assert.That(commandFactory.WasProperlyReleased, Is.True, "The created command instance was NOT properly released after use!"); + } + + class CustomFactory : ICommandFactory + { + CreatedByFactory _instance; + + public bool WasProperlyReleased { get; set; } + + public ICommand Create(Type type) + { + if (type == typeof(CreatedByFactory)) + { + _instance = new CreatedByFactory("ctor!!"); + return _instance; + } + + throw new ArgumentException($"Unknown command type: {type}"); + } + + public void Release(ICommand command) + { + if (_instance == command) + { + WasProperlyReleased = true; + } + } + } + + class CreatedByFactory : ICommand + { + public string CtorInjectedValue { get; } + + public CreatedByFactory(string ctorInjectedValue) + { + CtorInjectedValue = ctorInjectedValue; + } + + public void Run() + { + } + } + + [Test] + public void CanGetParameterFromAppSettingsAndConnectionStrings() + { + var invoker = new CommandInvoker("null", typeof(CanUseAppSetting), new Settings()); + + var appSettings = new Dictionary + { + {"my-setting", "my-value"} + }; + + var connectionStrings = new Dictionary + { + {"my-conn", "my-value"} + }; + + var environmentVariables = new Dictionary + { + {"my-env", "my-value"} + }; + + invoker.Invoke(Enumerable.Empty(), new EnvironmentSettings(appSettings, connectionStrings, environmentVariables)); + + var instance = (CanUseAppSetting)invoker.CommandInstance; + + Assert.That(instance.AppSetting, Is.EqualTo("my-value")); + Assert.That(instance.ConnectionString, Is.EqualTo("my-value")); + Assert.That(instance.EnvironmentVariable, Is.EqualTo("my-value")); + } + + class CanUseAppSetting : ICommand + { + [Parameter("my-setting", allowAppSetting: true)] + public string AppSetting { get; set; } + + [Parameter("my-conn", allowConnectionString: true)] + public string ConnectionString { get; set; } + + [Parameter("my-env", allowEnvironmentVariable: true)] + public string EnvironmentVariable { get; set; } + + public void Run() + { + + } + } + } +} \ No newline at end of file diff --git a/GoCommando.Tests/packages.config b/GoCommando.Tests_old/packages.config similarity index 100% rename from GoCommando.Tests/packages.config rename to GoCommando.Tests_old/packages.config diff --git a/GoCommando.sln b/GoCommando.sln index 46def31..030255c 100644 --- a/GoCommando.sln +++ b/GoCommando.sln @@ -1,24 +1,18 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30330.147 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoCommando", "GoCommando\GoCommando.csproj", "{2839AA55-B40A-4BB8-BDA0-C5057E5A683F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "TestApp\TestApp.csproj", "{1DC7365E-E01D-477A-82F7-2BC5EFC51640}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoCommando.Tests", "GoCommando.Tests\GoCommando.Tests.csproj", "{5B0BCF64-81B6-4A33-8D00-C673A0EF3551}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "stuff", "stuff", "{4777EF91-8FBF-42DC-A31F-2025CC5841F8}" ProjectSection(SolutionItems) = preProject Package.nuspec = Package.nuspec README.md = README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Beverage", "Beverage\Beverage.csproj", "{324FC1A5-749A-44BF-BDB7-EB093F5DD88F}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{94F2785A-49E5-447F-BAFF-BE8F783EC71F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp2", "TestApp2\TestApp2.csproj", "{07010B53-B1CB-4696-8BB4-DCA2943B6B59}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoCommando", "GoCommando\GoCommando.csproj", "{91A9A887-0298-4B72-9D24-C2438F469192}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoCommando.Tests", "GoCommando.Tests\GoCommando.Tests.csproj", "{FBD7AE5E-C842-448B-9A6B-81513E60F357}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -26,33 +20,19 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2839AA55-B40A-4BB8-BDA0-C5057E5A683F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2839AA55-B40A-4BB8-BDA0-C5057E5A683F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2839AA55-B40A-4BB8-BDA0-C5057E5A683F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2839AA55-B40A-4BB8-BDA0-C5057E5A683F}.Release|Any CPU.Build.0 = Release|Any CPU - {1DC7365E-E01D-477A-82F7-2BC5EFC51640}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1DC7365E-E01D-477A-82F7-2BC5EFC51640}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1DC7365E-E01D-477A-82F7-2BC5EFC51640}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1DC7365E-E01D-477A-82F7-2BC5EFC51640}.Release|Any CPU.Build.0 = Release|Any CPU - {5B0BCF64-81B6-4A33-8D00-C673A0EF3551}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5B0BCF64-81B6-4A33-8D00-C673A0EF3551}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5B0BCF64-81B6-4A33-8D00-C673A0EF3551}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5B0BCF64-81B6-4A33-8D00-C673A0EF3551}.Release|Any CPU.Build.0 = Release|Any CPU - {324FC1A5-749A-44BF-BDB7-EB093F5DD88F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {324FC1A5-749A-44BF-BDB7-EB093F5DD88F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {324FC1A5-749A-44BF-BDB7-EB093F5DD88F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {324FC1A5-749A-44BF-BDB7-EB093F5DD88F}.Release|Any CPU.Build.0 = Release|Any CPU - {07010B53-B1CB-4696-8BB4-DCA2943B6B59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07010B53-B1CB-4696-8BB4-DCA2943B6B59}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07010B53-B1CB-4696-8BB4-DCA2943B6B59}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07010B53-B1CB-4696-8BB4-DCA2943B6B59}.Release|Any CPU.Build.0 = Release|Any CPU + {91A9A887-0298-4B72-9D24-C2438F469192}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91A9A887-0298-4B72-9D24-C2438F469192}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91A9A887-0298-4B72-9D24-C2438F469192}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91A9A887-0298-4B72-9D24-C2438F469192}.Release|Any CPU.Build.0 = Release|Any CPU + {FBD7AE5E-C842-448B-9A6B-81513E60F357}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBD7AE5E-C842-448B-9A6B-81513E60F357}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBD7AE5E-C842-448B-9A6B-81513E60F357}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBD7AE5E-C842-448B-9A6B-81513E60F357}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {1DC7365E-E01D-477A-82F7-2BC5EFC51640} = {94F2785A-49E5-447F-BAFF-BE8F783EC71F} - {324FC1A5-749A-44BF-BDB7-EB093F5DD88F} = {94F2785A-49E5-447F-BAFF-BE8F783EC71F} - {07010B53-B1CB-4696-8BB4-DCA2943B6B59} = {94F2785A-49E5-447F-BAFF-BE8F783EC71F} + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F99A8DD1-DE53-43D3-A0E0-BE222C826E90} EndGlobalSection EndGlobal diff --git a/GoCommando/Go.cs b/GoCommando/Go.cs index 4b64256..4685204 100644 --- a/GoCommando/Go.cs +++ b/GoCommando/Go.cs @@ -1,11 +1,9 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Configuration; using System.Diagnostics; using System.Linq; using System.Reflection; -using System.Runtime.InteropServices; using System.Security; using GoCommando.Internals; using Switch = GoCommando.Internals.Switch; @@ -283,22 +281,23 @@ static void InnerRun(ICommandFactory commandFactory, bool supportImpersonation) {availableCommands}"); } - var appSettings = ConfigurationManager.AppSettings.AllKeys - .Select(key => new - { - Key = key, - Value = ConfigurationManager.AppSettings[key] - }) - .ToDictionary(a => a.Key, a => a.Value); + //var appSettings = ConfigurationManager.AppSettings.AllKeys + // .Select(key => new + // { + // Key = key, + // Value = ConfigurationManager.AppSettings[key] + // }) + // .ToDictionary(a => a.Key, a => a.Value); - var connectionStrings = ConfigurationManager.ConnectionStrings.Cast() - .ToDictionary(a => a.Name, a => a.ConnectionString); + //var connectionStrings = ConfigurationManager.ConnectionStrings.Cast() + // .ToDictionary(a => a.Name, a => a.ConnectionString); var environmentVariables = Environment.GetEnvironmentVariables() .Cast() .ToDictionary(a => (string)a.Key, a => (string)a.Value); - var environmentSettings = new EnvironmentSettings(appSettings, connectionStrings, environmentVariables); + //var environmentSettings = new EnvironmentSettings(appSettings, connectionStrings, environmentVariables); + var environmentSettings = new EnvironmentSettings(new Dictionary(), new Dictionary(), environmentVariables); commandToRun.Invoke(arguments.Switches, environmentSettings); } diff --git a/GoCommando/GoCommando.csproj b/GoCommando/GoCommando.csproj index 8611a30..9f5c4f4 100644 --- a/GoCommando/GoCommando.csproj +++ b/GoCommando/GoCommando.csproj @@ -1,76 +1,7 @@ - - - + + - Debug - AnyCPU - {2839AA55-B40A-4BB8-BDA0-C5057E5A683F} - Library - Properties - GoCommando - GoCommando - v4.5 - 512 - + netstandard2.0 - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\GoCommando.XML - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + diff --git a/GoCommando/Internals/InternalsVisibleTo.cs b/GoCommando/Internals/InternalsVisibleTo.cs new file mode 100644 index 0000000..b8b5556 --- /dev/null +++ b/GoCommando/Internals/InternalsVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("GoCommando.Tests")] \ No newline at end of file diff --git a/GoCommando_old/BannerAttribute.cs b/GoCommando_old/BannerAttribute.cs new file mode 100644 index 0000000..ae86b51 --- /dev/null +++ b/GoCommando_old/BannerAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace GoCommando +{ + /// + /// Apply this attribute to the class that has your Main method in order to have a nice banner printed out when the program starts + /// + [AttributeUsage(AttributeTargets.Class)] + public class BannerAttribute : Attribute + { + /// + /// Gets the banner text + /// + public string BannerText { get; } + + /// + /// Constructs the attribute + /// + public BannerAttribute(string bannerText) + { + BannerText = bannerText; + } + } +} \ No newline at end of file diff --git a/GoCommando_old/CommandAttribute.cs b/GoCommando_old/CommandAttribute.cs new file mode 100644 index 0000000..c204ab4 --- /dev/null +++ b/GoCommando_old/CommandAttribute.cs @@ -0,0 +1,31 @@ +using System; + +namespace GoCommando +{ + /// + /// Attribute that can be applied to a class that represents a command. The class must implement + /// + [AttributeUsage(AttributeTargets.Class)] + public class CommandAttribute : Attribute + { + /// + /// Gets the name of the command + /// + public string Command { get; } + + /// + /// Gets the name of the command's group (if any). Grouping commands affects how they are presented when printing + /// help texts + /// + public string Group { get; } + + /// + /// Constructs the attribute + /// + public CommandAttribute(string command, string group = null) + { + Command = command; + Group = group ?? ""; + } + } +} \ No newline at end of file diff --git a/GoCommando_old/DescriptionAttribute.cs b/GoCommando_old/DescriptionAttribute.cs new file mode 100644 index 0000000..d585504 --- /dev/null +++ b/GoCommando_old/DescriptionAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace GoCommando +{ + /// + /// Apply this attribute to a property of a command class (which is also decorated with ) in + /// order to provide a description of the parameter + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)] + public class DescriptionAttribute : Attribute + { + /// + /// Gets the description text + /// + public string DescriptionText { get; } + + /// + /// Constructs the attribute + /// + public DescriptionAttribute(string descriptionText) + { + DescriptionText = descriptionText; + } + } +} \ No newline at end of file diff --git a/GoCommando_old/ExampleAttribute.cs b/GoCommando_old/ExampleAttribute.cs new file mode 100644 index 0000000..9f46135 --- /dev/null +++ b/GoCommando_old/ExampleAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace GoCommando +{ + /// + /// Apply one or more of these to a command property to show examples on how this particular parameter can be used + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public class ExampleAttribute : Attribute + { + /// + /// Gets the example text + /// + public string ExampleValue { get; } + + /// + /// Constructs the attribute + /// + public ExampleAttribute(string exampleValue) + { + ExampleValue = exampleValue; + } + } +} \ No newline at end of file diff --git a/GoCommando_old/ExitCodeException.cs b/GoCommando_old/ExitCodeException.cs new file mode 100644 index 0000000..f0150c1 --- /dev/null +++ b/GoCommando_old/ExitCodeException.cs @@ -0,0 +1,32 @@ +using System; +using System.Runtime.Serialization; + +namespace GoCommando +{ + /// + /// Exception that can be used to exit the program with a custom exit code + /// + [Serializable] + public class CustomExitCodeException : Exception + { + /// + /// Constructs the exception + /// + protected CustomExitCodeException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + /// + /// Constructs the exception + /// + public CustomExitCodeException(int exitCode, string message) : base(message) + { + ExitCode = exitCode; + } + + /// + /// Gets the exit code that the program must exit with + /// + public int ExitCode { get; } + } +} \ No newline at end of file diff --git a/GoCommando_old/Go.cs b/GoCommando_old/Go.cs new file mode 100644 index 0000000..4b64256 --- /dev/null +++ b/GoCommando_old/Go.cs @@ -0,0 +1,507 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Configuration; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security; +using GoCommando.Internals; +using Switch = GoCommando.Internals.Switch; +// ReSharper disable ArgumentsStyleNamedExpression + +namespace GoCommando +{ + /// + /// Here's how to go commando: 1. Go.Run() + /// + public static class Go + { + /// + /// Call this method from your Main method if you want to supply a custom which + /// will be used to create command instances + /// + public static void Run() where TCommandFactory : ICommandFactory, new() + { + var commandFactory = CreateCommandFactory(); + + Run(commandFactory); + } + + /// + /// Call this method from your Main method + /// + public static void Run() + { + Run(null); + } + + static void Run(ICommandFactory commandFactory) + { + try + { + var declaringType = Assembly.GetEntryAssembly()?.EntryPoint?.DeclaringType; + var supportImpersonation = declaringType?.GetCustomAttribute() != null; + + if (supportImpersonation) + { + var args = Environment.GetCommandLineArgs().Skip(1).ToList(); + var settings = new Settings(); + var arguments = Parse(args, settings); + + var impersonateNow = arguments.Switches.Any(s => s.Key == "username"); + + if (impersonateNow) + { + Impersonate(arguments); + return; + } + } + + var bannerAttribute = declaringType?.GetCustomAttribute(); + + if (bannerAttribute != null) + { + Console.WriteLine(bannerAttribute.BannerText); + } + + InnerRun(commandFactory, supportImpersonation); + } + catch (GoCommandoException friendlyException) + { + Environment.ExitCode = -1; + Console.WriteLine(friendlyException.Message); + Console.WriteLine(); + Console.WriteLine("Invoke with -help to get help for each command."); + Console.WriteLine(); + Console.WriteLine("Exit code: -1"); + Console.WriteLine(); + } + catch (CustomExitCodeException customExitCodeException) + { + FailAndExit(customExitCodeException, customExitCodeException.ExitCode); + } + catch (Exception exception) + { + FailAndExit(exception, -2); + } + } + + static void Impersonate(Arguments arguments) + { + var username = arguments.Switches.First(s => s.Key == "username"); + var password = arguments.Switches.FirstOrDefault(s => s.Key == "password") + ?? throw new ArgumentException("Please remember to also specify the -password switch when you use the -username switch"); + var domain = arguments.Switches.FirstOrDefault(s => s.Key == "domain")?.Value; + + var keysToRemove = new[] { "username", "password", "domain" }; + + Impersonate(username.Value, password.Value, domain, arguments.Switches.Where(s => !keysToRemove.Contains(s.Key)), arguments.Command); + } + + static void Impersonate(string username, string password, string domain, IEnumerable switches, string command) + { + var commandLineArgs = string.Join(" ", switches.Select(s => $"-{s.Key} {EnsureQuoted(s.Value)}")); + var ohSoSecureString = new SecureString(); + + foreach (var @char in password) { ohSoSecureString.AppendChar(@char); } + + var processStartInfo = new ProcessStartInfo + { + FileName = $"{Assembly.GetEntryAssembly().GetName().Name}.exe", + Arguments = $"{command} {string.Join(" ", commandLineArgs)}", + WorkingDirectory = Environment.CurrentDirectory, + + UserName = username, + Domain = domain, + Password = ohSoSecureString, + + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + + ErrorDialog = false, + WindowStyle = ProcessWindowStyle.Hidden, + LoadUserProfile = false + }; + + var process = new Process + { + StartInfo = processStartInfo, + }; + + void Write(string str, string prefix = "") + { + if (string.IsNullOrWhiteSpace(str)) + { + Console.WriteLine(); + } + else if (string.IsNullOrWhiteSpace(prefix)) + { + Console.WriteLine(str); + } + else + { + Console.WriteLine($"{prefix} {str}"); + } + } + + process.OutputDataReceived += (s, ea) => Write(ea.Data); + process.ErrorDataReceived += (s, ea) => Write(ea.Data, "ERR"); + + try + { + Console.WriteLine($"Invoking command as {username}..."); + + process.Start(); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + process.WaitForExit(); + } + catch (Exception exception) + { + throw new ApplicationException("An error occurred when running the command under impersonation", exception); + } + + if (process.ExitCode != 0) + { + throw new ApplicationException($"Impersonated process exited with code {process.ExitCode}"); + } + } + + static string EnsureQuoted(string str) + { + if (str == null) return str; + const string doubleQuote = "\""; + return str.StartsWith(doubleQuote) && str.EndsWith(doubleQuote) ? str : $"\"{str}\""; + } + + + static TCommandFactory CreateCommandFactory() where TCommandFactory : ICommandFactory, new() + { + try + { + return new TCommandFactory(); + } + catch (Exception exception) + { + throw new ApplicationException($"Could not create command factory of type {typeof(TCommandFactory)}", exception); + } + } + + static void FailAndExit(Exception customExitCodeException, int exitCode) + { + Console.WriteLine(); + Console.Error.WriteLine(customExitCodeException); + Console.WriteLine(); + Console.WriteLine("Exit code: {0}", exitCode); + Console.WriteLine(); + + Environment.ExitCode = exitCode; + } + + static void InnerRun(ICommandFactory commandFactory, bool supportImpersonation) + { + var args = Environment.GetCommandLineArgs().Skip(1).ToList(); + var settings = new Settings(); + var arguments = Parse(args, settings); + var commandTypes = GetCommands(commandFactory, settings, supportImpersonation: supportImpersonation); + + var helpSwitch = arguments.Switches.FirstOrDefault(s => s.Key == "?") + ?? arguments.Switches.FirstOrDefault(s => s.Key == "help"); + + if (helpSwitch != null) + { + var exe = Assembly.GetEntryAssembly().GetName().Name + ".exe"; + + if (helpSwitch.Value != null) + { + var command = commandTypes.FirstOrDefault(c => c.Command == helpSwitch.Value); + + if (command != null) + { + if (command.Parameters.Any()) + { + Console.WriteLine( + $@"{command.Description} + +Type + + {exe} {command.Command} + +where can consist of the following parameters: + +{string + .Join(Environment.NewLine, + command.Parameters.Select(parameter => FormatParameter(parameter, settings)))}"); + } + else + { + Console.WriteLine(@"Type + + {0} {1} + +", exe, command.Command); + } + return; + } + + throw new GoCommandoException($"Unknown command: '{helpSwitch.Value}'"); + } + + var availableCommands = GetAvailableCommandsHelpText(commandTypes); + + Console.WriteLine($@"The following commands are available: + +{availableCommands} + +Type + + {exe} -help + +to get help for a command. +"); + return; + } + + var commandToRun = commandTypes.FirstOrDefault(c => c.Command == arguments.Command); + + if (commandToRun == null) + { + var errorText = !string.IsNullOrWhiteSpace(arguments.Command) + ? $"Could not find command '{arguments.Command}'" + : "Please invoke with a command"; + + var availableCommands = GetAvailableCommandsHelpText(commandTypes); + + throw new GoCommandoException($@"{errorText} - the following commands are available: + +{availableCommands}"); + } + + var appSettings = ConfigurationManager.AppSettings.AllKeys + .Select(key => new + { + Key = key, + Value = ConfigurationManager.AppSettings[key] + }) + .ToDictionary(a => a.Key, a => a.Value); + + var connectionStrings = ConfigurationManager.ConnectionStrings.Cast() + .ToDictionary(a => a.Name, a => a.ConnectionString); + + var environmentVariables = Environment.GetEnvironmentVariables() + .Cast() + .ToDictionary(a => (string)a.Key, a => (string)a.Value); + + var environmentSettings = new EnvironmentSettings(appSettings, connectionStrings, environmentVariables); + + commandToRun.Invoke(arguments.Switches, environmentSettings); + } + + static string GetAvailableCommandsHelpText(List commandTypes) + { + var commandGroups = commandTypes + .GroupBy(c => c.Group) + .ToList(); + + if (commandGroups.Count == 1) + { + return string.Join(Environment.NewLine, commandTypes.Select(c => $" {c.Command} - {c.Description}")); + } + + return string.Join(Environment.NewLine + Environment.NewLine, + commandGroups + .Select(g => $@" {g.Key}: + +{string.Join(Environment.NewLine, g.Select(c => $" {c.Command} - {c.Description}"))}")); + + } + + static string FormatParameter(Parameter parameter, Settings settings) + { + var shorthand = parameter.Shortname != null + ? $" / {settings.SwitchPrefix}{parameter.Shortname}" + : ""; + + var additionalProperties = new List(); + + var isFlag = parameter.IsFlag; + + if (isFlag) + { + additionalProperties.Add("flag"); + } + + if (parameter.Optional) + { + additionalProperties.Add("optional"); + } + + var additionalPropertiesText = additionalProperties.Any() + ? $" ({string.Join("/", additionalProperties)})" + : ""; + + var helpText = " " + (parameter.DescriptionText ?? "(no help text available)"); + + var examplesText = !parameter.ExampleValues.Any() + ? "" + : FormatExamples(parameter, settings); + + var switchText = $"{settings.SwitchPrefix}{parameter.Name}{shorthand}{additionalPropertiesText}"; + + return $@" {switchText} +{helpText} +{examplesText}"; + } + + static string FormatExamples(Parameter parameter, Settings settings) + { + var examples = string.Join(Environment.NewLine, parameter.ExampleValues + .Select(e => $" {settings.SwitchPrefix}{parameter.Name} {e}")); + + return $@" + Examples: +{examples} +"; + } + + internal static List GetCommands(ICommandFactory commandFactory, Settings settings, bool supportImpersonation) + { + var commandAttributes = Assembly.GetEntryAssembly().GetTypes() + .Select(t => new + { + Type = t, + Attribute = t.GetCustomAttribute() + }) + .Where(a => a.Attribute != null) + .GroupBy(a => a.Attribute.Command) + .ToList(); + + var duplicateCommands = commandAttributes + .Where(g => g.Count() > 1) + .ToList(); + + if (duplicateCommands.Any()) + { + throw new GoCommandoException($@"The following commands are duplicates: + +{string.Join(Environment.NewLine, duplicateCommands.SelectMany(g => g.Select(a => $" {a.Attribute.Command}: {a.Type}")))}"); + } + + return commandAttributes + .Select(g => + { + var a = g.First(); + + return new CommandInvoker(a.Attribute.Command, a.Type, settings, a.Attribute.Group, commandFactory); + }) + .ToList(); + } + + internal static Arguments Parse(IEnumerable args, Settings settings) + { + var list = args.ToList(); + + if (!list.Any()) return new Arguments(null, Enumerable.Empty(), settings); + + var first = list.First(); + + string command; + List switchArgs; + + if (first.StartsWith(settings.SwitchPrefix)) + { + command = null; + switchArgs = list; + } + else + { + command = first; + switchArgs = list.Skip(1).ToList(); + } + + var switches = new List(); + + string key = null; + + foreach (var arg in switchArgs) + { + if (arg.StartsWith(settings.SwitchPrefix)) + { + if (key != null) + { + switches.Add(Switch.Flag(key)); + } + + key = arg.Substring(settings.SwitchPrefix.Length); + + if (HasKeyAndValue(key)) + { + var keyAndValue = GetKeyAndValueFromKey(key); + if (keyAndValue == null) + { + throw new ApplicationException($"Expected to get key-value-pair from key '{key}'"); + } + switches.Add(Switch.KeyValue(keyAndValue.Value.Key, keyAndValue.Value.Value)); + key = null; + } + + continue; + } + + var value = arg; + + if (key == null) + { + throw new GoCommandoException($"Got command line argument '{value}' without a switch in front of it - please specify switches like this: '{settings.SwitchPrefix}switch some-value'"); + } + + switches.Add(Switch.KeyValue(key, value)); + + key = null; + } + + if (key != null) + { + switches.Add(Switch.Flag(key)); + } + + return new Arguments(command, switches, settings); + } + + static bool HasKeyAndValue(string key) + { + return GetKeyAndValueFromKey(key) != null; + } + + static KeyValuePair? GetKeyAndValueFromKey(string key) + { + for (var index = 0; index < key.Length; index++) + { + var c = key[index]; + + if (c == ':') + { + return new KeyValuePair(key.Substring(0, index), key.Substring(index + 1)); + } + + if (c == '=') + { + return new KeyValuePair(key.Substring(0, index), key.Substring(index + 1)); + } + + if (!char.IsLetter(c)) + { + return new KeyValuePair(key.Substring(0, index), key.Substring(index)); + } + } + + return null; + } + } +} diff --git a/GoCommando_old/GoCommando.csproj b/GoCommando_old/GoCommando.csproj new file mode 100644 index 0000000..8611a30 --- /dev/null +++ b/GoCommando_old/GoCommando.csproj @@ -0,0 +1,76 @@ + + + + + Debug + AnyCPU + {2839AA55-B40A-4BB8-BDA0-C5057E5A683F} + Library + Properties + GoCommando + GoCommando + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + bin\Release\GoCommando.XML + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GoCommando_old/GoCommandoException.cs b/GoCommando_old/GoCommandoException.cs new file mode 100644 index 0000000..94ce5a9 --- /dev/null +++ b/GoCommando_old/GoCommandoException.cs @@ -0,0 +1,27 @@ +using System; +using System.Runtime.Serialization; + +namespace GoCommando +{ + /// + /// Friendly exception that can be thrown in cases where you want the program to exit with + /// a nice, human-readable message. Only the message will be shown. + /// + [Serializable] + public class GoCommandoException : Exception + { + /// + /// Constructs the exception + /// + protected GoCommandoException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + /// + /// Constructs the exception + /// + public GoCommandoException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/GoCommando_old/ICommand.cs b/GoCommando_old/ICommand.cs new file mode 100644 index 0000000..d2f1b61 --- /dev/null +++ b/GoCommando_old/ICommand.cs @@ -0,0 +1,13 @@ +namespace GoCommando +{ + /// + /// Implement this interface on each command + /// + public interface ICommand + { + /// + /// Main run method that is invoked by GoCommando + /// + void Run(); + } +} \ No newline at end of file diff --git a/GoCommando_old/ICommandFactory.cs b/GoCommando_old/ICommandFactory.cs new file mode 100644 index 0000000..8a6fc53 --- /dev/null +++ b/GoCommando_old/ICommandFactory.cs @@ -0,0 +1,21 @@ +using System; + +namespace GoCommando +{ + /// + /// Can be implemented to supply a custom command factory that will be given a chance to create command instances + /// and dispose of them after use + /// + public interface ICommandFactory + { + /// + /// Should create a new command instance of the given + /// + ICommand Create(Type commandType); + + /// + /// Should release the command instance - probably by disposing it or delegating the disposal to an IoC container + /// + void Release(ICommand command); + } +} \ No newline at end of file diff --git a/GoCommando_old/Internals/Arguments.cs b/GoCommando_old/Internals/Arguments.cs new file mode 100644 index 0000000..862be68 --- /dev/null +++ b/GoCommando_old/Internals/Arguments.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace GoCommando.Internals +{ + class Arguments + { + readonly Settings _settings; + + public Arguments(string command, IEnumerable switches, Settings settings) + { + _settings = settings; + var switchList = switches.ToList(); + var duplicateSwitchKeys = switchList.GroupBy(s => s.Key).Where(g => g.Count() > 1).ToList(); + + if (duplicateSwitchKeys.Any()) + { + var dupes = string.Join(", ", duplicateSwitchKeys.Select(g => $"{settings.SwitchPrefix}{g.Key}")); + + throw new GoCommandoException($"The following switches have been specified more than once: {dupes}"); + } + + Command = command; + Switches = switchList; + } + + public string Command { get; } + + public IEnumerable Switches { get; } + + public TValue Get(string key) + { + var desiredType = typeof(TValue); + + try + { + if (desiredType == typeof(bool)) + { + return (TValue)Convert.ChangeType(Switches.Any(s => s.Key == key), desiredType); + } + + var relevantSwitch = Switches.FirstOrDefault(s => s.Key == key); + + if (relevantSwitch != null) + { + return (TValue)Convert.ChangeType(relevantSwitch.Value, desiredType); + } + + throw new GoCommandoException($"Could not find switch '{key}'"); + } + catch (Exception exception) + { + throw new FormatException($"Could not get switch '{key}' as a {desiredType}", exception); + } } + + public override string ToString() + { + return $@"{Command} + +{string.Join(Environment.NewLine, Switches.Select(s => " " + s))}"; + } + } +} \ No newline at end of file diff --git a/GoCommando_old/Internals/CommandInvoker.cs b/GoCommando_old/Internals/CommandInvoker.cs new file mode 100644 index 0000000..a85f520 --- /dev/null +++ b/GoCommando_old/Internals/CommandInvoker.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace GoCommando.Internals +{ + class CommandInvoker + { + readonly Settings _settings; + readonly ICommand _commandInstance; + readonly Action _releaser; + readonly List _parameters; + + public CommandInvoker(string command, Type type, Settings settings, string @group = null, + ICommandFactory commandFactory = null) + : this(command, settings, CreateInstance(type, GetFactoryMethod(commandFactory)), group, GetReleaseMethod(commandFactory)) + { + } + + public CommandInvoker(string command, Settings settings, ICommand commandInstance, string group = null, Action releaseMethod = null) + { + if (command == null) throw new ArgumentNullException(nameof(command)); + if (settings == null) throw new ArgumentNullException(nameof(settings)); + if (commandInstance == null) throw new ArgumentNullException(nameof(commandInstance)); + + _settings = settings; + _commandInstance = commandInstance; + _releaser = releaseMethod ?? DefaultReleaseMethod; + + Command = command; + Group = group; + + _parameters = GetParameters(Type).ToList(); + } + + static void DefaultReleaseMethod(ICommand command) + { + var disposable = command as IDisposable; + + disposable?.Dispose(); + } + + static Func GetFactoryMethod(ICommandFactory commandFactory) + { + if (commandFactory == null) return null; + + return commandFactory.Create; + } + + static Action GetReleaseMethod(ICommandFactory commandFactory) + { + if (commandFactory == null) return null; + + return commandFactory.Release; + } + + static ICommand CreateInstance(Type type, Func commandFactory = null) + { + try + { + var instance = commandFactory?.Invoke(type) + ?? Activator.CreateInstance(type); + + if (!(instance is ICommand)) + { + throw new ApplicationException($"{instance} does not implement ICommand!"); + } + + return (ICommand)instance; + } + catch (Exception exception) + { + throw new ApplicationException($"Could not use type {type} as a GoCommando command", exception); + } + } + + static IEnumerable GetParameters(Type type) + { + return type + .GetProperties() + .Select(p => new + { + Property = p, + ParameterAttribute = GetSingleAttributeOrNull(p), + DescriptionAttribute = GetSingleAttributeOrNull(p), + ExampleAttributes = p.GetCustomAttributes() + }) + .Where(a => a.ParameterAttribute != null) + .Select(a => new Parameter(a.Property, + a.ParameterAttribute.Name, + a.ParameterAttribute.ShortName, + a.ParameterAttribute.Optional, + a.DescriptionAttribute?.DescriptionText, + a.ExampleAttributes.Select(e => e.ExampleValue), + a.ParameterAttribute.DefaultValue, + a.ParameterAttribute.AllowAppSetting, + a.ParameterAttribute.AllowConnectionString, + a.ParameterAttribute.AllowEnvironmentVariable)) + .ToList(); + } + + static TAttribute GetSingleAttributeOrNull(PropertyInfo p) where TAttribute : Attribute + { + return p.GetCustomAttributes(typeof(TAttribute), false) + .Cast() + .FirstOrDefault(); + } + + public string Group { get; } + + public string Command { get; } + + public Type Type => _commandInstance.GetType(); + + public IEnumerable Parameters => _parameters; + + public string Description => Type.GetCustomAttribute()?.DescriptionText ?? + "(no help text for this command)"; + + public ICommand CommandInstance => _commandInstance; + + public void Invoke(IEnumerable switches, EnvironmentSettings environmentSettings) + { + try + { + InnerInvoke(switches, environmentSettings); + } + finally + { + _releaser(CommandInstance); + } + } + + void InnerInvoke(IEnumerable switches, EnvironmentSettings environmentSettings) + { + var commandInstance = _commandInstance; + + var requiredParametersMissing = Parameters + .Where(p => !p.Optional + && !p.HasDefaultValue + && !CanBeResolvedFromSwitches(switches, p) + && !CanBeResolvedFromEnvironmentSettings(environmentSettings, p)) + .ToList(); + + var optionalParamtersNotSpecified = Parameters + .Where(p => p.Optional + && !CanBeResolvedFromSwitches(switches, p) + && !CanBeResolvedFromEnvironmentSettings(environmentSettings, p)) + .ToList(); + + if (requiredParametersMissing.Any()) + { + var requiredParametersMissingString = string.Join(Environment.NewLine, + requiredParametersMissing.Select(p => $" {_settings.SwitchPrefix}{p.Name} - {p.DescriptionText}")); + + var text = $@"The following required parameters are missing: + +{requiredParametersMissingString}"; + + if (optionalParamtersNotSpecified.Any()) + { + var optionalParamtersNotSpecifiedString = string.Join(Environment.NewLine, + optionalParamtersNotSpecified.Select(p => $" {_settings.SwitchPrefix}{p.Name} - {p.DescriptionText}")); + + var moreText = $@"The following optional parameters are also available: + +{optionalParamtersNotSpecifiedString}"; + + throw new GoCommandoException(string.Concat( + text, + Environment.NewLine, + Environment.NewLine, + moreText + )); + } + + throw new GoCommandoException(text); + } + + var switchesWithoutMathingParameter = switches + .Where(s => !Parameters.Any(p => p.MatchesKey(s.Key))) + .ToList(); + + if (switchesWithoutMathingParameter.Any()) + { + var switchesWithoutMathingParameterString = string.Join(Environment.NewLine, + switchesWithoutMathingParameter.Select(p => p.Value != null + ? $" {_settings.SwitchPrefix}{p.Key} = {p.Value}" + : $" {_settings.SwitchPrefix}{p.Key}")); + + throw new GoCommandoException( + $@"The following switches do not have a corresponding parameter: + +{switchesWithoutMathingParameterString}"); + } + + var setParameters = new HashSet(); + + ResolveParametersFromSwitches(switches, commandInstance, setParameters); + + ResolveParametersFromEnvironmentSettings(environmentSettings, commandInstance, setParameters, Parameters); + + ResolveParametersWithDefaultValues(setParameters, commandInstance); + + commandInstance.Run(); + } + + static void ResolveParametersFromEnvironmentSettings(EnvironmentSettings environmentSettings, ICommand commandInstance, HashSet setParameters, IEnumerable parameters) + { + foreach (var parameter in parameters.Where(p => p.AllowAppSetting && !setParameters.Contains(p))) + { + if (!environmentSettings.HasAppSetting(parameter.Name)) continue; + + var appSettingValue = environmentSettings.GetAppSetting(parameter.Name); + + SetParameter(commandInstance, setParameters, parameter, appSettingValue); + } + + foreach (var parameter in parameters.Where(p => p.AllowConnectionString && !setParameters.Contains(p))) + { + if (!environmentSettings.HasConnectionString(parameter.Name)) continue; + + var appSettingValue = environmentSettings.GetConnectionString(parameter.Name); + + SetParameter(commandInstance, setParameters, parameter, appSettingValue); + } + + foreach (var parameter in parameters.Where(p => p.AllowEnvironmentVariable && !setParameters.Contains(p))) + { + if (!environmentSettings.HasEnvironmentVariable(parameter.Name)) continue; + + var appSettingValue = environmentSettings.GetEnvironmentVariable(parameter.Name); + + SetParameter(commandInstance, setParameters, parameter, appSettingValue); + } + } + + void ResolveParametersWithDefaultValues(IEnumerable setParameters, ICommand commandInstance) + { + foreach (var parameterWithDefaultValue in Parameters.Where(p => p.HasDefaultValue).Except(setParameters)) + { + parameterWithDefaultValue.ApplyDefaultValue(commandInstance); + } + } + + void ResolveParametersFromSwitches(IEnumerable switches, ICommand commandInstance, ISet setParameters) + { + foreach (var switchToSet in switches) + { + var correspondingParameter = Parameters.FirstOrDefault(p => p.MatchesKey(switchToSet.Key)); + + if (correspondingParameter == null) + { + throw new GoCommandoException( + $"The switch {_settings}{switchToSet.Key} does not correspond to a parameter of the '{Command}' command!"); + } + + var value = switchToSet.Value; + + SetParameter(commandInstance, setParameters, correspondingParameter, value); + } + } + + static void SetParameter(ICommand commandInstance, ISet setParameters, Parameter parameter, string value) + { + parameter.SetValue(commandInstance, value); + setParameters.Add(parameter); + } + + static bool CanBeResolvedFromEnvironmentSettings(EnvironmentSettings environmentSettings, Parameter parameter) + { + var name = parameter.Name; + + if (parameter.AllowAppSetting && environmentSettings.HasAppSetting(name)) + { + return true; + } + + if (parameter.AllowConnectionString && environmentSettings.HasConnectionString(name)) + { + return true; + } + + if (parameter.AllowEnvironmentVariable && environmentSettings.HasEnvironmentVariable(name)) + { + return true; + } + + return false; + } + + static bool CanBeResolvedFromSwitches(IEnumerable switches, Parameter p) + { + return switches.Any(s => p.MatchesKey(s.Key)); + } + } +} \ No newline at end of file diff --git a/GoCommando_old/Internals/EnvironmentSettings.cs b/GoCommando_old/Internals/EnvironmentSettings.cs new file mode 100644 index 0000000..dce6ff4 --- /dev/null +++ b/GoCommando_old/Internals/EnvironmentSettings.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; + +namespace GoCommando.Internals +{ + class EnvironmentSettings + { + static readonly IDictionary None = new Dictionary(); + public static readonly EnvironmentSettings Empty = new EnvironmentSettings(None, None); + + readonly IDictionary _appSettings; + readonly IDictionary _connectionStrings; + readonly IDictionary _environmentVariables; + + public EnvironmentSettings(IDictionary appSettings = null, IDictionary connectionStrings = null, IDictionary environmentVariables = null) + { + _environmentVariables = environmentVariables ?? None; + _appSettings = appSettings ?? None; + _connectionStrings = connectionStrings ?? None; + } + + public bool HasAppSetting(string name) + { + return _appSettings.ContainsKey(name); + } + + public bool HasConnectionString(string name) + { + return _connectionStrings.ContainsKey(name); + } + + public bool HasEnvironmentVariable(string name) + { + return _environmentVariables.ContainsKey(name); + } + + public string GetAppSetting(string key) + { + try + { + return _appSettings[key]; + } + catch (Exception exception) + { + throw new KeyNotFoundException($"Could not find appSetting with key '{key}'", exception); + } + } + + public string GetEnvironmentVariable(string name) + { + try + { + return _environmentVariables[name]; + } + catch (Exception exception) + { + throw new KeyNotFoundException($"Could not find environment variable with the name '{name}'", exception); + } + } + + public string GetConnectionString(string name) + { + try + { + return _connectionStrings[name]; + } + catch (Exception exception) + { + throw new KeyNotFoundException($"Could not find connectionString with key '{name}'", exception); + } + } + } +} \ No newline at end of file diff --git a/GoCommando_old/Internals/Parameter.cs b/GoCommando_old/Internals/Parameter.cs new file mode 100644 index 0000000..14a2bb2 --- /dev/null +++ b/GoCommando_old/Internals/Parameter.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace GoCommando.Internals +{ + class Parameter : IEquatable + { + public PropertyInfo PropertyInfo { get; } + public string Name { get; } + public string Shortname { get; } + public bool Optional { get; } + public string DescriptionText { get; } + public string DefaultValue { get; } + public bool AllowAppSetting { get; } + public bool AllowConnectionString { get; } + public bool AllowEnvironmentVariable { get; } + public string[] ExampleValues { get; } + + public bool IsFlag => PropertyInfo.PropertyType == typeof(bool); + + public bool HasDefaultValue => DefaultValue != null; + + public Parameter(PropertyInfo propertyInfo, string name, string shortname, bool optional, string descriptionText, IEnumerable exampleValues, string defaultValue, bool allowAppSetting, bool allowConnectionString, bool allowEnvironmentVariable) + { + PropertyInfo = propertyInfo; + Name = name; + Shortname = shortname; + Optional = optional; + DescriptionText = GetText(descriptionText, allowAppSetting, allowConnectionString, allowEnvironmentVariable); + DefaultValue = defaultValue; + AllowAppSetting = allowAppSetting; + AllowConnectionString = allowConnectionString; + AllowEnvironmentVariable = allowEnvironmentVariable; + ExampleValues = exampleValues.ToArray(); + } + + private string GetText(string descriptionText, bool allowAppSetting, bool allowConnectionString, bool allowEnvironmentVariable) + { + if (!allowAppSetting && !allowConnectionString && !allowEnvironmentVariable) + { + return $"{descriptionText ?? ""}"; + } + + var autoBindings = new List(); + + if (allowEnvironmentVariable) + { + autoBindings.Add("ENV"); + } + + if (allowAppSetting) + { + autoBindings.Add("APP"); + } + + if (allowConnectionString) + { + autoBindings.Add("CONN"); + } + + return $"{descriptionText ?? ""} ({string.Join(", ", autoBindings)})"; + } + + public bool MatchesKey(string key) + { + return key == Name + || (Shortname != null && key == Shortname); + } + + public void SetValue(object commandInstance, string value) + { + try + { + var valueInTheRightType = PropertyInfo.PropertyType == typeof(bool) + ? true + : Convert.ChangeType(value, PropertyInfo.PropertyType); + + PropertyInfo.SetValue(commandInstance, valueInTheRightType); + } + catch (Exception exception) + { + throw new FormatException($"Could not set value '{value}' on property named '{PropertyInfo.Name}' on {PropertyInfo.DeclaringType}", exception); + } + } + + public void ApplyDefaultValue(ICommand commandInstance) + { + if (!HasDefaultValue) + { + throw new InvalidOperationException($"Cannot apply default value of '{Name}' parameter because it has no default!"); + } + + SetValue(commandInstance, DefaultValue); + } + + public bool Equals(Parameter other) + { + return Name.Equals(other.Name); + } + } +} \ No newline at end of file diff --git a/GoCommando_old/Internals/Settings.cs b/GoCommando_old/Internals/Settings.cs new file mode 100644 index 0000000..f9c1edb --- /dev/null +++ b/GoCommando_old/Internals/Settings.cs @@ -0,0 +1,12 @@ +namespace GoCommando.Internals +{ + class Settings + { + public Settings() + { + SwitchPrefix = "-"; + } + + public string SwitchPrefix { get; set; } + } +} \ No newline at end of file diff --git a/GoCommando_old/Internals/StringExtensions.cs b/GoCommando_old/Internals/StringExtensions.cs new file mode 100644 index 0000000..4f8aaaa --- /dev/null +++ b/GoCommando_old/Internals/StringExtensions.cs @@ -0,0 +1,68 @@ +using System; +using System.Linq; +using System.Text; + +namespace GoCommando.Internals +{ + static class StringExtensions + { + public static string WrappedAt(this string str, int width) + { + var twoLineBreaks = Environment.NewLine + Environment.NewLine; + + var sections = str.Split(new[] { twoLineBreaks }, + StringSplitOptions.RemoveEmptyEntries); + + return string.Join(twoLineBreaks, sections.Select(section => WrapSection(section, width))); + } + + static string WrapSection(string section, int width) + { + var oneLongString = string.Join(" ", + section.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)); + + var words = oneLongString.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries); + + var builder = new StringBuilder(); + + var currentLineLength = 0; + + for (var index = 0; index < words.Length; index++) + { + var word = words[index]; + builder.Append(word); + currentLineLength += word.Length; + + if (index < words.Length - 1) + { + var nextWord = words[index]; + + var spaceLeftOnCurrentLine = width - currentLineLength - 1; // -1 to leave room for space... + var nextWordIsTooLong = nextWord.Length > spaceLeftOnCurrentLine; + + if (nextWordIsTooLong) + { + builder.AppendLine(); + currentLineLength = 0; + } + else + { + builder.Append(" "); + currentLineLength++; + } + } + } + + return builder.ToString(); + } + + public static string Indented(this string str, int indent) + { + var indentedLines = str + .Split(new[] { Environment.NewLine }, StringSplitOptions.None) + .Select(line => string.Concat(new string(' ', indent), line)); + + return string.Join(Environment.NewLine, indentedLines); + } + } +} \ No newline at end of file diff --git a/GoCommando_old/Internals/Switch.cs b/GoCommando_old/Internals/Switch.cs new file mode 100644 index 0000000..185c6f8 --- /dev/null +++ b/GoCommando_old/Internals/Switch.cs @@ -0,0 +1,55 @@ +// ReSharper disable LoopCanBeConvertedToQuery +namespace GoCommando.Internals +{ + class Switch + { + static readonly char[] AcceptedQuoteCharacters = { '"', '\'' }; + + public static Switch KeyValue(string key, string value) + { + return new Switch(key, value); + } + + public static Switch Flag(string key) + { + return new Switch(key, null); + } + + Switch(string key, string value) + { + Key = key; + Value = Unquote(value); + } + + static string Unquote(string value) + { + if (value == null) return null; + + // can't be quoted + if (value.Length < 2) return value; + + foreach (var quoteChar in AcceptedQuoteCharacters) + { + var quote = quoteChar.ToString(); + + if (value.StartsWith(quote) + && value.EndsWith(quote)) + { + return value.Substring(1, value.Length - 2); + } + } + + return value; + } + + public string Key { get; } + public string Value { get; } + + public override string ToString() + { + return Value == null + ? Key + : $"{Key} = {Value}"; + } + } +} \ No newline at end of file diff --git a/GoCommando_old/ParameterAttribute.cs b/GoCommando_old/ParameterAttribute.cs new file mode 100644 index 0000000..e8b5c39 --- /dev/null +++ b/GoCommando_old/ParameterAttribute.cs @@ -0,0 +1,79 @@ +using System; + +namespace GoCommando +{ + /// + /// Apply this attribute to a property of a command class (i.e. one that implements ) + /// + [AttributeUsage(AttributeTargets.Property)] + public class ParameterAttribute : Attribute + { + /// + /// Gets the primary parameter name + /// + public string Name { get; } + + /// + /// Gets a shorthand for the parameter (or null if none has been specified) + /// + public string ShortName { get; } + + /// + /// Gets whether this parameter is optional + /// + public bool Optional { get; } + + /// + /// Gets a default value for the parameter (or null if none has been specified) + /// + public string DefaultValue { get; } + + /// + /// Gets whether this parameter can have its value resolved from the <appSettings> section of the application configuration file. + /// If the value is provided with a command-line switch, the provided value takes precedence. + /// + public bool AllowAppSetting { get; } + + /// + /// Gets whether this parameter can have its value resolved from the <connectionStrings> section of the application configuration file. + /// If the value is provided with a command-line switch, the provided value takes precedence. + /// + public bool AllowConnectionString { get; } + + /// + /// Gets whether this parameter can have its value resolved from an environment variable with the same name as specified by + /// If the value is provided with a command-line switch, the provided value takes precedence. + /// + public bool AllowEnvironmentVariable { get; } + + /// + /// Constructs the parameter attribute + /// + /// Primary name of the parameter + /// Optional shorthand of the parameter + /// Indicates whether the parameter MUST be specified or can be omitted + /// Provides a default value to use when other values could not be found + /// + /// Indicates whether parameter value resolution can go and look in the <appSettings> section of + /// the current application configuration file for a value. Will look for the key specified by + /// + /// + /// Indicates whether parameter value resolution can go and look in the <connectionStrings> section of + /// the current application configuration file for a value. Will look for the name specified by + /// + /// + /// Indicates whether parameter value resolution can go and look for an environment variable for a value. + /// Will look for the name specified by + /// + public ParameterAttribute(string name, string shortName = null, bool optional = false, string defaultValue = null, bool allowAppSetting = false, bool allowConnectionString = false, bool allowEnvironmentVariable = false) + { + Name = name; + ShortName = shortName; + Optional = optional; + DefaultValue = defaultValue; + AllowAppSetting = allowAppSetting; + AllowConnectionString = allowConnectionString; + AllowEnvironmentVariable = allowEnvironmentVariable; + } + } +} \ No newline at end of file diff --git a/GoCommando/Properties/AssemblyInfo.cs b/GoCommando_old/Properties/AssemblyInfo.cs similarity index 100% rename from GoCommando/Properties/AssemblyInfo.cs rename to GoCommando_old/Properties/AssemblyInfo.cs diff --git a/GoCommando_old/SupportImpersonationAttribute.cs b/GoCommando_old/SupportImpersonationAttribute.cs new file mode 100644 index 0000000..a657567 --- /dev/null +++ b/GoCommando_old/SupportImpersonationAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace GoCommando +{ + /// + /// Apply this attribute to the class that has your Main method in order to support impersonation + /// + [AttributeUsage(AttributeTargets.Class)] + public class SupportImpersonationAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/TestApp2/App.config b/TestApp2_old/App.config similarity index 100% rename from TestApp2/App.config rename to TestApp2_old/App.config diff --git a/TestApp2/ElCommandante.cs b/TestApp2_old/ElCommandante.cs similarity index 100% rename from TestApp2/ElCommandante.cs rename to TestApp2_old/ElCommandante.cs diff --git a/TestApp2/Program.cs b/TestApp2_old/Program.cs similarity index 100% rename from TestApp2/Program.cs rename to TestApp2_old/Program.cs diff --git a/TestApp2/Properties/AssemblyInfo.cs b/TestApp2_old/Properties/AssemblyInfo.cs similarity index 100% rename from TestApp2/Properties/AssemblyInfo.cs rename to TestApp2_old/Properties/AssemblyInfo.cs diff --git a/TestApp2/TestApp2.csproj b/TestApp2_old/TestApp2.csproj similarity index 92% rename from TestApp2/TestApp2.csproj rename to TestApp2_old/TestApp2.csproj index b4e8c44..adb1d2f 100644 --- a/TestApp2/TestApp2.csproj +++ b/TestApp2_old/TestApp2.csproj @@ -49,12 +49,6 @@ - - - {2839AA55-B40A-4BB8-BDA0-C5057E5A683F} - GoCommando - -