diff --git a/src/tye/CommonArguments.cs b/src/tye/CommonArguments.cs new file mode 100644 index 00000000..8c5f238b --- /dev/null +++ b/src/tye/CommonArguments.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Parsing; +using System.IO; + +namespace Tye +{ + internal static class CommonArguments + { + private static readonly string[] FileFormats = new[] { "tye.yaml", "tye.yml", "*.csproj", "*.fsproj", "*.sln" }; + + public static Argument Path_Optional + { + get + { + return new Argument((r) => TryParsePath(r, required: false), isDefault: true) + { + Arity = ArgumentArity.ZeroOrOne, + Description = "file or directory, can be a yaml, sln, or project file", + Name = "path", + }; + } + } + + public static Argument Path_Required + { + get + { + return new Argument((r) => TryParsePath(r, required: true), isDefault: true) + { + Arity = ArgumentArity.ZeroOrOne, + Description = "file or directory, can be a yaml, sln, or project file", + Name = "path", + }; + } + } + + public static bool TryFindSupportedFile(string directoryPath, out string? filePath, out string? errorMessage) + { + foreach (var format in FileFormats) + { + var files = Directory.GetFiles(directoryPath, format); + + if (files.Length == 1) + { + errorMessage = null; + filePath = files[0]; + return true; + } + + if (files.Length > 1) + { + errorMessage = $"More than one matching files was found in directory '{directoryPath}'."; + filePath = default; + return false; + } + } + + errorMessage = $"No project project file or solution was found in directory '{directoryPath}'."; + filePath = default; + return false; + } + + static FileInfo TryParsePath(ArgumentResult result, bool required) + { + var token = result.Tokens.Count switch + { + 0 => ".", + 1 => result.Tokens[0].Value, + _ => throw new InvalidOperationException("Unexpected token count."), + }; + + if (string.IsNullOrEmpty(token)) + { + token = "."; + } + + if (File.Exists(token)) + { + return new FileInfo(token); + } + + if (Directory.Exists(token)) + { + if (TryFindSupportedFile(token, out var filePath, out var errorMessage)) + { + return new FileInfo(filePath); + } + else if (required) + { + result.ErrorMessage = errorMessage; + return default!; + } + else + { + return default!; + } + } + + result.ErrorMessage = $"The file '{token}' could not be found."; + return default!; + } + } +} diff --git a/src/tye/Program.DeployCommand.cs b/src/tye/Program.DeployCommand.cs index 1e323d01..748a4ec1 100644 --- a/src/tye/Program.DeployCommand.cs +++ b/src/tye/Program.DeployCommand.cs @@ -13,26 +13,22 @@ namespace Tye { public static Command CreateDeployCommand() { - var command = new Command("deploy", "Deploy the application"); - - var argument = new Argument("path") + var command = new Command("deploy", "Deploy the application") { - Description = "A solution or project file to generate a yaml manifest from", - Arity = ArgumentArity.ZeroOrOne + CommonArguments.Path_Required, + StandardOptions.Interactive, + StandardOptions.Verbosity, }; - command.Add(argument); - command.Add(StandardOptions.Interactive); - command.Add(StandardOptions.Verbosity); - - command.Handler = CommandHandler.Create((console, path, verbosity, interactive) => + command.Handler = CommandHandler.Create((console, path, verbosity, interactive) => { - var application = ResolveApplication(path); - if (application is null) + // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 + if (path is null) { - throw new CommandException($"None of the supported files were found (tye.yaml, .csproj, .fsproj, .sln)"); + throw new CommandException("No project or solution file was found."); } + var application = ResolveApplication(path); return ExecuteAsync(new OutputContext(console, verbosity), application, environment: "production", interactive); }); diff --git a/src/tye/Program.InitCommand.cs b/src/tye/Program.InitCommand.cs index 7ca586f0..2f80a543 100644 --- a/src/tye/Program.InitCommand.cs +++ b/src/tye/Program.InitCommand.cs @@ -16,22 +16,17 @@ namespace Tye { var command = new Command("init", "create a yaml manifest") { + CommonArguments.Path_Optional, }; - var argument = new Argument("path") + command.Handler = CommandHandler.Create((console, path) => { - Description = "A solution or project file to generate a yaml manifest from", - Arity = ArgumentArity.ZeroOrOne - }; - - command.AddArgument(argument); - - command.Handler = CommandHandler.Create((console, path) => - { - if (File.Exists("tye.yaml")) + if (path is FileInfo && + path.Exists && + (string.Equals(".yml", path.Extension, StringComparison.OrdinalIgnoreCase) || + string.Equals(".yaml", path.Extension, StringComparison.OrdinalIgnoreCase))) { - console.Out.WriteLine("\"tye.yaml\" already exists."); - return; + throw new CommandException($"File '{path.FullName}' already exists."); } var template = @"- name: app @@ -46,39 +41,32 @@ namespace Tye # - port: 8080 # number port of the binding "; - try + if (path is FileInfo && path.Exists) { var application = ResolveApplication(path); - if (application is object) - { - var serializer = new SerializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) - .Build(); + var serializer = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) + .Build(); - var extension = Path.GetExtension(application.Source).ToLowerInvariant(); - var directory = Path.GetDirectoryName(application.Source); - var descriptions = application.Services.Select(s => s.Value.Description).ToList(); + var extension = path.Extension.ToLowerInvariant(); + var directory = path.Directory; + var descriptions = application.Services.Select(s => s.Value.Description).ToList(); - // Clear all bindings if any for solutions and project files - if (extension == ".sln" || extension == ".csproj" || extension == ".fsproj") + // Clear all bindings if any for solutions and project files + if (extension == ".sln" || extension == ".csproj" || extension == ".fsproj") + { + foreach (var d in descriptions) { - foreach (var d in descriptions) - { - d.Bindings = null; - d.Replicas = null; - d.Build = null; - d.Configuration = null; - d.Project = d.Project.Substring(directory.Length).TrimStart(Path.DirectorySeparatorChar); - } + d.Bindings = null; + d.Replicas = null; + d.Build = null; + d.Configuration = null; + d.Project = d.Project.Substring(directory.FullName.Length).TrimStart(Path.DirectorySeparatorChar); } - - template = serializer.Serialize(descriptions); } - } - catch (FileNotFoundException) - { - // No file found, just generate a new one + + template = serializer.Serialize(descriptions); } File.WriteAllText("tye.yaml", template); @@ -88,66 +76,29 @@ namespace Tye return command; } - private static Application? ResolveApplication(string? path) + private static Application ResolveApplication(FileInfo file) { - if (string.IsNullOrEmpty(path)) - { - path = ResolveFileFromDirectory(Directory.GetCurrentDirectory()); - } - else if (Directory.Exists(path)) + if (!file.Exists) { - path = ResolveFileFromDirectory(Path.GetFullPath(path)); + throw new FileNotFoundException($"File '{file.FullName}' does not exist"); } - if (path == null) - { - return null; - } - - if (!File.Exists(path)) - { - throw new FileNotFoundException($"{path} does not exist"); - } - - switch (Path.GetExtension(path).ToLower()) + switch (file.Extension.ToLower()) { case ".yaml": case ".yml": - return Application.FromYaml(path); + return Application.FromYaml(file.FullName); case ".csproj": case ".fsproj": - return Application.FromProject(path); + return Application.FromProject(file.FullName); case ".sln": - return Application.FromSolution(path); + return Application.FromSolution(file.FullName); default: - throw new NotSupportedException($"{path} not supported"); + throw new NotSupportedException($"File '{file.FullName}' is not a supported format."); } } - - private static string? ResolveFileFromDirectory(string basePath) - { - var formats = new[] { "tye.yaml", "tye.yml", "*.csproj", "*.fsproj", "*.sln" }; - - foreach (var format in formats) - { - var files = Directory.GetFiles(basePath, format); - if (files.Length == 0) - { - continue; - } - - if (files.Length > 1) - { - throw new InvalidOperationException($"Ambiguous match found {string.Join(", ", files.Select(Path.GetFileName))}"); - } - - return files[0]; - } - - return null; - } } } diff --git a/src/tye/Program.RunCommand.cs b/src/tye/Program.RunCommand.cs index b1ddd224..4a11162e 100644 --- a/src/tye/Program.RunCommand.cs +++ b/src/tye/Program.RunCommand.cs @@ -1,6 +1,7 @@ using System; using System.CommandLine; using System.CommandLine.Invocation; +using System.IO; using Micronetes.Hosting; namespace Tye @@ -11,12 +12,7 @@ namespace Tye { var command = new Command("run", "run the application") { - }; - - var argument = new Argument("path") - { - Description = "A file or directory to execute. Supports a project files, solution files or a yaml manifest.", - Arity = ArgumentArity.ZeroOrOne + CommonArguments.Path_Required, }; // TODO: We'll need to support a --build-args @@ -53,16 +49,15 @@ namespace Tye Required = false }); - command.AddArgument(argument); - - command.Handler = CommandHandler.Create((console, path) => + command.Handler = CommandHandler.Create((console, path) => { - var application = ResolveApplication(path); - if (application is null) + // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 + if (path is null) { - throw new CommandException($"None of the supported files were found (tye.yaml, .csproj, .fsproj, .sln)"); + throw new CommandException("No project or solution file was found."); } + var application = ResolveApplication(path); return MicronetesHost.RunAsync(application, args); }); diff --git a/src/tye/Program.cs b/src/tye/Program.cs index bf903c56..d452b9c6 100644 --- a/src/tye/Program.cs +++ b/src/tye/Program.cs @@ -5,6 +5,7 @@ using System.CommandLine.Help; using System.CommandLine.Invocation; using System.CommandLine.IO; using System.CommandLine.Parsing; +using System.Reflection; using System.Threading.Tasks; namespace Tye @@ -48,6 +49,12 @@ namespace Tye context.Console.ResetTerminalForegroundColor(); context.Console.SetTerminalForegroundColor(ConsoleColor.Red); + if (exception is TargetInvocationException tie && + tie.InnerException is object) + { + exception = tie.InnerException; + } + if (exception is OperationCanceledException) { context.Console.Error.WriteLine("Oh dear! Operation canceled.");