From cb482adbadd9d7fb7a5131928ef8259c983b4728 Mon Sep 17 00:00:00 2001 From: Arne Peirs Date: Wed, 5 Aug 2020 18:06:22 +0200 Subject: [PATCH] Update CommandLineParser --- Cmdline/Action/AuthToken.cs | 268 ++++----- Cmdline/Action/Available.cs | 51 +- Cmdline/Action/Cache.cs | 333 +++++------ Cmdline/Action/Compare.cs | 94 ++-- Cmdline/Action/Compat.cs | 352 ++++++------ Cmdline/Action/GameInstance.cs | 877 +++++++++++++---------------- Cmdline/Action/ICommand.cs | 15 +- Cmdline/Action/ISubCommand.cs | 21 +- Cmdline/Action/Import.cs | 124 ++-- Cmdline/Action/Install.cs | 334 +++++------ Cmdline/Action/List.cs | 130 +++-- Cmdline/Action/Mark.cs | 243 ++++---- Cmdline/Action/Prompt.cs | 48 +- Cmdline/Action/Remove.cs | 144 ++--- Cmdline/Action/Repair.cs | 124 ++-- Cmdline/Action/Replace.cs | 196 ++++--- Cmdline/Action/Repo.cs | 457 +++++++-------- Cmdline/Action/Search.cs | 265 +++++---- Cmdline/Action/Show.cs | 237 ++++---- Cmdline/Action/Update.cs | 143 +++-- Cmdline/Action/Upgrade.cs | 217 +++---- Cmdline/CKAN-cmdline.csproj | 5 +- Cmdline/ConsoleUser.cs | 200 ++++--- Cmdline/Exit.cs | 10 - Cmdline/Main.cs | 485 ++++++++-------- Cmdline/Options.cs | 544 ++++-------------- Cmdline/ParserExtensions.cs | 93 +++ Cmdline/ProgressReporter.cs | 34 -- Cmdline/Properties/AssemblyInfo.cs | 5 +- Core/Exit.cs | 23 + Core/Types/Kraken.cs | 11 + Netkan/CKAN-netkan.csproj | 2 +- Netkan/CmdLineOptions.cs | 21 +- Netkan/Program.cs | 108 ++-- 34 files changed, 3044 insertions(+), 3170 deletions(-) delete mode 100644 Cmdline/Exit.cs create mode 100644 Cmdline/ParserExtensions.cs delete mode 100644 Cmdline/ProgressReporter.cs create mode 100644 Core/Exit.cs diff --git a/Cmdline/Action/AuthToken.cs b/Cmdline/Action/AuthToken.cs index d5bd9ff60c..ba3eeb6aaf 100644 --- a/Cmdline/Action/AuthToken.cs +++ b/Cmdline/Action/AuthToken.cs @@ -3,180 +3,190 @@ using Autofac; using CKAN.Configuration; using CommandLine; -using CommandLine.Text; -using log4net; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { /// - /// Subcommand for managing authentication tokens + /// Class for managing authentication tokens. /// public class AuthToken : ISubCommand { - /// - /// Initialize the subcommand - /// - public AuthToken() { } + private GameInstanceManager _manager; + private IUser _user; /// - /// Run the subcommand + /// Run the 'authtoken' command. /// - /// Manager to provide game instances - /// Command line parameters paritally handled by parser - /// Command line parameters not yet handled by parser - /// - /// Exit code - /// - public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCommandOptions unparsed) + /// + public int RunCommand(GameInstanceManager manager, object args) { - string[] args = unparsed.options.ToArray(); - int exitCode = Exit.OK; + var s = args.ToString(); + var opts = s.Replace(s.Substring(0, s.LastIndexOf('.') + 1), "").Split('+'); + + CommonOptions options = new CommonOptions(); + _user = new ConsoleUser(options.Headless); + _manager = manager ?? new GameInstanceManager(_user); + var exitCode = options.Handle(_manager, _user); + + if (exitCode != Exit.Ok) + return exitCode; - Parser.Default.ParseArgumentsStrict(args, new AuthTokenSubOptions(), (string option, object suboptions) => + switch (opts[1]) { - if (!string.IsNullOrEmpty(option) && suboptions != null) - { - CommonOptions options = (CommonOptions)suboptions; - options.Merge(opts); - user = new ConsoleUser(options.Headless); - if (manager == null) - { - manager = new GameInstanceManager(user); - } - exitCode = options.Handle(manager, user); - if (exitCode == Exit.OK) - { - switch (option) - { - case "list": - exitCode = listAuthTokens(options); - break; - case "add": - exitCode = addAuthToken((AddAuthTokenOptions)options); - break; - case "remove": - exitCode = removeAuthToken((RemoveAuthTokenOptions)options); - break; - } - } - } - }, () => { exitCode = MainClass.AfterHelp(); }); + case "AddAuthToken": + exitCode = AddAuthToken(args); + break; + case "ListAuthToken": + exitCode = ListAuthTokens(); + break; + case "RemoveAuthToken": + exitCode = RemoveAuthToken(args); + break; + default: + exitCode = Exit.BadOpt; + break; + } + return exitCode; } - private int listAuthTokens(CommonOptions opts) + /// + public string GetUsage(string prefix, string[] args) { - List hosts = new List(ServiceLocator.Container.Resolve().GetAuthTokenHosts()); - if (hosts.Count > 0) + if (args.Length == 1) + return $"{prefix} {args[0]} [options]"; + + switch (args[1]) { - int longestHostLen = hostHeader.Length; - int longestTokenLen = tokenHeader.Length; - foreach (string host in hosts) - { - longestHostLen = Math.Max(longestHostLen, host.Length); - string token; - if (ServiceLocator.Container.Resolve().TryGetAuthToken(host, out token)) - { - longestTokenLen = Math.Max(longestTokenLen, token.Length); - } - } - // Create format string: {0,-longestHostLen} {1,-longestTokenLen} - string fmt = string.Format("{0}0,-{2}{1} {0}1,-{3}{1}", - "{", "}", longestHostLen, longestTokenLen); - user.RaiseMessage(fmt, hostHeader, tokenHeader); - user.RaiseMessage(fmt, - new string('-', longestHostLen), - new string('-', longestTokenLen) - ); - foreach (string host in hosts) - { - string token; - if (ServiceLocator.Container.Resolve().TryGetAuthToken(host, out token)) - { - user.RaiseMessage(fmt, host, token); - } - } + case "add": + return $"{prefix} {args[0]} {args[1]} [options] "; + case "list": + return $"{prefix} {args[0]} {args[1]} [options]"; + case "remove": + return $"{prefix} {args[0]} {args[1]} [options] "; + default: + return $"{prefix} {args[0]} [options]"; } - return Exit.OK; } - private int addAuthToken(AddAuthTokenOptions opts) + private int AddAuthToken(object args) { - if (Uri.CheckHostName(opts.host) != UriHostNameType.Unknown) + var opts = (AuthTokenOptions.AddAuthToken)args; + if (opts.Host == null || opts.Token == null) { - ServiceLocator.Container.Resolve().SetAuthToken(opts.host, opts.token); + _user.RaiseMessage("add - argument(s) missing, perhaps you forgot it?"); + return Exit.BadOpt; + } + + if (Uri.CheckHostName(opts.Host) != UriHostNameType.Unknown) + { + ServiceLocator.Container.Resolve().SetAuthToken(opts.Host, opts.Token); + _user.RaiseMessage("Successfully added \"{0}\".", opts.Host); } else { - user.RaiseError("Invalid host name: {0}", opts.host); + _user.RaiseMessage("Invalid host name."); + return Exit.BadOpt; } - return Exit.OK; + + return Exit.Ok; } - private int removeAuthToken(RemoveAuthTokenOptions opts) + private int ListAuthTokens() { - ServiceLocator.Container.Resolve().SetAuthToken(opts.host, null); - return Exit.OK; - } + const string hostHeader = "Host"; + const string tokenHeader = "Token"; - private const string hostHeader = "Host"; - private const string tokenHeader = "Token"; + var hosts = new List(ServiceLocator.Container.Resolve().GetAuthTokenHosts()); + if (hosts.Count > 0) + { + var hostWidth = hostHeader.Length; + var tokenWidth = tokenHeader.Length; + foreach (var host in hosts) + { + hostWidth = Math.Max(hostWidth, host.Length); + if (ServiceLocator.Container.Resolve().TryGetAuthToken(host, out string token) && token != null) + { + tokenWidth = Math.Max(tokenWidth, token.Length); + } + } - private IUser user; - private static readonly ILog log = LogManager.GetLogger(typeof(AuthToken)); - } + _user.RaiseMessage("{0} {1}", + hostHeader.PadRight(hostWidth), + tokenHeader.PadRight(tokenWidth) + ); - internal class AuthTokenSubOptions : VerbCommandOptions - { - [VerbOption("list", HelpText = "List auth tokens")] - public CommonOptions ListOptions { get; set; } + _user.RaiseMessage("{0} {1}", + new string('-', hostWidth), + new string('-', tokenWidth) + ); - [VerbOption("add", HelpText = "Add an auth token")] - public AddAuthTokenOptions AddOptions { get; set; } + foreach (var host in hosts) + { + if (ServiceLocator.Container.Resolve().TryGetAuthToken(host, out string token)) + { + _user.RaiseMessage("{0} {1}", + host.PadRight(hostWidth), + token.PadRight(tokenWidth) + ); + } + } + } - [VerbOption("remove", HelpText = "Delete an auth token")] - public RemoveAuthTokenOptions RemoveOptions { get; set; } + return Exit.Ok; + } - [HelpVerbOption] - public string GetUsage(string verb) + private int RemoveAuthToken(object args) { - HelpText ht = HelpText.AutoBuild(this, verb); - // Add a usage prefix line - ht.AddPreOptionsLine(" "); - if (string.IsNullOrEmpty(verb)) + var opts = (AuthTokenOptions.RemoveAuthToken)args; + if (opts.Host == null) { - ht.AddPreOptionsLine("ckan authtoken - Manage authentication tokens"); - ht.AddPreOptionsLine($"Usage: ckan authtoken [options]"); + _user.RaiseMessage("remove - argument missing, perhaps you forgot it?"); + return Exit.BadOpt; + } + + var hosts = new List(ServiceLocator.Container.Resolve().GetAuthTokenHosts()); + if (hosts.Contains(opts.Host)) + { + ServiceLocator.Container.Resolve().SetAuthToken(opts.Host, null); + _user.RaiseMessage("Successfully removed \"{0}\".", opts.Host); } else { - ht.AddPreOptionsLine("authtoken " + verb + " - " + GetDescription(verb)); - switch (verb) - { - case "add": - ht.AddPreOptionsLine($"Usage: ckan authtoken {verb} [options] host token"); - break; - case "remove": - ht.AddPreOptionsLine($"Usage: ckan authtoken {verb} [options] host"); - break; - case "list": - ht.AddPreOptionsLine($"Usage: ckan authtoken {verb} [options]"); - break; - } + _user.RaiseMessage("There is no host with the name \"{0}\".", opts.Host); + _user.RaiseMessage("Use 'ckan authtoken list' to view a list of hosts."); + return Exit.BadOpt; } - return ht; + + return Exit.Ok; } } - internal class AddAuthTokenOptions : CommonOptions + [Verb("authtoken", HelpText = "Manage authentication tokens")] + [ChildVerbs(typeof(AddAuthToken), typeof(ListAuthToken), typeof(RemoveAuthToken))] + internal class AuthTokenOptions { - [ValueOption(0)] public string host { get; set; } - [ValueOption(1)] public string token { get; set; } - } + [VerbExclude] + [Verb("add", HelpText = "Add an authentication token")] + internal class AddAuthToken : CommonOptions + { + [Value(0, MetaName = "Host", HelpText = "The host (DNS / IP) to authenticate with")] + public string Host { get; set; } - internal class RemoveAuthTokenOptions : CommonOptions - { - [ValueOption(0)] public string host { get; set; } - } + [Value(1, MetaName = "Token", HelpText = "The token to authenticate with")] + public string Token { get; set; } + } + + [VerbExclude] + [Verb("list", HelpText = "List authentication tokens")] + internal class ListAuthToken : CommonOptions { } + [VerbExclude] + [Verb("remove", HelpText = "Remove an authentication token")] + internal class RemoveAuthToken : CommonOptions + { + [Value(0, MetaName = "Host", HelpText = "The host (DNS / IP) to remove")] + public string Host { get; set; } + } + } } diff --git a/Cmdline/Action/Available.cs b/Cmdline/Action/Available.cs index ae445fe6d9..3fba5d2bb2 100644 --- a/Cmdline/Action/Available.cs +++ b/Cmdline/Action/Available.cs @@ -1,45 +1,62 @@ using System.Linq; -using System.Collections.Generic; +using CommandLine; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { + /// + /// Class for listing the available mods. + /// public class Available : ICommand { - public IUser user { get; set; } + private readonly IUser _user; + /// + /// Initializes a new instance of the class. + /// + /// The current to raise messages to the user. public Available(IUser user) { - this.user = user; + _user = user; } - public int RunCommand(CKAN.GameInstance ksp, object raw_options) + /// + /// Run the 'available' command. + /// + /// + public int RunCommand(CKAN.GameInstance inst, object args) { - AvailableOptions opts = (AvailableOptions)raw_options; - IRegistryQuerier registry = RegistryManager.Instance(ksp).registry; - + var opts = (AvailableOptions)args; + IRegistryQuerier registry = RegistryManager.Instance(inst).registry; + var compatible = registry - .CompatibleModules(ksp.VersionCriteria()) + .CompatibleModules(inst.VersionCriteria()) .Where(m => !m.IsDLC); - user.RaiseMessage("Modules compatible with KSP {0}", ksp.Version()); - user.RaiseMessage(""); + _user.RaiseMessage("Mods compatible with {0} {1}\r\n", inst.game.ShortName, inst.Version()); - if (opts.detail) + if (opts.Detail) { - foreach (CkanModule module in compatible) + foreach (var module in compatible) { - user.RaiseMessage("* {0} ({1}) - {2} - {3}", module.identifier, module.version, module.name, module.@abstract); + _user.RaiseMessage("* {0} ({1}) - {2} - {3}", module.identifier, module.version, module.name, module.@abstract); } } else { - foreach (CkanModule module in compatible) + foreach (var module in compatible) { - user.RaiseMessage("* {0} ({1}) - {2}", module.identifier, module.version, module.name); + _user.RaiseMessage("* {0} ({1}) - {2}", module.identifier, module.version, module.name); } } - return Exit.OK; + return Exit.Ok; } } + + [Verb("available", HelpText = "List available mods")] + internal class AvailableOptions : InstanceSpecificOptions + { + [Option("detail", HelpText = "Shows a short description of each mod")] + public bool Detail { get; set; } + } } diff --git a/Cmdline/Action/Cache.cs b/Cmdline/Action/Cache.cs index 5463709664..613fb4bad7 100644 --- a/Cmdline/Action/Cache.cs +++ b/Cmdline/Action/Cache.cs @@ -1,244 +1,209 @@ -using CommandLine; -using CommandLine.Text; -using log4net; -using CKAN.Configuration; using Autofac; +using CKAN.Configuration; +using CommandLine; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { + /// + /// Class for managing the CKAN cache. + /// public class Cache : ISubCommand { - public Cache() { } + private GameInstanceManager _manager; + private IUser _user; - private class CacheSubOptions : VerbCommandOptions + /// + /// Run the 'cache' command. + /// + /// + public int RunCommand(GameInstanceManager manager, object args) { - [VerbOption("list", HelpText = "List the download cache path")] - public CommonOptions ListOptions { get; set; } + var s = args.ToString(); + var opts = s.Replace(s.Substring(0, s.LastIndexOf('.') + 1), "").Split('+'); - [VerbOption("set", HelpText = "Set the download cache path")] - public SetOptions SetOptions { get; set; } + CommonOptions options = new CommonOptions(); + _user = new ConsoleUser(options.Headless); + _manager = manager ?? new GameInstanceManager(_user); + var exitCode = options.Handle(_manager, _user); - [VerbOption("clear", HelpText = "Clear the download cache directory")] - public CommonOptions ClearOptions { get; set; } + if (exitCode != Exit.Ok) + return exitCode; - [VerbOption("reset", HelpText = "Set the download cache path to the default")] - public CommonOptions ResetOptions { get; set; } + switch (opts[1]) + { + case "ClearCache": + exitCode = ClearCacheDirectory(); + break; + case "ListCache": + exitCode = ListCacheDirectory(); + break; + case "ResetCache": + exitCode = ResetCacheDirectory(); + break; + case "SetCache": + exitCode = SetCacheDirectory(args); + break; + case "SetCacheLimit": + exitCode = SetCacheSizeLimit(args); + break; + case "ShowCacheLimit": + exitCode = ShowCacheSizeLimit(); + break; + default: + exitCode = Exit.BadOpt; + break; + } - [VerbOption("showlimit", HelpText = "Show the cache size limit")] - public CommonOptions ShowLimitOptions { get; set; } + return exitCode; + } - [VerbOption("setlimit", HelpText = "Set the cache size limit")] - public SetLimitOptions SetLimitOptions { get; set; } + /// + public string GetUsage(string prefix, string[] args) + { + if (args.Length == 1) + return $"{prefix} {args[0]} [options]"; - [HelpVerbOption] - public string GetUsage(string verb) + switch (args[1]) { - HelpText ht = HelpText.AutoBuild(this, verb); - // Add a usage prefix line - ht.AddPreOptionsLine(" "); - if (string.IsNullOrEmpty(verb)) - { - ht.AddPreOptionsLine("ckan cache - Manage the download cache path of CKAN"); - ht.AddPreOptionsLine($"Usage: ckan cache [options]"); - } - else - { - ht.AddPreOptionsLine("cache " + verb + " - " + GetDescription(verb)); - switch (verb) - { - // First the commands with one string argument - case "set": - ht.AddPreOptionsLine($"Usage: ckan cache {verb} [options] path"); - break; - case "setlimit": - ht.AddPreOptionsLine($"Usage: ckan cache {verb} [options] megabytes"); - break; - - // Now the commands with only --flag type options - case "list": - case "clear": - case "reset": - case "showlimit": - default: - ht.AddPreOptionsLine($"Usage: ckan cache {verb} [options]"); - break; - } - } - return ht; + case "set": + return $"{prefix} {args[0]} {args[1]} [options] "; + case "setlimit": + return $"{prefix} {args[0]} {args[1]} [options] "; + case "clear": + case "list": + case "reset": + case "showlimit": + return $"{prefix} {args[0]} {args[1]} [options]"; + default: + return $"{prefix} {args[0]} [options]"; } } - private class SetOptions : CommonOptions + private int ClearCacheDirectory() { - [ValueOption(0)] - public string Path { get; set; } + _manager.Cache.RemoveAll(); + _user.RaiseMessage("Cleared download cache."); + return Exit.Ok; } - private class SetLimitOptions : CommonOptions + private int ListCacheDirectory() { - [ValueOption(0)] - public long Megabytes { get; set; } = -1; + var cfg = ServiceLocator.Container.Resolve(); + _user.RaiseMessage("Download cache is set to \"{0}\".", cfg.DownloadCacheDir); + PrintCacheInfo(); + return Exit.Ok; } - /// - /// Execute a cache subcommand - /// - /// GameInstanceManager object containing our instances and cache - /// Command line options object - /// Raw command line options - /// - /// Exit code for shell environment - /// - public int RunSubCommand(GameInstanceManager mgr, CommonOptions opts, SubCommandOptions unparsed) + private int ResetCacheDirectory() { - string[] args = unparsed.options.ToArray(); - - int exitCode = Exit.OK; - // Parse and process our sub-verbs - Parser.Default.ParseArgumentsStrict(args, new CacheSubOptions(), (string option, object suboptions) => + if (_manager.TrySetupCache("", out string failReason)) { - // ParseArgumentsStrict calls us unconditionally, even with bad arguments - if (!string.IsNullOrEmpty(option) && suboptions != null) - { - CommonOptions options = (CommonOptions)suboptions; - options.Merge(opts); - user = new ConsoleUser(options.Headless); - manager = mgr ?? new GameInstanceManager(user); - exitCode = options.Handle(manager, user); - if (exitCode != Exit.OK) - return; - - switch (option) - { - case "list": - exitCode = ListCacheDirectory((CommonOptions)suboptions); - break; - - case "set": - exitCode = SetCacheDirectory((SetOptions)suboptions); - break; - - case "clear": - exitCode = ClearCacheDirectory((CommonOptions)suboptions); - break; - - case "reset": - exitCode = ResetCacheDirectory((CommonOptions)suboptions); - break; - - case "showlimit": - exitCode = ShowCacheSizeLimit((CommonOptions)suboptions); - break; - - case "setlimit": - exitCode = SetCacheSizeLimit((SetLimitOptions)suboptions); - break; - - default: - user.RaiseMessage("Unknown command: cache {0}", option); - exitCode = Exit.BADOPT; - break; - } - } - }, () => { exitCode = MainClass.AfterHelp(); }); - return exitCode; - } + var cfg = ServiceLocator.Container.Resolve(); + _user.RaiseMessage("Download cache reset to \"{0}\".", cfg.DownloadCacheDir); + PrintCacheInfo(); + } + else + { + _user.RaiseError("Can't reset cache path: {0}.", failReason); + return Exit.Error; + } - private int ListCacheDirectory(CommonOptions options) - { - IConfiguration cfg = ServiceLocator.Container.Resolve(); - user.RaiseMessage(cfg.DownloadCacheDir); - printCacheInfo(); - return Exit.OK; + return Exit.Ok; } - private int SetCacheDirectory(SetOptions options) + private int SetCacheDirectory(object args) { - if (string.IsNullOrEmpty(options.Path)) + var opts = (CacheOptions.SetCache)args; + if (opts.Path == null) { - user.RaiseError("set - argument missing, perhaps you forgot it?"); - return Exit.BADOPT; + _user.RaiseMessage("set - argument missing, perhaps you forgot it?"); + return Exit.BadOpt; } - string failReason; - if (manager.TrySetupCache(options.Path, out failReason)) + if (_manager.TrySetupCache(opts.Path, out string failReason)) { - IConfiguration cfg = ServiceLocator.Container.Resolve(); - user.RaiseMessage($"Download cache set to {cfg.DownloadCacheDir}"); - printCacheInfo(); - return Exit.OK; + var cfg = ServiceLocator.Container.Resolve(); + _user.RaiseMessage("Download cache set to \"{0}\".", cfg.DownloadCacheDir); + PrintCacheInfo(); } else { - user.RaiseError($"Invalid path: {failReason}"); - return Exit.BADOPT; + _user.RaiseError("Invalid path: {0}.", failReason); + return Exit.Error; } - } - private int ClearCacheDirectory(CommonOptions options) - { - manager.Cache.RemoveAll(); - user.RaiseMessage("Download cache cleared."); - printCacheInfo(); - return Exit.OK; + return Exit.Ok; } - private int ResetCacheDirectory(CommonOptions options) + private int SetCacheSizeLimit(object args) { - string failReason; - if (manager.TrySetupCache("", out failReason)) + var opts = (CacheOptions.SetCacheLimit)args; + var cfg = ServiceLocator.Container.Resolve(); + if (opts.Megabytes < 0) { - IConfiguration cfg = ServiceLocator.Container.Resolve(); - user.RaiseMessage($"Download cache reset to {cfg.DownloadCacheDir}"); - printCacheInfo(); + cfg.CacheSizeLimit = null; } else { - user.RaiseError($"Can't reset cache path: {failReason}"); + cfg.CacheSizeLimit = opts.Megabytes * 1024 * 1024; } - return Exit.OK; + + ShowCacheSizeLimit(); + return Exit.Ok; } - private int ShowCacheSizeLimit(CommonOptions options) + private int ShowCacheSizeLimit() { - IConfiguration cfg = ServiceLocator.Container.Resolve(); - if (cfg.CacheSizeLimit.HasValue) - { - user.RaiseMessage(CkanModule.FmtSize(cfg.CacheSizeLimit.Value)); - } - else - { - user.RaiseMessage("Unlimited"); - } - return Exit.OK; + var cfg = ServiceLocator.Container.Resolve(); + var limit = cfg.CacheSizeLimit.HasValue + ? CkanModule.FmtSize(cfg.CacheSizeLimit.Value) + : "Unlimited"; + + _user.RaiseMessage("Cache limit set to {0}.", limit); + return Exit.Ok; } - private int SetCacheSizeLimit(SetLimitOptions options) + private void PrintCacheInfo() { - IConfiguration cfg = ServiceLocator.Container.Resolve(); - if (options.Megabytes < 0) - { - cfg.CacheSizeLimit = null; - } - else - { - cfg.CacheSizeLimit = options.Megabytes * (long)1024 * (long)1024; - } - return ShowCacheSizeLimit(null); + _manager.Cache.GetSizeInfo(out int fileCount, out long bytes); + _user.RaiseMessage("Cache currently has {0} files that use {1}.", fileCount, CkanModule.FmtSize(bytes)); } + } + + [Verb("cache", HelpText = "Manage download cache path")] + [ChildVerbs(typeof(ClearCache), typeof(ListCache), typeof(ResetCache), typeof(SetCache), typeof(SetCacheLimit), typeof(ShowCacheLimit))] + internal class CacheOptions + { + [VerbExclude] + [Verb("clear", HelpText = "Clear the download cache directory")] + internal class ClearCache : CommonOptions { } + + [VerbExclude] + [Verb("list", HelpText = "List the download cache path")] + internal class ListCache : CommonOptions { } - private void printCacheInfo() + [VerbExclude] + [Verb("reset", HelpText = "Set the download cache path to the default")] + internal class ResetCache : CommonOptions { } + + [VerbExclude] + [Verb("set", HelpText = "Set the download cache path")] + internal class SetCache : CommonOptions { - int fileCount; - long bytes; - manager.Cache.GetSizeInfo(out fileCount, out bytes); - user.RaiseMessage($"{fileCount} files, {CkanModule.FmtSize(bytes)}"); + [Value(0, MetaName = "Path", HelpText = "The path to set the download cache to")] + public string Path { get; set; } } - private GameInstanceManager manager; - private IUser user; + [VerbExclude] + [Verb("setlimit", HelpText = "Set the cache size limit")] + internal class SetCacheLimit : CommonOptions + { + [Value(0, MetaName = "MB", HelpText = "The max amount of MB the download cache stores files")] + public long Megabytes { get; set; } = -1; + } - private static readonly ILog log = LogManager.GetLogger(typeof(Cache)); + [VerbExclude] + [Verb("showlimit", HelpText = "Show the cache size limit")] + internal class ShowCacheLimit : CommonOptions { } } - } diff --git a/Cmdline/Action/Compare.cs b/Cmdline/Action/Compare.cs index 328358a8a7..a5f5b94f7a 100644 --- a/Cmdline/Action/Compare.cs +++ b/Cmdline/Action/Compare.cs @@ -1,61 +1,73 @@ using CKAN.Versioning; +using CommandLine; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { - // Does not need an instance, so this is not an ICommand - public class Compare + /// + /// Class for comparing version strings. + /// + public class Compare : ICommand { - private IUser user; + private readonly IUser _user; + /// + /// Initializes a new instance of the class. + /// + /// The current to raise messages to the user. public Compare(IUser user) { - this.user = user; + _user = user; } - public int RunCommand(object rawOptions) + /// + /// Run the 'compare' command. + /// + /// + public int RunCommand(CKAN.GameInstance inst, object args) { - var options = (CompareOptions)rawOptions; + var opts = (CompareOptions)args; + if (string.IsNullOrWhiteSpace(opts.Left) || string.IsNullOrWhiteSpace(opts.Right)) + { + _user.RaiseMessage("compare - argument(s) missing, perhaps you forgot it?"); + return Exit.BadOpt; + } + + var leftVersion = new ModuleVersion(opts.Left); + var rightVersion = new ModuleVersion(opts.Right); + + var compareResult = leftVersion.CompareTo(rightVersion); - if (options.Left != null && options.Right != null) + if (opts.MachineReadable) { - var leftVersion = new ModuleVersion(options.Left); - var rightVersion = new ModuleVersion(options.Right); - - int compareResult = leftVersion.CompareTo(rightVersion); - - if (options.machine_readable) - { - user.RaiseMessage(compareResult.ToString()); - } - else if (compareResult == 0) - { - user.RaiseMessage( - "\"{0}\" and \"{1}\" are the same versions.", leftVersion, rightVersion); - } - else if (compareResult < 0) - { - user.RaiseMessage( - "\"{0}\" is lower than \"{1}\".", leftVersion, rightVersion); - } - else if (compareResult > 0) - { - user.RaiseMessage( - "\"{0}\" is higher than \"{1}\".", leftVersion, rightVersion); - } - else - { - user.RaiseMessage( - "Usage: ckan compare version1 version2"); - } + _user.RaiseMessage(compareResult.ToString()); + } + else if (compareResult == 0) + { + _user.RaiseMessage("\"{0}\" and \"{1}\" are the same versions.", leftVersion, rightVersion); + } + else if (compareResult < 0) + { + _user.RaiseMessage("\"{0}\" is lower than \"{1}\".", leftVersion, rightVersion); } else { - user.RaiseMessage( - "Usage: ckan compare version1 version2"); - return Exit.BADOPT; + _user.RaiseMessage("\"{0}\" is higher than \"{1}\".", leftVersion, rightVersion); } - return Exit.OK; + return Exit.Ok; } } + + [Verb("compare", HelpText = "Compare version strings")] + internal class CompareOptions : CommonOptions + { + [Option("machine-readable", HelpText = "Output in a machine readable format: -1, 0 or 1")] + public bool MachineReadable { get; set; } + + [Value(0, MetaName = "version1", HelpText = "The first version to compare")] + public string Left { get; set; } + + [Value(1, MetaName = "version2", HelpText = "The second version to compare")] + public string Right { get; set; } + } } diff --git a/Cmdline/Action/Compat.cs b/Cmdline/Action/Compat.cs index 785b82fe6c..8fa9c0c882 100644 --- a/Cmdline/Action/Compat.cs +++ b/Cmdline/Action/Compat.cs @@ -1,206 +1,212 @@ using System.Linq; using CKAN.Versioning; using CommandLine; -using CommandLine.Text; namespace CKAN.CmdLine.Action { + /// + /// Class for managing KSP version compatibility. + /// public class Compat : ISubCommand { - public Compat() { } + private GameInstanceManager _manager; + private IUser _user; + + /// + /// Run the 'compat' command. + /// + /// + public int RunCommand(GameInstanceManager manager, object args) + { + var s = args.ToString(); + var opts = s.Replace(s.Substring(0, s.LastIndexOf('.') + 1), "").Split('+'); + + CommonOptions options = new CommonOptions(); + _user = new ConsoleUser(options.Headless); + _manager = manager ?? new GameInstanceManager(_user); + var exitCode = options.Handle(_manager, _user); + + if (exitCode != Exit.Ok) + return exitCode; + + switch (opts[1]) + { + case "AddCompat": + exitCode = AddCompatibility(args); + break; + case "ForgetCompat": + exitCode = ForgetCompatibility(args); + break; + case "ListCompat": + exitCode = ListCompatibility(); + break; + default: + exitCode = Exit.BadOpt; + break; + } - public class CompatOptions : VerbCommandOptions + return exitCode; + } + + /// + public string GetUsage(string prefix, string[] args) { - [VerbOption("list", HelpText = "List compatible KSP versions")] - public CompatListOptions List { get; set; } + if (args.Length == 1) + return $"{prefix} {args[0]} [options]"; - [VerbOption("add", HelpText = "Add version to KSP compatibility list")] - public CompatAddOptions Add { get; set; } + switch (args[1]) + { + case "add": + case "forget": + return $"{prefix} {args[0]} {args[1]} [options] "; + case "list": + return $"{prefix} {args[0]} {args[1]} [options]"; + default: + return $"{prefix} {args[0]} [options]"; + } + } - [VerbOption("forget", HelpText = "Forget version on KSP compatibility list")] - public CompatForgetOptions Forget { get; set; } + private int AddCompatibility(object args) + { + var opts = (CompatOptions.AddCompat)args; + if (opts.Version == null) + { + _user.RaiseMessage("add - argument missing, perhaps you forgot it?"); + return Exit.BadOpt; + } - [HelpVerbOption] - public string GetUsage(string verb) + var inst = MainClass.GetGameInstance(_manager); + if (GameVersion.TryParse(opts.Version, out GameVersion gameVersion)) + { + var newCompatibleVersion = inst.GetCompatibleVersions(); + newCompatibleVersion.Add(gameVersion); + inst.SetCompatibleVersions(newCompatibleVersion); + _user.RaiseMessage("Successfully added \"{0}\".", gameVersion); + } + else + { + _user.RaiseError("Invalid KSP version."); + return Exit.Error; + } + + return Exit.Ok; + } + + private int ForgetCompatibility(object args) + { + var opts = (CompatOptions.ForgetCompat)args; + if (opts.Version == null) { - HelpText ht = HelpText.AutoBuild(this, verb); - // Add a usage prefix line - ht.AddPreOptionsLine(" "); - if (string.IsNullOrEmpty(verb)) + _user.RaiseMessage("forget - argument missing, perhaps you forgot it?"); + return Exit.BadOpt; + } + + var inst = MainClass.GetGameInstance(_manager); + if (GameVersion.TryParse(opts.Version, out GameVersion gameVersion)) + { + if (gameVersion != inst.Version()) { - ht.AddPreOptionsLine("ckan compat - Manage KSP version compatibility"); - ht.AddPreOptionsLine($"Usage: ckan compat [options]"); + var newCompatibleVersion = inst.GetCompatibleVersions(); + newCompatibleVersion.RemoveAll(i => i == gameVersion); + inst.SetCompatibleVersions(newCompatibleVersion); + _user.RaiseMessage("Successfully removed \"{0}\".", gameVersion); } else { - ht.AddPreOptionsLine("compat " + verb + " - " + GetDescription(verb)); - switch (verb) - { - // First the commands with one string argument - case "add": - case "forget": - ht.AddPreOptionsLine($"Usage: ckan compat {verb} [options] version"); - break; - - // Now the commands with only --flag type options - case "list": - default: - ht.AddPreOptionsLine($"Usage: ckan compat {verb} [options]"); - break; - } + _user.RaiseError("Cannot forget actual KSP version."); + return Exit.Error; } - return ht; } - } + else + { + _user.RaiseError("Invalid KSP version."); + return Exit.Error; + } - public class CompatListOptions : InstanceSpecificOptions { } + return Exit.Ok; + } - public class CompatAddOptions : InstanceSpecificOptions + private int ListCompatibility() { - [ValueOption(0)] public string Version { get; set; } + const string versionHeader = "Version"; + const string actualHeader = "Actual"; + + var inst = MainClass.GetGameInstance(_manager); + + var output = inst + .GetCompatibleVersions() + .Select(i => new + { + Version = i, + Actual = false + }) + .ToList(); + + output.Add(new + { + Version = inst.Version(), + Actual = true + }); + + output = output + .OrderByDescending(i => i.Actual) + .ThenByDescending(i => i.Version) + .ToList(); + + var versionWidth = Enumerable + .Repeat(versionHeader, 1) + .Concat(output.Select(i => i.Version.ToString())) + .Max(i => i.Length); + + var actualWidth = Enumerable + .Repeat(actualHeader, 1) + .Concat(output.Select(i => i.Actual.ToString())) + .Max(i => i.Length); + + _user.RaiseMessage("{0} {1}", + versionHeader.PadRight(versionWidth), + actualHeader.PadRight(actualWidth) + ); + + _user.RaiseMessage("{0} {1}", + new string('-', versionWidth), + new string('-', actualWidth) + ); + + foreach (var line in output) + { + _user.RaiseMessage("{0} {1}", + line.Version.ToString().PadRight(versionWidth), + line.Actual.ToString().PadRight(actualWidth) + ); + } + + return Exit.Ok; } + } - public class CompatForgetOptions : InstanceSpecificOptions + [Verb("compat", HelpText = "Manage KSP version compatibility")] + [ChildVerbs(typeof(AddCompat), typeof(ForgetCompat), typeof(ListCompat))] + internal class CompatOptions + { + [VerbExclude] + [Verb("add", HelpText = "Add version to KSP compatibility list")] + internal class AddCompat : InstanceSpecificOptions { - [ValueOption(0)] public string Version { get; set; } + [Value(0, MetaName = "KSP version", HelpText = "The KSP version to add as compatible")] + public string Version { get; set; } } - public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCommandOptions options) + [VerbExclude] + [Verb("forget", HelpText = "Forget version on KSP compatibility list")] + internal class ForgetCompat : InstanceSpecificOptions { - var exitCode = Exit.OK; - - Parser.Default.ParseArgumentsStrict(options.options.ToArray(), new CompatOptions(), (string option, object suboptions) => - { - // ParseArgumentsStrict calls us unconditionally, even with bad arguments - if (!string.IsNullOrEmpty(option) && suboptions != null) - { - CommonOptions comOpts = (CommonOptions)suboptions; - comOpts.Merge(opts); - _user = new ConsoleUser(comOpts.Headless); - _kspManager = manager ?? new GameInstanceManager(_user); - exitCode = comOpts.Handle(_kspManager, _user); - if (exitCode != Exit.OK) - return; - - switch (option) - { - case "list": - { - var ksp = MainClass.GetGameInstance(_kspManager); - - const string versionHeader = "Version"; - const string actualHeader = "Actual"; - - var output = ksp - .GetCompatibleVersions() - .Select(i => new - { - Version = i, - Actual = false - }) - .ToList(); - - output.Add(new - { - Version = ksp.Version(), - Actual = true - }); - - output = output - .OrderByDescending(i => i.Actual) - .ThenByDescending(i => i.Version) - .ToList(); - - var versionWidth = Enumerable - .Repeat(versionHeader, 1) - .Concat(output.Select(i => i.Version.ToString())) - .Max(i => i.Length); - - var actualWidth = Enumerable - .Repeat(actualHeader, 1) - .Concat(output.Select(i => i.Actual.ToString())) - .Max(i => i.Length); - - const string columnFormat = "{0} {1}"; - - _user.RaiseMessage(string.Format(columnFormat, - versionHeader.PadRight(versionWidth), - actualHeader.PadRight(actualWidth) - )); - - _user.RaiseMessage(string.Format(columnFormat, - new string('-', versionWidth), - new string('-', actualWidth) - )); - - foreach (var line in output) - { - _user.RaiseMessage(string.Format(columnFormat, - line.Version.ToString().PadRight(versionWidth), - line.Actual.ToString().PadRight(actualWidth) - )); - } - } - break; - - case "add": - { - var ksp = MainClass.GetGameInstance(_kspManager); - var addOptions = (CompatAddOptions)suboptions; - - GameVersion GameVersion; - if (GameVersion.TryParse(addOptions.Version, out GameVersion)) - { - var newCompatibleVersion = ksp.GetCompatibleVersions(); - newCompatibleVersion.Add(GameVersion); - ksp.SetCompatibleVersions(newCompatibleVersion); - } - else - { - _user.RaiseError("ERROR: Invalid KSP version."); - exitCode = Exit.ERROR; - } - } - break; - - case "forget": - { - var ksp = MainClass.GetGameInstance(_kspManager); - var addOptions = (CompatForgetOptions)suboptions; - - GameVersion GameVersion; - if (GameVersion.TryParse(addOptions.Version, out GameVersion)) - { - if (GameVersion != ksp.Version()) - { - var newCompatibleVersion = ksp.GetCompatibleVersions(); - newCompatibleVersion.RemoveAll(i => i == GameVersion); - ksp.SetCompatibleVersions(newCompatibleVersion); - } - else - { - _user.RaiseError("ERROR: Cannot forget actual KSP version."); - exitCode = Exit.ERROR; - } - } - else - { - _user.RaiseError("ERROR: Invalid KSP version."); - exitCode = Exit.ERROR; - } - } - break; - - default: - exitCode = Exit.BADOPT; - break; - } - } - }, () => { exitCode = MainClass.AfterHelp(); }); - return exitCode; + [Value(0, MetaName = "KSP version", HelpText = "The KSP version to remove as compatible")] + public string Version { get; set; } } - private GameInstanceManager _kspManager; - private IUser _user; + [VerbExclude] + [Verb("list", HelpText = "List compatible KSP versions")] + internal class ListCompat : InstanceSpecificOptions { } } } diff --git a/Cmdline/Action/GameInstance.cs b/Cmdline/Action/GameInstance.cs index 5112d76a50..8272a6d78b 100644 --- a/Cmdline/Action/GameInstance.cs +++ b/Cmdline/Action/GameInstance.cs @@ -2,663 +2,596 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using CKAN.DLC; +using CKAN.Games; +using CKAN.Versioning; using CommandLine; -using CommandLine.Text; using log4net; -using CKAN.Versioning; -using CKAN.Games; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { + /// + /// Class for managing KSP installs. + /// public class GameInstance : ISubCommand { - public GameInstance() { } - protected static readonly ILog log = LogManager.GetLogger(typeof(GameInstance)); - - internal class InstanceSubOptions : VerbCommandOptions - { - [VerbOption("list", HelpText = "List game instances")] - public CommonOptions ListOptions { get; set; } - - [VerbOption("add", HelpText = "Add a game instance")] - public AddOptions AddOptions { get; set; } - - [VerbOption("clone", HelpText = "Clone an existing game instance")] - public CloneOptions CloneOptions { get; set; } - - [VerbOption("rename", HelpText = "Rename a game instance")] - public RenameOptions RenameOptions { get; set; } - - [VerbOption("forget", HelpText = "Forget a game instance")] - public ForgetOptions ForgetOptions { get; set; } - - [VerbOption("default", HelpText = "Set the default game instance")] - public DefaultOptions DefaultOptions { get; set; } - - [VerbOption("fake", HelpText = "Fake a game instance")] - public FakeOptions FakeOptions { get; set; } - - [HelpVerbOption] - public string GetUsage(string verb) - { - HelpText ht = HelpText.AutoBuild(this, verb); - // Add a usage prefix line - ht.AddPreOptionsLine(" "); - if (string.IsNullOrEmpty(verb)) - { - ht.AddPreOptionsLine("ckan instance - Manage game instances"); - ht.AddPreOptionsLine($"Usage: ckan instance [options]"); - } - else - { - ht.AddPreOptionsLine("instance " + verb + " - " + GetDescription(verb)); - switch (verb) - { - // First the commands with three string arguments - case "fake": - ht.AddPreOptionsLine($"Usage: ckan instance {verb} [options] name path version [--MakingHistory ] [--BreakingGround ]"); - break; - - case "clone": - ht.AddPreOptionsLine($"Usage: ckan instance {verb} [options] instanceNameOrPath newname newpath"); - break; - - // Second the commands with two string arguments - case "add": - ht.AddPreOptionsLine($"Usage: ckan instance {verb} [options] name url"); - break; - case "rename": - ht.AddPreOptionsLine($"Usage: ckan instance {verb} [options] oldname newname"); - break; - - // Now the commands with one string argument - case "remove": - case "forget": - case "use": - case "default": - ht.AddPreOptionsLine($"Usage: ckan instance {verb} [options] name"); - break; + private static readonly ILog Log = LogManager.GetLogger(typeof(GameInstance)); - // Now the commands with only --flag type options - case "list": - default: - ht.AddPreOptionsLine($"Usage: ckan instance {verb} [options]"); - break; + private GameInstanceManager _manager; + private IUser _user; - } - } - return ht; - } - } - - internal class AddOptions : CommonOptions - { - [ValueOption(0)] public string name { get; set; } - [ValueOption(1)] public string path { get; set; } - } - - internal class CloneOptions : CommonOptions - { - [ValueOption(0)] public string nameOrPath { get; set; } - [ValueOption(1)] public string new_name { get; set; } - [ValueOption(2)] public string new_path { get; set; } - } - - internal class RenameOptions : CommonOptions - { - [ValueOption(0)] public string old_name { get; set; } - [ValueOption(1)] public string new_name { get; set; } - } - - internal class ForgetOptions : CommonOptions - { - [ValueOption(0)] public string name { get; set; } - } - - internal class DefaultOptions : CommonOptions - { - [ValueOption(0)] public string name { get; set; } - } - - internal class FakeOptions : CommonOptions - { - [ValueOption(0)] public string name { get; set; } - [ValueOption(1)] public string path { get; set; } - [ValueOption(2)] public string version { get; set; } - - [Option("MakingHistory", DefaultValue = "none", HelpText = "The version of the Making History DLC to be faked.")] - public string makingHistoryVersion { get; set; } - [Option("BreakingGround", DefaultValue = "none", HelpText = "The version of the Breaking Ground DLC to be faked.")] - public string breakingGroundVersion { get; set; } - - [Option("set-default", DefaultValue = false, HelpText = "Set the new instance as the default one.")] - public bool setDefault { get; set; } - } - - // This is required by ISubCommand - public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCommandOptions unparsed) + /// + /// Run the 'ksp' command. + /// + /// + public int RunCommand(GameInstanceManager manager, object args) { - string[] args = unparsed.options.ToArray(); - - #region Aliases - - for (int i = 0; i < args.Length; i++) - { - switch (args[i]) - { - case "use": - args[i] = "default"; - break; - - default: - break; - } + var s = args.ToString(); + var opts = s.Replace(s.Substring(0, s.LastIndexOf('.') + 1), "").Split('+'); + + CommonOptions options = new CommonOptions(); + _user = new ConsoleUser(options.Headless); + _manager = manager ?? new GameInstanceManager(_user); + var exitCode = options.Handle(_manager, _user); + + if (exitCode != Exit.Ok) + return exitCode; + + switch (opts[1]) + { + case "AddKsp": + exitCode = AddInstall(args); + break; + case "CloneKsp": + exitCode = CloneInstall(args); + break; + case "DefaultKsp": + exitCode = SetDefaultInstall(args); + break; + case "FakeKsp": + exitCode = FakeNewKspInstall(args); + break; + case "ForgetKsp": + exitCode = ForgetInstall(args); + break; + case "ListKsp": + exitCode = ListInstalls(); + break; + case "RenameKsp": + exitCode = RenameInstall(args); + break; + default: + exitCode = Exit.BadOpt; + break; } - #endregion - - int exitCode = Exit.OK; - // Parse and process our sub-verbs - Parser.Default.ParseArgumentsStrict(args, new InstanceSubOptions(), (string option, object suboptions) => - { - // ParseArgumentsStrict calls us unconditionally, even with bad arguments - if (!string.IsNullOrEmpty(option) && suboptions != null) - { - CommonOptions options = (CommonOptions)suboptions; - options.Merge(opts); - User = new ConsoleUser(options.Headless); - Manager = manager ?? new GameInstanceManager(User); - exitCode = options.Handle(Manager, User); - if (exitCode != Exit.OK) - return; - - switch (option) - { - case "list": - exitCode = ListInstalls(); - break; - - case "add": - exitCode = AddInstall((AddOptions)suboptions); - break; - - case "clone": - exitCode = CloneInstall((CloneOptions)suboptions); - break; - - case "rename": - exitCode = RenameInstall((RenameOptions)suboptions); - break; - - case "forget": - exitCode = ForgetInstall((ForgetOptions)suboptions); - break; - - case "use": - case "default": - exitCode = SetDefaultInstall((DefaultOptions)suboptions); - break; - - case "fake": - exitCode = FakeNewGameInstance((FakeOptions)suboptions); - break; - - default: - User.RaiseMessage("Unknown command: instance {0}", option); - exitCode = Exit.BADOPT; - break; - } - } - }, () => { exitCode = MainClass.AfterHelp(); }); return exitCode; } - private GameInstanceManager Manager { get; set; } - private IUser User { get; set; } - - #region option functions - - private int ListInstalls() + /// + public string GetUsage(string prefix, string[] args) { - var output = Manager.Instances - .OrderByDescending(i => i.Value.Name == Manager.AutoStartInstance) - .ThenByDescending(i => i.Value.Version() ?? GameVersion.Any) - .ThenBy(i => i.Key) - .Select(i => new - { - Name = i.Key, - Version = i.Value.Version()?.ToString() ?? "", - Default = i.Value.Name == Manager.AutoStartInstance ? "Yes" : "No", - Path = i.Value.GameDir() - }) - .ToList(); - - const string nameHeader = "Name"; - const string versionHeader = "Version"; - const string defaultHeader = "Default"; - const string pathHeader = "Path"; - - var nameWidth = Enumerable.Repeat(nameHeader, 1).Concat(output.Select(i => i.Name)).Max(i => i.Length); - var versionWidth = Enumerable.Repeat(versionHeader, 1).Concat(output.Select(i => i.Version)).Max(i => i.Length); - var defaultWidth = Enumerable.Repeat(defaultHeader, 1).Concat(output.Select(i => i.Default)).Max(i => i.Length); - var pathWidth = Enumerable.Repeat(pathHeader, 1).Concat(output.Select(i => i.Path)).Max(i => i.Length); - - const string columnFormat = "{0} {1} {2} {3}"; - - User.RaiseMessage(string.Format(columnFormat, - nameHeader.PadRight(nameWidth), - versionHeader.PadRight(versionWidth), - defaultHeader.PadRight(defaultWidth), - pathHeader.PadRight(pathWidth) - )); - - User.RaiseMessage(string.Format(columnFormat, - new string('-', nameWidth), - new string('-', versionWidth), - new string('-', defaultWidth), - new string('-', pathWidth) - )); - - foreach (var line in output) - { - User.RaiseMessage(string.Format(columnFormat, - line.Name.PadRight(nameWidth), - line.Version.PadRight(versionWidth), - line.Default.PadRight(defaultWidth), - line.Path.PadRight(pathWidth) - )); + if (args.Length == 1) + return $"{prefix} {args[0]} [options]"; + + switch (args[1]) + { + case "add": + return $"{prefix} {args[0]} {args[1]} [options] "; + case "clone": + return $"{prefix} {args[0]} {args[1]} [options] "; + case "fake": + return $"{prefix} {args[0]} {args[1]} [options] [--MakingHistory ] [--BreakingGround ]"; + case "default": + case "forget": + return $"{prefix} {args[0]} {args[1]} [options] "; + case "list": + return $"{prefix} {args[0]} {args[1]} [options]"; + case "rename": + return $"{prefix} {args[0]} {args[1]} [options] "; + default: + return $"{prefix} {args[0]} [options]"; } - - return Exit.OK; } - private int AddInstall(AddOptions options) + private int AddInstall(object args) { - if (options.name == null || options.path == null) + var opts = (KspOptions.AddKsp)args; + if (opts.Name == null || opts.Path == null) { - User.RaiseMessage("add - argument missing, perhaps you forgot it?"); - return Exit.BADOPT; + _user.RaiseMessage("add - argument(s) missing, perhaps you forgot it?"); + return Exit.BadOpt; } - if (Manager.HasInstance(options.name)) + if (_manager.HasInstance(opts.Name)) { - User.RaiseMessage("Install with name \"{0}\" already exists, aborting..", options.name); - return Exit.BADOPT; + _user.RaiseMessage("Install with the name \"{0}\" already exists, aborting...", opts.Name); + return Exit.BadOpt; } try { - string path = options.path; - Manager.AddInstance(path, options.name, User); - User.RaiseMessage("Added \"{0}\" with root \"{1}\" to known installs", options.name, options.path); - return Exit.OK; + _manager.AddInstance(opts.Path, opts.Name, _user); + _user.RaiseMessage("Added \"{0}\" with root \"{1}\" to known installs.", opts.Name, opts.Path); } - catch (NotKSPDirKraken ex) + catch (NotKSPDirKraken kraken) { - User.RaiseMessage("Sorry, {0} does not appear to be a game instance", ex.path); - return Exit.BADOPT; + _user.RaiseMessage("Sorry, \"{0}\" does not appear to be a KSP directory.", kraken.path); + return Exit.Error; } + + return Exit.Ok; } - private int CloneInstall(CloneOptions options) + private int CloneInstall(object args) { - if (options.nameOrPath == null || options.new_name == null || options.new_path == null) + var opts = (KspOptions.CloneKsp)args; + if (opts.NameOrPath == null || opts.NewName == null || opts.NewPath == null) { - User.RaiseMessage("instance clone - argument(s) missing"); - return Exit.BADOPT; + _user.RaiseMessage("clone - argument(s) missing, perhaps you forgot it?"); + return Exit.BadOpt; } // Parse all options - string instanceNameOrPath = options.nameOrPath; - string newName = options.new_name; - string newPath = options.new_path; - - - log.Info("Cloning the game instance: " + options.nameOrPath); + var nameOrPath = opts.NameOrPath; + var newName = opts.NewName; + var newPath = opts.NewPath; + Log.InfoFormat("Cloning the KSP instance \"{0}\".", nameOrPath); try { - // Try instanceNameOrPath as name and search the registry for it. - if (Manager.HasInstance(instanceNameOrPath)) + // Try nameOrPath as name and search the registry for it + if (_manager.HasInstance(nameOrPath)) { - CKAN.GameInstance[] listOfInstances = Manager.Instances.Values.ToArray(); - foreach (CKAN.GameInstance instance in listOfInstances) + var listOfInstances = _manager.Instances.Values.ToArray(); + foreach (var instance in listOfInstances) { - if (instance.Name == instanceNameOrPath) + if (instance.Name == nameOrPath) { - // Found it, now clone it. - Manager.CloneInstance(instance, newName, newPath); + // Found it, now clone it + _manager.CloneInstance(instance, newName, newPath); break; } } } - // Try to use instanceNameOrPath as a path and create a new game instance. - // If it's valid, go on. - else if (Manager.InstanceAt(instanceNameOrPath, newName) is CKAN.GameInstance instance && instance.Valid) + // Try to use nameOrPath as a path and create a new game object + else if (_manager.InstanceAt(nameOrPath, newName) is CKAN.GameInstance instance && instance.Valid) { - Manager.CloneInstance(instance, newName, newPath); + _manager.CloneInstance(instance, newName, newPath); } - // There is no instance with this name or at this path. + // There is no instance with this name or at this path else { - throw new NoGameInstanceKraken(); + throw new NoGameInstanceKraken(nameOrPath); } } catch (NotKSPDirKraken kraken) { - // Two possible reasons: - // First: The instance to clone is not a valid game instance. - // Only occurs if user manipulated directory and deleted files/folders - // which CKAN searches for in validity test. + // There are two possible reasons: + // First: The instance to clone is not a valid KSP instance + // This only occurs if the user manipulated the directory and deleted + // files/folders which CKAN searches for in the validity test - // Second: Something went wrong adding the new instance to the registry, - // most likely because the newly created directory is not valid. + // Second: Something went wrong with adding the new instance to the registry, + // most likely because the newly created directory is not valid - log.Error(kraken); - return Exit.ERROR; + _user.RaiseError("The specified instance \"{0}\" is not a valid KSP instance.\r\n {1}", nameOrPath, kraken.path); + return Exit.Error; } catch (PathErrorKraken kraken) { // The new path is not empty - // The kraken contains a message to inform the user. - log.Error(kraken.Message + kraken.path); - return Exit.ERROR; + _user.RaiseError("The directory to clone to is not empty.\r\n {0}", kraken.path); + return Exit.Error; } - catch (IOException e) + catch (IOException ex) { - // Something went wrong copying the files. Contains a message. - log.Error(e); - return Exit.ERROR; + // Something went wrong while copying the files + _user.RaiseError(ex.ToString()); + return Exit.Error; } catch (NoGameInstanceKraken) { - User.RaiseError(String.Format("No instance with this name or at this path: {0}\n See below for a list of known instances:\n", instanceNameOrPath)); + // Did not find a known game instance + _user.RaiseError("No instance found with this name or at this path: \"{0}\".\r\nSee below for a list of known instances:\r\n", nameOrPath); ListInstalls(); - return Exit.ERROR; + return Exit.Error; } catch (InstanceNameTakenKraken kraken) { - User.RaiseError("This instance name is already taken: {0}", kraken.instName); - return Exit.BADOPT; + // Instance name already exists + _user.RaiseError("An instance with the name \"{0}\" already exists.", kraken.instName); + return Exit.BadOpt; } - // Test if the instance was added to the registry. + // Test if the instance was added to the registry // No need to test if valid, because this is done in AddInstance(), - // so if something went wrong, HasInstance is false. - if (Manager.HasInstance(newName)) - { - return Exit.OK; - } - else + // so if something went wrong, HasInstance is false + if (!_manager.HasInstance(newName)) { - User.RaiseMessage("Something went wrong. Please look if the new directory has been created.\n", - "Try to add the new instance manually with \"ckan instance add\".\n"); - return Exit.ERROR; - } - } - - private int RenameInstall(RenameOptions options) - { - if (options.old_name == null || options.new_name == null) - { - User.RaiseMessage("rename - argument missing, perhaps you forgot it?"); - return Exit.BADOPT; - } - - if (!Manager.HasInstance(options.old_name)) - { - User.RaiseMessage("Couldn't find install with name \"{0}\", aborting..", options.old_name); - return Exit.BADOPT; - } - - Manager.RenameInstance(options.old_name, options.new_name); - - User.RaiseMessage("Successfully renamed \"{0}\" to \"{1}\"", options.old_name, options.new_name); - return Exit.OK; - } - - private int ForgetInstall(ForgetOptions options) - { - if (options.name == null) - { - User.RaiseMessage("forget - argument missing, perhaps you forgot it?"); - return Exit.BADOPT; - } - - if (!Manager.HasInstance(options.name)) - { - User.RaiseMessage("Couldn't find install with name \"{0}\", aborting..", options.name); - return Exit.BADOPT; + _user.RaiseMessage("Something went wrong. Please look if the new directory has been created.\r\nTry to add the new instance manually with 'ckan ksp add'."); + return Exit.Error; } - Manager.RemoveInstance(options.name); - - User.RaiseMessage("Successfully removed \"{0}\"", options.name); - return Exit.OK; + _user.RaiseMessage("Successfully cloned the instance \"{0}\" into \"{1}\".", nameOrPath, newPath); + return Exit.Ok; } - private int SetDefaultInstall(DefaultOptions options) + private int SetDefaultInstall(object args) { - string name = options.name; + var opts = (KspOptions.DefaultKsp)args; + var name = opts.Name; if (name == null) { - // No input argument from the user. Present a list of the possible instances. - string message = "default - argument missing, please select from the list below."; - - // Check if there is a default instance. - string defaultInstance = Manager.Configuration.AutoStartInstance; - int defaultInstancePresent = 0; + // Check if there is a default instance + var defaultInstance = _manager.Configuration.AutoStartInstance; + var defaultInstancePresent = 0; - if (!String.IsNullOrWhiteSpace(defaultInstance)) + if (!string.IsNullOrWhiteSpace(defaultInstance)) { defaultInstancePresent = 1; } - object[] keys = new object[Manager.Instances.Count + defaultInstancePresent]; + var keys = new object[_manager.Instances.Count + defaultInstancePresent]; - // Populate the list of instances. - for (int i = 0; i < Manager.Instances.Count; i++) + // Populate the list of instances + for (var i = 0; i < _manager.Instances.Count; i++) { - var instance = Manager.Instances.ElementAt(i); + var instance = _manager.Instances.ElementAt(i); - keys[i + defaultInstancePresent] = String.Format("\"{0}\" - {1}", instance.Key, instance.Value.GameDir()); + keys[i + defaultInstancePresent] = string.Format("\"{0}\" - {1}", instance.Key, instance.Value.GameDir()); } - // Mark the default instance for the user. - if (!String.IsNullOrWhiteSpace(defaultInstance)) + // Mark the default instance for the user + if (!string.IsNullOrWhiteSpace(defaultInstance)) { - keys[0] = Manager.Instances.IndexOfKey(defaultInstance); + keys[0] = _manager.Instances.IndexOfKey(defaultInstance); } int result; - try { - result = User.RaiseSelectionDialog(message, keys); + // No input argument from the user. Present a list of the possible instances + var message = "default - argument missing, please select an instance from the list below."; + result = _user.RaiseSelectionDialog(message, keys); } catch (Kraken) { - return Exit.BADOPT; + return Exit.BadOpt; } if (result < 0) { - return Exit.BADOPT; + return Exit.BadOpt; } - name = Manager.Instances.ElementAt(result).Key; + name = _manager.Instances.ElementAt(result).Key; } - if (!Manager.Instances.ContainsKey(name)) + if (!_manager.Instances.ContainsKey(name)) { - User.RaiseMessage("Couldn't find install with name \"{0}\", aborting..", name); - return Exit.BADOPT; + _user.RaiseMessage("Couldn't find an install with the name \"{0}\", aborting...", name); + return Exit.BadOpt; } try { - Manager.SetAutoStart(name); + _manager.SetAutoStart(name); } - catch (NotKSPDirKraken k) + catch (NotKSPDirKraken kraken) { - User.RaiseMessage("Sorry, {0} does not appear to be a game instance", k.path); - return Exit.BADOPT; + _user.RaiseMessage("Sorry, \"{0}\" does not appear to be a KSP directory.", kraken.path); + return Exit.Error; } - User.RaiseMessage("Successfully set \"{0}\" as the default game instance", name); - return Exit.OK; + _user.RaiseMessage("Successfully set \"{0}\" as the default KSP installation.", name); + return Exit.Ok; } - /// - /// Creates a new fake game instance after the conditions CKAN tests for valid install directories. - /// Used for developing and testing purposes. - /// - private int FakeNewGameInstance(FakeOptions options) + private int FakeNewKspInstall(object args) { - int error() + var opts = (KspOptions.FakeKsp)args; + if (opts.Name == null || opts.Path == null || opts.Version == null) { - log.Debug("Instance faking failed, see console output for details."); - User.RaiseMessage("--Error--"); - return Exit.ERROR; + _user.RaiseMessage("fake [--MakingHistory ] [--BreakingGround ] - argument(s) missing, perhaps you forgot it?"); + return Exit.BadOpt; } - int badArgument() + // opts.Version is "none" if the DLC shouldn't be simulated + var dlcs = new Dictionary(); + if (opts.MakingHistory != null && opts.MakingHistory.ToLower() != "none") { - log.Debug("Instance faking failed: bad argument(s). See console output for details."); - User.RaiseMessage("--Error: bad argument(s)--"); - return Exit.BADOPT; - } - - - if (options.name == null || options.path == null || options.version == null) - { - User.RaiseMessage("instance fake " + - "[--MakingHistory ] [--BreakingGround ] - argument(s) missing"); - return badArgument(); - } - - log.Debug("Parsing arguments..."); - // Parse all options - string installName = options.name; - string path = options.path; - GameVersion version; - bool setDefault = options.setDefault; - - // options.Version is "none" if the DLC should not be simulated. - Dictionary dlcs = new Dictionary(); - if (options.makingHistoryVersion != null && options.makingHistoryVersion.ToLower() != "none") - { - if (GameVersion.TryParse(options.makingHistoryVersion, out GameVersion ver)) + if (GameVersion.TryParse(opts.MakingHistory, out GameVersion ver)) { - dlcs.Add(new DLC.MakingHistoryDlcDetector(), ver); + dlcs.Add(new MakingHistoryDlcDetector(), ver); } else { - User.RaiseError("Please check the Making History DLC version argument - Format it like Maj.Min.Patch - e.g. 1.1.0"); - return badArgument(); + _user.RaiseError("Please check the Making History DLC version argument - Format it like Maj.Min.Patch - e.g. 1.1.0"); + return Exit.BadOpt; } } - if (options.breakingGroundVersion != null && options.breakingGroundVersion.ToLower() != "none") + + if (opts.BreakingGround != null && opts.BreakingGround.ToLower() != "none") { - if (GameVersion.TryParse(options.breakingGroundVersion, out GameVersion ver)) + if (GameVersion.TryParse(opts.BreakingGround, out GameVersion ver)) { - dlcs.Add(new DLC.BreakingGroundDlcDetector(), ver); + dlcs.Add(new BreakingGroundDlcDetector(), ver); } else { - User.RaiseError("Please check the Breaking Ground DLC version argument - Format it like Maj.Min.Patch - e.g. 1.1.0"); - return badArgument(); + _user.RaiseError("Please check the Breaking Ground DLC version argument - Format it like Maj.Min.Patch - e.g. 1.1.0"); + return Exit.BadOpt; } } - // Parse the choosen game version + // Parse all options + var name = opts.Name; + var path = opts.Path; + var setDefault = opts.SetDefault; + GameVersion version; + try { - version = GameVersion.Parse(options.version); + version = GameVersion.Parse(opts.Version); } catch (FormatException) { - // Thrown if there is anything besides numbers and points in the version string or a different syntactic error. - User.RaiseError("Please check the version argument - Format it like Maj.Min.Patch[.Build] - e.g. 1.6.0 or 1.2.2.1622"); - return badArgument(); + // Thrown if there is anything besides numbers and points in the version string or a different syntactic error + _user.RaiseError("Please check the version argument - Format it like Maj.Min.Patch[.Build] - e.g. 1.6.0 or 1.2.2.1622"); + return Exit.BadOpt; } - // Get the full version including build number. + // Get the full version including build number try { - version = version.RaiseVersionSelectionDialog(new KerbalSpaceProgram(), User); + version = version.RaiseVersionSelectionDialog(new KerbalSpaceProgram(), _user); } catch (BadGameVersionKraken) { - User.RaiseError("Couldn't find a valid game version for your input.\n" + - "Make sure to enter the at least the version major and minor values in the form Maj.Min - e.g. 1.5"); - return badArgument(); + _user.RaiseError("Couldn't find a valid KSP version for your input.\r\nMake sure to enter at least the version major and minor values in the form Maj.Min - e.g. 1.5"); + return Exit.Error; } catch (CancelledActionKraken) { - User.RaiseError("Selection cancelled! Please call 'ckan instance fake' again."); - return error(); + _user.RaiseError("Selection cancelled! Please call 'ckan ksp fake' again."); + return Exit.Error; } - User.RaiseMessage(String.Format("Creating new fake game instance {0} at {1} with version {2}", installName, path, version.ToString())); - log.Debug("Faking instance..."); + _user.RaiseMessage("Creating a new fake KSP install with the name \"{0}\" at \"{1}\" with version \"{2}\".", name, path, version); + Log.Debug("Faking instance..."); try { - // Pass all arguments to CKAN.GameInstanceManager.FakeInstance() and create a new one. - Manager.FakeInstance(new KerbalSpaceProgram(), installName, path, version, dlcs); + // Pass all arguments to CKAN.GameInstanceManager.FakeInstance() and create a new one + _manager.FakeInstance(new KerbalSpaceProgram(), name, path, version, dlcs); if (setDefault) { - User.RaiseMessage("Setting new instance to default..."); - Manager.SetAutoStart(installName); + _user.RaiseMessage("Setting new instance to default..."); + _manager.SetAutoStart(name); } } catch (InstanceNameTakenKraken kraken) { - User.RaiseError("This instance name is already taken: {0}", kraken.instName); - return badArgument(); + // Instance name already exists + _user.RaiseError("An instance with the name \"{0}\" already exists.", kraken.instName); + return Exit.BadOpt; } - catch (BadInstallLocationKraken kraken) + catch (BadInstallLocationKraken) { - // The folder exists and is not empty. - User.RaiseError(kraken.Message); - return badArgument(); + // The folder exists but is not empty + _user.RaiseError("The directory to clone to is not empty.\r\n {0}", path); + return Exit.Error; } catch (WrongGameVersionKraken kraken) { - // Thrown because the specified game instance is too old for one of the selected DLCs. - User.RaiseError(kraken.Message); - return badArgument(); + // Thrown because the specified KSP version is too old for one of the selected DLCs + _user.RaiseError(kraken.Message); + return Exit.Error; } catch (NotKSPDirKraken kraken) { // Something went wrong adding the new instance to the registry, - // most likely because the newly created directory is somehow not valid. - log.Error(kraken); - return error(); + // most likely because the newly created directory is somehow not valid + _user.RaiseError("The specified instance \"{0}\" is not a valid KSP instance.\r\n {1}", name, kraken.path); + return Exit.Error; } catch (InvalidKSPInstanceKraken) { - // Thrown by Manager.SetAutoStart() if Manager.HasInstance returns false. + // Thrown by Manager.SetAutoStart() if Manager.HasInstance returns false // Will be checked again down below with a proper error message } + // Test if the instance was added to the registry + // No need to test if valid, because this is done in AddInstance(), + // so if something went wrong, HasInstance is false + if (!_manager.HasInstance(name)) + { + _user.RaiseMessage("Something went wrong. Please look if the new directory has been created.\r\nTry to add the new instance manually with 'ckan ksp add'."); + return Exit.Error; + } + + _user.RaiseMessage("Successfully faked the instance \"{0}\" into \"{1}\".", name, path); + return Exit.Ok; + } - // Test if the instance was added to the registry. - // No need to test if valid, because this is done in AddInstance(). - if (Manager.HasInstance(installName)) + private int ForgetInstall(object args) + { + var opts = (KspOptions.ForgetKsp)args; + if (opts.Name == null) { - User.RaiseMessage("--Done--"); - return Exit.OK; + _user.RaiseMessage("forget - argument missing, perhaps you forgot it?"); + return Exit.BadOpt; } - else + + if (!_manager.HasInstance(opts.Name)) { - User.RaiseError("Something went wrong. Try to add the instance yourself with \"ckan instance add\".", - "Also look if the new directory has been created."); - return error(); + _user.RaiseMessage("Couldn't find an install with the name \"{0}\", aborting...", opts.Name); + return Exit.BadOpt; } + + _manager.RemoveInstance(opts.Name); + + _user.RaiseMessage("Successfully removed \"{0}\".", opts.Name); + return Exit.Ok; + } + + private int ListInstalls() + { + const string nameHeader = "Name"; + const string versionHeader = "Version"; + const string defaultHeader = "Default"; + const string pathHeader = "Path"; + + var output = _manager.Instances + .OrderByDescending(i => i.Value.Name == _manager.AutoStartInstance) + .ThenByDescending(i => i.Value.Version() ?? GameVersion.Any) + .ThenBy(i => i.Key) + .Select(i => new + { + Name = i.Key, + Version = i.Value.Version()?.ToString() ?? "", + Default = i.Value.Name == _manager.AutoStartInstance ? "Yes" : "No", + Path = i.Value.GameDir() + }) + .ToList(); + + var nameWidth = Enumerable.Repeat(nameHeader, 1).Concat(output.Select(i => i.Name)).Max(i => i.Length); + var versionWidth = Enumerable.Repeat(versionHeader, 1).Concat(output.Select(i => i.Version)).Max(i => i.Length); + var defaultWidth = Enumerable.Repeat(defaultHeader, 1).Concat(output.Select(i => i.Default)).Max(i => i.Length); + var pathWidth = Enumerable.Repeat(pathHeader, 1).Concat(output.Select(i => i.Path)).Max(i => i.Length); + + _user.RaiseMessage("{0} {1} {2} {3}", + nameHeader.PadRight(nameWidth), + versionHeader.PadRight(versionWidth), + defaultHeader.PadRight(defaultWidth), + pathHeader.PadRight(pathWidth) + ); + + _user.RaiseMessage("{0} {1} {2} {3}", + new string('-', nameWidth), + new string('-', versionWidth), + new string('-', defaultWidth), + new string('-', pathWidth) + ); + + foreach (var line in output) + { + _user.RaiseMessage("{0} {1} {2} {3}", + line.Name.PadRight(nameWidth), + line.Version.PadRight(versionWidth), + line.Default.PadRight(defaultWidth), + line.Path.PadRight(pathWidth) + ); + } + + return Exit.Ok; + } + + private int RenameInstall(object args) + { + var opts = (KspOptions.RenameKsp)args; + if (opts.OldName == null || opts.NewName == null) + { + _user.RaiseMessage("rename - argument(s) missing, perhaps you forgot it?"); + return Exit.BadOpt; + } + + if (!_manager.HasInstance(opts.OldName)) + { + _user.RaiseMessage("Couldn't find an install with the name \"{0}\", aborting...", opts.OldName); + return Exit.BadOpt; + } + + _manager.RenameInstance(opts.OldName, opts.NewName); + + _user.RaiseMessage("Successfully renamed \"{0}\" to \"{1}\".", opts.OldName, opts.NewName); + return Exit.Ok; + } + } + + [Verb("ksp", HelpText = "Manage KSP installs")] + [ChildVerbs(typeof(AddKsp), typeof(CloneKsp), typeof(DefaultKsp), typeof(FakeKsp), typeof(ForgetKsp), typeof(ListKsp), typeof(RenameKsp))] + internal class KspOptions + { + [VerbExclude] + [Verb("add", HelpText = "Add a KSP install")] + internal class AddKsp : CommonOptions + { + [Value(0, MetaName = "Name", HelpText = "The name of the new KSP install")] + public string Name { get; set; } + + [Value(1, MetaName = "Path", HelpText = "The path where KSP is installed")] + public string Path { get; set; } + } + + [VerbExclude] + [Verb("clone", HelpText = "Clone an existing KSP install")] + internal class CloneKsp : CommonOptions + { + [Value(0, MetaName = "NameOrPath", HelpText = "The name or path to clone")] + public string NameOrPath { get; set; } + + [Value(1, MetaName = "NewName", HelpText = "The name of the new KSP install")] + public string NewName { get; set; } + + [Value(2, MetaName = "NewPath", HelpText = "The path to clone the KSP install to")] + public string NewPath { get; set; } + } + + [VerbExclude] + [Verb("default", HelpText = "Set the default KSP install")] + internal class DefaultKsp : CommonOptions + { + [Value(0, MetaName = "Name", HelpText = "The name of the KSP install to set as the default")] + public string Name { get; set; } + } + + [VerbExclude] + [Verb("fake", HelpText = "Fake a KSP install")] + internal class FakeKsp : CommonOptions + { + [Option("MakingHistory", Default = "none", HelpText = "The version of the Making History DLC to be faked")] + public string MakingHistory { get; set; } + + [Option("BreakingGround", Default = "none", HelpText = "The version of the Breaking Ground DLC to be faked")] + public string BreakingGround { get; set; } + + [Option("set-default", HelpText = "Set the new instance as the default one")] + public bool SetDefault { get; set; } + + [Value(0, MetaName = "Name", HelpText = "The name of the faked KSP install")] + public string Name { get; set; } + + [Value(1, MetaName = "Path", HelpText = "The path to fake the KSP install to")] + public string Path { get; set; } + + [Value(2, MetaName = "KSP version", HelpText = "The KSP version of the faked install")] + public string Version { get; set; } + } + + [VerbExclude] + [Verb("forget", HelpText = "Forget a KSP install")] + internal class ForgetKsp : CommonOptions + { + [Value(0, MetaName = "Name", HelpText = "The name of the KSP install to remove")] + public string Name { get; set; } + } + + [VerbExclude] + [Verb("list", HelpText = "List KSP installs")] + internal class ListKsp : CommonOptions { } + + [VerbExclude] + [Verb("rename", HelpText = "Rename a KSP install")] + internal class RenameKsp : CommonOptions + { + [Value(0, MetaName = "Old name", HelpText = "The name of the KSP install to rename")] + public string OldName { get; set; } + + [Value(1, MetaName = "New name", HelpText = "The new name of the KSP install")] + public string NewName { get; set; } } - #endregion } } diff --git a/Cmdline/Action/ICommand.cs b/Cmdline/Action/ICommand.cs index 85ba382baa..01008f2509 100644 --- a/Cmdline/Action/ICommand.cs +++ b/Cmdline/Action/ICommand.cs @@ -1,7 +1,16 @@ -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { - public interface ICommand + /// + /// Interface for regular commands. + /// + internal interface ICommand { - int RunCommand(CKAN.GameInstance ksp, object options); + /// + /// Run the command. + /// + /// The game instance which to handle with mods. + /// The command line arguments handled by the parser. + /// An code. + int RunCommand(CKAN.GameInstance inst, object args); } } diff --git a/Cmdline/Action/ISubCommand.cs b/Cmdline/Action/ISubCommand.cs index 409f49dbee..5e3c5eebb9 100644 --- a/Cmdline/Action/ISubCommand.cs +++ b/Cmdline/Action/ISubCommand.cs @@ -1,7 +1,24 @@ -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { + /// + /// Interface for commands that use nested commands. + /// internal interface ISubCommand { - int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCommandOptions options); + /// + /// Run the command with nested commands. + /// + /// The manager to provide game instances. + /// The command line arguments handled by the parser. + /// An code. + int RunCommand(GameInstanceManager manager, object args); + + /// + /// Displays an USAGE prefix line in the help screen. + /// + /// The USAGE prefix with the app name. + /// The command line arguments handled by the parser. + /// An USAGE: which contains information on how to use the command. + string GetUsage(string prefix, string[] args); } } diff --git a/Cmdline/Action/Import.cs b/Cmdline/Action/Import.cs index 044d9170a7..2ebf83fec0 100644 --- a/Cmdline/Action/Import.cs +++ b/Cmdline/Action/Import.cs @@ -1,112 +1,118 @@ using System; -using System.IO; using System.Collections.Generic; +using System.IO; +using System.Linq; +using CommandLine; using log4net; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { - /// - /// Handler for "ckan import" command. - /// Imports manually downloaded ZIP files into the cache. + /// Class for managing the importing of mods. /// public class Import : ICommand { + private static readonly ILog Log = LogManager.GetLogger(typeof(Import)); + + private readonly GameInstanceManager _manager; + private readonly IUser _user; /// - /// Initialize the command + /// Initializes a new instance of the class. /// - /// IUser object for user interaction - public Import(GameInstanceManager mgr, IUser user) + /// The manager to provide game instances. + /// The current to raise messages to the user. + public Import(GameInstanceManager manager, IUser user) { - manager = mgr; - this.user = user; + _manager = manager; + _user = user; } /// - /// Execute an import command + /// Run the 'import' command. /// - /// Game instance into which to import - /// Command line parameters from the user - /// - /// Process exit code - /// - public int RunCommand(CKAN.GameInstance ksp, object options) + /// + public int RunCommand(CKAN.GameInstance inst, object args) { + var opts = (ImportOptions)args; + if (!opts.Paths.Any()) + { + _user.RaiseMessage("import [ ...] - argument(s) missing, perhaps you forgot it?"); + return Exit.BadOpt; + } + + var toImport = GetFiles(opts); try { - ImportOptions opts = options as ImportOptions; - HashSet toImport = GetFiles(opts); - if (toImport.Count < 1) - { - user.RaiseMessage("Usage: ckan import path [path2, ...]"); - return Exit.ERROR; - } - else + Log.InfoFormat("Importing {0} files...", toImport.Count); + var toInstall = new List(); + var regMgr = RegistryManager.Instance(inst); + var installer = new ModuleInstaller(inst, _manager.Cache, _user); + + installer.ImportFiles(toImport, _user, mod => toInstall.Add(mod.identifier), regMgr.registry, !opts.Headless); + + HashSet possibleConfigOnlyDirs = null; + if (toInstall.Count > 0) { - log.InfoFormat("Importing {0} files", toImport.Count); - List toInstall = new List(); - RegistryManager regMgr = RegistryManager.Instance(ksp); - ModuleInstaller inst = new ModuleInstaller(ksp, manager.Cache, user); - inst.ImportFiles(toImport, user, mod => toInstall.Add(mod.identifier), regMgr.registry, !opts.Headless); - HashSet possibleConfigOnlyDirs = null; - if (toInstall.Count > 0) - { - inst.InstallList( - toInstall, - new RelationshipResolverOptions(), - regMgr, - ref possibleConfigOnlyDirs - ); - } - return Exit.OK; + installer.InstallList( + toInstall, + new RelationshipResolverOptions(), + regMgr, + ref possibleConfigOnlyDirs + ); } } catch (Exception ex) { - user.RaiseError("Import error: {0}", ex.Message); - return Exit.ERROR; + _user.RaiseError("Import error: {0}", ex.Message); + return Exit.Error; } + + _user.RaiseMessage("Successfully imported {0} files.", toImport.Count); + return Exit.Ok; } private HashSet GetFiles(ImportOptions options) { - HashSet files = new HashSet(); - foreach (string filename in options.paths) + var files = new HashSet(); + foreach (var fileName in options.Paths) { - if (Directory.Exists(filename)) + if (Directory.Exists(fileName)) { // Import everything in this folder - log.InfoFormat("{0} is a directory", filename); - foreach (string dirfile in Directory.EnumerateFiles(filename)) + Log.InfoFormat("{0} is a directory. Adding contents...", fileName); + foreach (var dirFile in Directory.EnumerateFiles(fileName)) { - AddFile(files, dirfile); + AddFile(files, dirFile); } } else { - AddFile(files, filename); + AddFile(files, fileName); } } + return files; } - private void AddFile(HashSet files, string filename) + private void AddFile(HashSet files, string fileName) { - if (File.Exists(filename)) + if (File.Exists(fileName)) { - log.InfoFormat("Attempting import of {0}", filename); - files.Add(new FileInfo(filename)); + Log.InfoFormat("Attempting import of \"{0}\".", fileName); + files.Add(new FileInfo(fileName)); } else { - user.RaiseMessage("File not found: {0}", filename); + _user.RaiseMessage("File not found: \"{0}\".", fileName); } } - - private readonly GameInstanceManager manager; - private readonly IUser user; - private static readonly ILog log = LogManager.GetLogger(typeof(Import)); } + [Verb("import", HelpText = "Import manually downloaded mods")] + internal class ImportOptions : InstanceSpecificOptions + { + [Value(0, MetaName = "File path(s)", HelpText = "The path(s) of the files to import (can also be a directory)")] + public IEnumerable Paths { get; set; } + } } diff --git a/Cmdline/Action/Install.cs b/Cmdline/Action/Install.cs index 4603cd8d4f..fe95abbb8e 100644 --- a/Cmdline/Action/Install.cs +++ b/Cmdline/Action/Install.cs @@ -1,307 +1,323 @@ using System; -using System.IO; using System.Collections.Generic; +using System.IO; using System.Linq; +using CommandLine; using log4net; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { + /// + /// Class for managing the installations of mods. + /// public class Install : ICommand { - private static readonly ILog log = LogManager.GetLogger(typeof(Install)); + private static readonly ILog Log = LogManager.GetLogger(typeof(Install)); - public IUser user { get; set; } - private GameInstanceManager manager; + private readonly GameInstanceManager _manager; + private readonly IUser _user; /// - /// Initialize the install command object + /// Initializes a new instance of the class. /// - /// GameInstanceManager containing our instances - /// IUser object for interaction - public Install(GameInstanceManager mgr, IUser user) + /// The manager to provide game instances. + /// The current to raise messages to the user. + public Install(GameInstanceManager manager, IUser user) { - manager = mgr; - this.user = user; + _manager = manager; + _user = user; } /// - /// Installs a module, if available + /// Run the 'install' command. /// - /// Game instance into which to install - /// Command line options object - /// - /// Exit code for shell environment - /// - public int RunCommand(CKAN.GameInstance ksp, object raw_options) + /// + public int RunCommand(CKAN.GameInstance inst, object args) { - InstallOptions options = (InstallOptions) raw_options; + var opts = (InstallOptions)args; + if (!opts.Mods.Any()) + { + _user.RaiseMessage("install [ ...] - argument(s) missing, perhaps you forgot it?"); + return Exit.BadOpt; + } - if (options.ckan_files != null) + if (opts.CkanFiles.Any()) { - // Oooh! We're installing from a CKAN file. - foreach (string ckan_file in options.ckan_files) + // Ooh! We're installing from a CKAN file + foreach (var ckanFile in opts.CkanFiles) { - Uri ckan_uri; + Uri ckanUri; - // Check if the argument if a wellformatted Uri. - if (!Uri.IsWellFormedUriString(ckan_file, UriKind.Absolute)) + // Check if the argument is a well formatted Uri + if (!Uri.IsWellFormedUriString(ckanFile, UriKind.Absolute)) { - // Assume it is a local file, check if the file exists. - if (File.Exists(ckan_file)) + // Assume it's a local file, check if the file exists + if (File.Exists(ckanFile)) { - // Get the full path of the file. - ckan_uri = new Uri(Path.GetFullPath(ckan_file)); + // Get the full path of the file + ckanUri = new Uri(Path.GetFullPath(ckanFile)); } else { - // We have no further ideas as what we can do with this Uri, tell the user. - user.RaiseError("Can not find file \"{0}\".", ckan_file); - user.RaiseError("Exiting."); - return Exit.ERROR; + // We have no further ideas as what we can do with this Uri, tell the user + _user.RaiseError("Can't find file \"{0}\".\r\nExiting.", ckanFile); + return Exit.Error; } } else { - ckan_uri = new Uri(ckan_file); + ckanUri = new Uri(ckanFile); } - string filename = String.Empty; + string fileName; - // If it is a local file, we already know the filename. If it is remote, create a temporary file and download the remote resource. - if (ckan_uri.IsFile) + // If it's a local file, we already know the filename. If it's remote, create a temporary file and download the remote resource + if (ckanUri.IsFile) { - filename = ckan_uri.LocalPath; - log.InfoFormat("Installing from local CKAN file \"{0}\"", filename); + fileName = ckanUri.LocalPath; + Log.InfoFormat("Installing from local CKAN file \"{0}\".", fileName); } else { - log.InfoFormat("Installing from remote CKAN file \"{0}\"", ckan_uri); - filename = Net.Download(ckan_uri, null, user); + Log.InfoFormat("Installing from remote CKAN file \"{0}\".", ckanUri); + fileName = Net.Download(ckanUri, null, _user); - log.DebugFormat("Temporary file for \"{0}\" is at \"{1}\".", ckan_uri, filename); + Log.DebugFormat("Temporary file for \"{0}\" is at \"{1}\".", ckanUri, fileName); } - // Parse the JSON file. + // Parse the JSON file try { - CkanModule m = MainClass.LoadCkanFromFile(ksp, filename); - options.modules.Add($"{m.identifier}={m.version}"); + var m = MainClass.LoadCkanFromFile(inst, fileName); + opts.Mods.ToList().Add(string.Format("{0}={1}", m.identifier, m.version)); } catch (Kraken kraken) { - user.RaiseError(kraken.InnerException == null + _user.RaiseError(kraken.InnerException == null ? kraken.Message - : $"{kraken.Message}: {kraken.InnerException.Message}"); + : string.Format("{0}: {1}", kraken.Message, kraken.InnerException.Message)); } } // At times RunCommand() calls itself recursively - in this case we do // not want to be doing this again, so "consume" the option - options.ckan_files = null; + opts.CkanFiles = null; } else { - Search.AdjustModulesCase(ksp, options.modules); - } - - if (options.modules.Count == 0) - { - // What? No files specified? - user.RaiseMessage( - "Usage: ckan install [--with-suggests] [--with-all-suggests] [--no-recommends] [--headless] Mod [Mod2, ...]"); - return Exit.BADOPT; + Search.AdjustModulesCase(inst, opts.Mods.ToList()); } // Prepare options. Can these all be done in the new() somehow? - var install_ops = new RelationshipResolverOptions + var installOpts = new RelationshipResolverOptions { - with_all_suggests = options.with_all_suggests, - with_suggests = options.with_suggests, - with_recommends = !options.no_recommends, - allow_incompatible = options.allow_incompatible + with_all_suggests = opts.WithAllSuggests, + with_suggests = opts.WithSuggests, + with_recommends = !opts.NoRecommends, + allow_incompatible = opts.AllowIncompatible }; - if (user.Headless) + if (_user.Headless) { - install_ops.without_toomanyprovides_kraken = true; - install_ops.without_enforce_consistency = true; + installOpts.without_toomanyprovides_kraken = true; + installOpts.without_enforce_consistency = true; } - RegistryManager regMgr = RegistryManager.Instance(ksp); - List modules = options.modules; + var regMgr = RegistryManager.Instance(inst); + var modules = opts.Mods.ToList(); - for (bool done = false; !done; ) + for (var done = false; !done;) { - // Install everything requested. :) + // Install everything requested try { HashSet possibleConfigOnlyDirs = null; - var installer = new ModuleInstaller(ksp, manager.Cache, user); - installer.InstallList(modules, install_ops, regMgr, ref possibleConfigOnlyDirs); - user.RaiseMessage(""); + var installer = new ModuleInstaller(inst, _manager.Cache, _user); + installer.InstallList(modules, installOpts, regMgr, ref possibleConfigOnlyDirs); + _user.RaiseMessage(""); done = true; } - catch (DependencyNotSatisfiedKraken ex) + catch (DependencyNotSatisfiedKraken kraken) { - user.RaiseError(ex.Message); - user.RaiseMessage("If you're lucky, you can do a `ckan update` and try again."); - user.RaiseMessage("Try `ckan install --no-recommends` to skip installation of recommended modules."); - user.RaiseMessage("Or `ckan install --allow-incompatible` to ignore module compatibility."); - return Exit.ERROR; + _user.RaiseError(kraken.Message); + _user.RaiseMessage("If you're lucky, you can do a 'ckan update' and try again."); + _user.RaiseMessage("Try 'ckan install --no-recommends' to skip installation of recommended mods."); + _user.RaiseMessage("Or 'ckan install --allow-incompatible' to ignore mod compatibility."); + return Exit.Error; } - catch (ModuleNotFoundKraken ex) + catch (ModuleNotFoundKraken kraken) { - if (ex.version == null) + if (kraken.version == null) { - user.RaiseError("Module {0} required but it is not listed in the index, or not available for your version of KSP.", - ex.module); + _user.RaiseError("The mod \"{0}\" is required but it is not listed in the index, or not available for your version of {1}.", kraken.module, inst.game.ShortName); } else { - user.RaiseError("Module {0} {1} required but it is not listed in the index, or not available for your version of KSP.", - ex.module, ex.version); + _user.RaiseError("The mod \"{0}\" {1} is required but it is not listed in the index, or not available for your version of {2}.", kraken.module, kraken.version, inst.game.ShortName); } - user.RaiseMessage("If you're lucky, you can do a `ckan update` and try again."); - user.RaiseMessage("Try `ckan install --no-recommends` to skip installation of recommended modules."); - user.RaiseMessage("Or `ckan install --allow-incompatible` to ignore module compatibility."); - return Exit.ERROR; + + _user.RaiseMessage("If you're lucky, you can do a 'ckan update' and try again."); + _user.RaiseMessage("Try 'ckan install --no-recommends' to skip installation of recommended mods."); + _user.RaiseMessage("Or 'ckan install --allow-incompatible' to ignore mod compatibility."); + return Exit.Error; } - catch (BadMetadataKraken ex) + catch (BadMetadataKraken kraken) { - user.RaiseError("Bad metadata detected for module {0}: {1}", - ex.module, ex.Message); - return Exit.ERROR; + _user.RaiseError("Bad metadata detected for mod {0}.\r\n{1}", kraken.module, kraken.Message); + return Exit.Error; } - catch (TooManyModsProvideKraken ex) + catch (TooManyModsProvideKraken kraken) { - // Request the user selects one of the mods. - string[] mods = new string[ex.modules.Count]; + // Request the user to select one of the mods + var mods = new string[kraken.modules.Count]; - for (int i = 0; i < ex.modules.Count; i++) + for (var i = 0; i < kraken.modules.Count; i++) { - mods[i] = String.Format("{0} ({1})", ex.modules[i].identifier, ex.modules[i].name); + mods[i] = string.Format("{0} ({1})", kraken.modules[i].identifier, kraken.modules[i].name); } - string message = String.Format("Too many mods provide {0}. Please pick from the following:\r\n", ex.requested); + var message = string.Format("Too many mods provide \"{0}\". Please pick one from the following mods:\r\n", kraken.requested); int result; - try { - result = user.RaiseSelectionDialog(message, mods); + result = _user.RaiseSelectionDialog(message, mods); } - catch (Kraken e) + catch (Kraken k) { - user.RaiseMessage(e.Message); - - return Exit.ERROR; + _user.RaiseMessage(k.Message); + return Exit.Error; } if (result < 0) { - user.RaiseMessage(String.Empty); // Looks tidier. - - return Exit.ERROR; + _user.RaiseMessage(string.Empty); // Looks tidier + return Exit.Error; } - // Add the module to the list. - modules.Add($"{ex.modules[result].identifier}={ex.modules[result].version}"); - // DON'T return so we can loop around and try again + // Add the module to the list + modules.Add(string.Format("{0}={1}", + kraken.modules[result].identifier, + kraken.modules[result].version)); } - catch (FileExistsKraken ex) + catch (FileExistsKraken kraken) { - if (ex.owningModule != null) + if (kraken.owningModule != null) { - user.RaiseError( - "Oh no! We tried to overwrite a file owned by another mod!\r\n"+ - "Please try a `ckan update` and try again.\r\n\r\n"+ - "If this problem re-occurs, then it maybe a packaging bug.\r\n"+ + _user.RaiseError( + "Oh no! We tried to overwrite a file owned by another mod!\r\n" + + "Please try a 'ckan update' and try again.\r\n\r\n" + + "If this problem re-occurs, then it may be a packaging bug.\r\n" + "Please report it at:\r\n\r\n" + - "https://github.com/KSP-CKAN/NetKAN/issues/new\r\n\r\n" + - "Please including the following information in your report:\r\n\r\n" + + "https://github.com/KSP-CKAN/NetKAN/issues/new/choose\r\n\r\n" + + "Please include the following information in your report:\r\n\r\n" + "File : {0}\r\n" + "Installing Mod : {1}\r\n" + "Owning Mod : {2}\r\n" + "CKAN Version : {3}\r\n", - ex.filename, ex.installingModule, ex.owningModule, + kraken.filename, kraken.installingModule, kraken.owningModule, Meta.GetVersion(VersionFormat.Full) ); } else { - user.RaiseError( - "Oh no!\r\n\r\n"+ - "It looks like you're trying to install a mod which is already installed,\r\n"+ - "or which conflicts with another mod which is already installed.\r\n\r\n"+ - "As a safety feature, CKAN will *never* overwrite or alter a file\r\n"+ - "that it did not install itself.\r\n\r\n"+ - "If you wish to install {0} via CKAN,\r\n"+ - "then please manually uninstall the mod which owns:\r\n\r\n"+ - "{1}\r\n\r\n"+"and try again.\r\n", - ex.installingModule, ex.filename + _user.RaiseError( + "Oh no!\r\n\r\n" + + "It looks like you're trying to install a mod which is already installed,\r\n" + + "or which conflicts with another mod which is already installed.\r\n\r\n" + + "As a safety feature, CKAN will *never* overwrite or alter a file\r\n" + + "that it did not install itself.\r\n\r\n" + + "If you wish to install {0} via CKAN,\r\n" + + "then please manually uninstall the mod which owns:\r\n\r\n" + + "{1}\r\n\r\n" + + "and try again.\r\n", + kraken.installingModule, kraken.filename ); } - user.RaiseMessage("Your GameData has been returned to its original state."); - return Exit.ERROR; + _user.RaiseMessage("Your GameData has been returned to its original state."); + return Exit.Error; } - catch (InconsistentKraken ex) + catch (InconsistentKraken kraken) { - // The prettiest Kraken formats itself for us. - user.RaiseError(ex.InconsistenciesPretty); - user.RaiseMessage("Install canceled. Your files have been returned to their initial state."); - return Exit.ERROR; + // The prettiest Kraken formats itself for us + _user.RaiseError(kraken.InconsistenciesPretty); + _user.RaiseMessage("Install canceled. Your files have been returned to their initial state."); + return Exit.Error; } - catch (CancelledActionKraken k) + catch (CancelledActionKraken kraken) { - user.RaiseError("Installation aborted: {0}", k.Message); - return Exit.ERROR; + _user.RaiseError("Installation aborted: {0}", kraken.Message); + return Exit.Error; } catch (MissingCertificateKraken kraken) { - // Another very pretty kraken. - user.RaiseError(kraken.ToString()); - return Exit.ERROR; + // Another very pretty kraken + _user.RaiseError(kraken.ToString()); + return Exit.Error; } catch (DownloadThrottledKraken kraken) { - user.RaiseError(kraken.ToString()); - user.RaiseMessage("Try the authtoken command. See {0} for details.", - kraken.infoUrl); - return Exit.ERROR; + _user.RaiseError(kraken.ToString()); + _user.RaiseMessage("Try the authtoken command. See \"{0}\" for more details.", kraken.infoUrl); + return Exit.Error; } catch (DownloadErrorsKraken) { - user.RaiseError("One or more files failed to download, stopped."); - return Exit.ERROR; + _user.RaiseError("One or more files failed to download, stopped."); + return Exit.Error; } catch (ModuleDownloadErrorsKraken kraken) { - user.RaiseError(kraken.ToString()); - return Exit.ERROR; + _user.RaiseError(kraken.ToString()); + return Exit.Error; } catch (DirectoryNotFoundKraken kraken) { - user.RaiseError(kraken.Message); - return Exit.ERROR; + _user.RaiseError("\r\n{0}", kraken.Message); + return Exit.Error; } catch (ModuleIsDLCKraken kraken) { - user.RaiseError("CKAN can't install expansion '{0}' for you.", - kraken.module.name); - var res = kraken?.module?.resources; - var storePagesMsg = new Uri[] { res?.store, res?.steamstore } + _user.RaiseError("Can't install the expansion \"{0}\".", kraken.module.name); + var res = kraken.module?.resources; + var storePagesMsg = new[] { res?.store, res?.steamstore } .Where(u => u != null) .Aggregate("", (a, b) => $"{a}\r\n- {b}"); + if (!string.IsNullOrEmpty(storePagesMsg)) { - user.RaiseMessage($"To install this expansion, purchase it from one of its store pages:\r\n{storePagesMsg}"); + _user.RaiseMessage("To install this expansion, purchase it from one of its store pages:\r\n {0}", storePagesMsg); } - return Exit.ERROR; + + return Exit.Error; } } - return Exit.OK; + _user.RaiseMessage("Successfully installed requested mods."); + return Exit.Ok; } } + + [Verb("install", HelpText = "Install a mod")] + internal class InstallOptions : InstanceSpecificOptions + { + [Option('c', "ckanfiles", HelpText = "Local CKAN files to process")] + public IEnumerable CkanFiles { get; set; } + + [Option("no-recommends", HelpText = "Do not install recommended mods")] + public bool NoRecommends { get; set; } + + [Option("with-suggests", HelpText = "Install suggested mods")] + public bool WithSuggests { get; set; } + + [Option("with-all-suggests", HelpText = "Install suggested mods all the way down")] + public bool WithAllSuggests { get; set; } + + [Option("allow-incompatible", HelpText = "Install mods that are not compatible with the current game version")] + public bool AllowIncompatible { get; set; } + + [Value(0, MetaName = "Mod name(s)", HelpText = "The mod name(s) to install")] + public IEnumerable Mods { get; set; } + } } diff --git a/Cmdline/Action/List.cs b/Cmdline/Action/List.cs index 04b0220202..a92ceff2d5 100644 --- a/Cmdline/Action/List.cs +++ b/Cmdline/Action/List.cs @@ -3,63 +3,71 @@ using CKAN.Exporters; using CKAN.Types; using CKAN.Versioning; +using CommandLine; using log4net; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { + /// + /// Class for listing the installed mods. + /// public class List : ICommand { - private static readonly ILog log = LogManager.GetLogger(typeof(List)); + private static readonly ILog Log = LogManager.GetLogger(typeof(List)); - public IUser user { get; set; } + private readonly IUser _user; + /// + /// Initializes a new instance of the class. + /// + /// The current to raise messages to the user. public List(IUser user) { - this.user = user; + _user = user; } - public int RunCommand(CKAN.GameInstance ksp, object raw_options) + /// + /// Run the 'list' command. + /// + /// + public int RunCommand(CKAN.GameInstance inst, object args) { - ListOptions options = (ListOptions) raw_options; - - IRegistryQuerier registry = RegistryManager.Instance(ksp).registry; - + var opts = (ListOptions)args; + IRegistryQuerier registry = RegistryManager.Instance(inst).registry; ExportFileType? exportFileType = null; - if (!string.IsNullOrWhiteSpace(options.export)) + if (!string.IsNullOrWhiteSpace(opts.Export)) { - exportFileType = GetExportFileType(options.export); - + exportFileType = GetExportFileType(opts.Export); if (exportFileType == null) { - user.RaiseError("Unknown export format: {0}", options.export); + _user.RaiseError("Unknown export format: {0}", opts.Export); } } - if (!(options.porcelain) && exportFileType == null) + if (!opts.Porcelain && exportFileType == null) { - user.RaiseMessage("\r\nKSP found at {0}\r\n", ksp.GameDir()); - user.RaiseMessage("KSP Version: {0}\r\n", ksp.Version()); - - user.RaiseMessage("Installed Modules:\r\n"); + _user.RaiseMessage("\r\nFound {0} at \"{1}\"", inst.game.ShortName, inst.GameDir()); + _user.RaiseMessage("\r\n{0} version: \"{1}\"", inst.game.ShortName, inst.Version()); + _user.RaiseMessage("\r\nInstalled Modules:\r\n"); } if (exportFileType == null) { - var installed = new SortedDictionary(registry.Installed()); - - foreach (KeyValuePair mod in installed) + var mods = new SortedDictionary(registry.Installed()); + foreach (var mod in mods) { - ModuleVersion current_version = mod.Value; - string modInfo = string.Format("{0} {1}", mod.Key, mod.Value); - string bullet = "*"; + var currentVersion = mod.Value; + var modInfo = string.Format("{0} {1}", mod.Key, mod.Value); + var bullet = "*"; - if (current_version is ProvidesModuleVersion) + if (currentVersion is ProvidesModuleVersion) { - // Skip virtuals for now. + // Skip virtuals for now continue; } - else if (current_version is UnmanagedModuleVersion) + + if (currentVersion is UnmanagedModuleVersion) { // Autodetected dll bullet = "A"; @@ -68,23 +76,26 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) { try { - // Check if upgrades are available, and show appropriately. - log.DebugFormat("Check if upgrades are available for {0}", mod.Key); - CkanModule latest = registry.LatestAvailable(mod.Key, ksp.VersionCriteria()); - CkanModule current = registry.GetInstalledVersion(mod.Key); - InstalledModule inst = registry.InstalledModule(mod.Key); + // Check if upgrades are available, and show appropriately + Log.DebugFormat("Checking if upgrades are available for \"{0}\"...", mod.Key); + var latest = registry.LatestAvailable(mod.Key, inst.VersionCriteria()); + var current = registry.GetInstalledVersion(mod.Key); + var installed = registry.InstalledModule(mod.Key); if (latest == null) { // Not compatible! - log.InfoFormat("Latest {0} is not compatible", mod.Key); + Log.InfoFormat("Latest \"{0}\" is not compatible.", mod.Key); bullet = "X"; - if ( current == null ) log.DebugFormat( " {0} installed version not found in registry", mod.Key); - + if (current == null) + { + Log.DebugFormat("No installed version of \"{0}\" found in the registry.", mod.Key); + } + // Check if mod is replaceable if (current.replaced_by != null) { - ModuleReplacement replacement = registry.GetReplacement(mod.Key, ksp.VersionCriteria()); + var replacement = registry.GetReplacement(mod.Key, inst.VersionCriteria()); if (replacement != null) { // Replaceable! @@ -93,15 +104,16 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) } } } - else if (latest.version.IsEqualTo(current_version)) + else if (latest.version.IsEqualTo(currentVersion)) { // Up to date - log.InfoFormat("Latest {0} is {1}", mod.Key, latest.version); - bullet = (inst?.AutoInstalled ?? false) ? "+" : "-"; + Log.InfoFormat("Latest \"{0}\" is {1}", mod.Key, latest.version); + bullet = installed?.AutoInstalled ?? false ? "+" : "-"; + // Check if mod is replaceable if (current.replaced_by != null) { - ModuleReplacement replacement = registry.GetReplacement(latest.identifier, ksp.VersionCriteria()); + var replacement = registry.GetReplacement(latest.identifier, inst.VersionCriteria()); if (replacement != null) { // Replaceable! @@ -118,12 +130,12 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) } catch (ModuleNotFoundKraken) { - log.InfoFormat("{0} is installed, but no longer in the registry", mod.Key); + Log.InfoFormat("\"{0}\" is installed, but no longer in the registry.", mod.Key); bullet = "?"; } } - user.RaiseMessage("{0} {1}", bullet, modInfo); + _user.RaiseMessage("{0} {1}", bullet, modInfo); } } else @@ -133,13 +145,13 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) stream.Flush(); } - if (!(options.porcelain) && exportFileType == null) + if (!opts.Porcelain && exportFileType == null) { - user.RaiseMessage("\r\nLegend: -: Up to date. +:Auto-installed. X: Incompatible. ^: Upgradable. >: Replaceable\r\n A: Autodetected. ?: Unknown. *: Broken. "); // Broken mods are in a state that CKAN doesn't understand, and therefore can't handle automatically + _user.RaiseMessage("\r\nLegend: -: Up to date. +: Auto-installed. X: Incompatible. ^: Upgradable. >: Replaceable\r\n A: Autodetected. ?: Unknown. *: Broken. "); } - return Exit.OK; + return Exit.Ok; } private static ExportFileType? GetExportFileType(string export) @@ -148,13 +160,29 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) switch (export) { - case "text": return ExportFileType.PlainText; - case "markdown": return ExportFileType.Markdown; - case "bbcode": return ExportFileType.BbCode; - case "csv": return ExportFileType.Csv; - case "tsv": return ExportFileType.Tsv; - default: return null; + case "text": + return ExportFileType.PlainText; + case "markdown": + return ExportFileType.Markdown; + case "bbcode": + return ExportFileType.BbCode; + case "csv": + return ExportFileType.Csv; + case "tsv": + return ExportFileType.Tsv; + default: + return null; } } } + + [Verb("list", HelpText = "List installed mods")] + internal class ListOptions : InstanceSpecificOptions + { + [Option("porcelain", HelpText = "Dump a raw list of mods, good for shell scripting")] + public bool Porcelain { get; set; } + + [Option("export", HelpText = "Export a list of mods in specified format to stdout")] + public string Export { get; set; } + } } diff --git a/Cmdline/Action/Mark.cs b/Cmdline/Action/Mark.cs index 777f73aeb5..a0c42593da 100644 --- a/Cmdline/Action/Mark.cs +++ b/Cmdline/Action/Mark.cs @@ -1,166 +1,193 @@ using System.Collections.Generic; +using System.Linq; using CommandLine; -using CommandLine.Text; -using log4net; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { /// - /// Subcommand for setting flags on modules, - /// currently the auto-installed flag + /// Class for setting flags on mods, currently the auto-installed flag. /// public class Mark : ISubCommand { - /// - /// Initialize the subcommand - /// - public Mark() { } + private GameInstanceManager _manager; + private IUser _user; /// - /// Run the subcommand + /// Run the 'mark' command. /// - /// Manager to provide game instances - /// Command line parameters paritally handled by parser - /// Command line parameters not yet handled by parser - /// - /// Exit code - /// - public int RunSubCommand(GameInstanceManager mgr, CommonOptions opts, SubCommandOptions unparsed) + /// + public int RunCommand(GameInstanceManager manager, object args) { - string[] args = unparsed.options.ToArray(); - int exitCode = Exit.OK; - // Parse and process our sub-verbs - Parser.Default.ParseArgumentsStrict(args, new MarkSubOptions(), (string option, object suboptions) => + var s = args.ToString(); + var opts = s.Replace(s.Substring(0, s.LastIndexOf('.') + 1), "").Split('+'); + + CommonOptions options = new CommonOptions(); + _user = new ConsoleUser(options.Headless); + _manager = manager ?? new GameInstanceManager(_user); + var exitCode = options.Handle(_manager, _user); + + if (exitCode != Exit.Ok) + return exitCode; + + switch (opts[1]) { - // ParseArgumentsStrict calls us unconditionally, even with bad arguments - if (!string.IsNullOrEmpty(option) && suboptions != null) - { - CommonOptions options = (CommonOptions)suboptions; - options.Merge(opts); - user = new ConsoleUser(options.Headless); - manager = mgr ?? new GameInstanceManager(user); - exitCode = options.Handle(manager, user); - if (exitCode != Exit.OK) - return; - - switch (option) - { - case "auto": - exitCode = MarkAuto((MarkAutoOptions)suboptions, true, option, "auto-installed"); - break; - - case "user": - exitCode = MarkAuto((MarkAutoOptions)suboptions, false, option, "user-selected"); - break; - - default: - user.RaiseMessage("Unknown command: mark {0}", option); - exitCode = Exit.BADOPT; - break; - } - } - }, () => { exitCode = MainClass.AfterHelp(); }); + case "MarkAuto": + exitCode = MarkAuto(args); + break; + case "MarkUser": + exitCode = MarkUser(args); + break; + default: + exitCode = Exit.BadOpt; + break; + } + return exitCode; } - private int MarkAuto(MarkAutoOptions opts, bool value, string verb, string descrip) + /// + public string GetUsage(string prefix, string[] args) { - if (opts.modules.Count < 1) + if (args.Length == 1) + return $"{prefix} {args[0]} [options]"; + + switch (args[1]) { - user.RaiseMessage("Usage: ckan mark {0} Mod [Mod2 ...]", verb); - return Exit.BADOPT; + case "auto": + case "user": + return $"{prefix} {args[0]} {args[1]} [options] [ ...]"; + default: + return $"{prefix} {args[0]} [options]"; } + } - int exitCode = opts.Handle(manager, user); - if (exitCode != Exit.OK) + private int MarkAuto(object args) + { + var opts = (MarkOptions.MarkAuto)args; + if (!opts.Mods.Any()) { - return exitCode; + _user.RaiseMessage("auto [ ...] - argument(s) missing, perhaps you forgot it?"); + return Exit.BadOpt; } - var ksp = MainClass.GetGameInstance(manager); - var regMgr = RegistryManager.Instance(ksp); - bool needSave = false; - Search.AdjustModulesCase(ksp, opts.modules); - foreach (string id in opts.modules) + var exitCode = opts.Handle(_manager, _user); + if (exitCode != Exit.Ok) + return exitCode; + + var inst = MainClass.GetGameInstance(_manager); + var regMgr = RegistryManager.Instance(inst); + var needSave = false; + Search.AdjustModulesCase(inst, opts.Mods.ToList()); + + foreach (var id in opts.Mods) { - InstalledModule im = regMgr.registry.InstalledModule(id); - if (im == null) + var mod = regMgr.registry.InstalledModule(id); + if (mod == null) { - user.RaiseError("{0} is not installed.", id); + _user.RaiseError("\"{0}\" is not installed.", id); } - else if (im.AutoInstalled == value) + else if (mod.AutoInstalled) { - user.RaiseError("{0} is already marked as {1}.", id, descrip); + _user.RaiseError("\"{0}\" is already marked as auto-installed.", id); } else { - user.RaiseMessage("Marking {0} as {1}...", id, descrip); + _user.RaiseMessage("Marking \"{0}\" as auto-installed...", id); try { - im.AutoInstalled = value; + mod.AutoInstalled = true; needSave = true; } catch (ModuleIsDLCKraken kraken) { - user.RaiseMessage($"Can't mark expansion '{kraken.module.name}' as auto-installed."); - return Exit.BADOPT; + _user.RaiseMessage("Can't mark expansion \"{0}\" as auto-installed.", kraken.module.name); + return Exit.BadOpt; } } } + if (needSave) { regMgr.Save(false); - user.RaiseMessage("Changes made!"); + _user.RaiseMessage("Successfully applied changes."); } - return Exit.OK; - } - private GameInstanceManager manager { get; set; } - private IUser user { get; set; } - - private static readonly ILog log = LogManager.GetLogger(typeof(Mark)); - } - - internal class MarkSubOptions : VerbCommandOptions - { - [VerbOption("auto", HelpText = "Mark modules as auto installed")] - public MarkAutoOptions MarkAutoOptions { get; set; } - - [VerbOption("user", HelpText = "Mark modules as user selected (opposite of auto installed)")] - public MarkAutoOptions MarkUserOptions { get; set; } + return Exit.Ok; + } - [HelpVerbOption] - public string GetUsage(string verb) + private int MarkUser(object args) { - HelpText ht = HelpText.AutoBuild(this, verb); - // Add a usage prefix line - ht.AddPreOptionsLine(" "); - if (string.IsNullOrEmpty(verb)) + var opts = (MarkOptions.MarkUser)args; + if (!opts.Mods.Any()) { - ht.AddPreOptionsLine("ckan mark - Edit flags on modules"); - ht.AddPreOptionsLine($"Usage: ckan mark [options]"); + _user.RaiseMessage("user [ ...] - argument(s) missing, perhaps you forgot it?"); + return Exit.BadOpt; } - else + + var exitCode = opts.Handle(_manager, _user); + if (exitCode != Exit.Ok) + return exitCode; + + var inst = MainClass.GetGameInstance(_manager); + var regMgr = RegistryManager.Instance(inst); + var needSave = false; + Search.AdjustModulesCase(inst, opts.Mods.ToList()); + + foreach (var id in opts.Mods) { - ht.AddPreOptionsLine("ksp " + verb + " - " + GetDescription(verb)); - switch (verb) + var mod = regMgr.registry.InstalledModule(id); + if (mod == null) { - case "auto": - ht.AddPreOptionsLine($"Usage: ckan mark {verb} [options] Mod [Mod2 ...]"); - break; - - case "user": - ht.AddPreOptionsLine($"Usage: ckan mark {verb} [options] Mod [Mod2 ...]"); - break; + _user.RaiseError("\"{0}\" is not installed.", id); } + else if (!mod.AutoInstalled) + { + _user.RaiseError("\"{0}\" is already marked as user-selected.", id); + } + else + { + _user.RaiseMessage("Marking \"{0}\" as user-selected...", id); + try + { + mod.AutoInstalled = false; + needSave = true; + } + catch (ModuleIsDLCKraken kraken) + { + _user.RaiseMessage("Can't mark expansion \"{0}\" as auto-installed.", kraken.module.name); + return Exit.BadOpt; + } + } + } + + if (needSave) + { + regMgr.Save(false); + _user.RaiseMessage("Successfully applied changes."); } - return ht; + + return Exit.Ok; } } - internal class MarkAutoOptions : InstanceSpecificOptions + [Verb("mark", HelpText = "Edit flags on mods")] + [ChildVerbs(typeof(MarkAuto), typeof(MarkUser))] + internal class MarkOptions { - [ValueList(typeof(List))] - public List modules { get; set; } + [VerbExclude] + [Verb("auto", HelpText = "Mark mods as auto installed")] + internal class MarkAuto : InstanceSpecificOptions + { + [Value(0, MetaName = "Mod name(s)", HelpText = "The mod name(s) to mark as auto-installed")] + public IEnumerable Mods { get; set; } + } + + [VerbExclude] + [Verb("user", HelpText = "Mark mods as user selected (opposite of auto installed)")] + internal class MarkUser : InstanceSpecificOptions + { + [Value(0, MetaName = "Mod name(s)", HelpText = "The mod name(s) to mark as user-installed")] + public IEnumerable Mods { get; set; } + } } } diff --git a/Cmdline/Action/Prompt.cs b/Cmdline/Action/Prompt.cs index 520d0f847a..4fb1ca97a3 100644 --- a/Cmdline/Action/Prompt.cs +++ b/Cmdline/Action/Prompt.cs @@ -1,18 +1,24 @@ using System; using CommandLine; -using CommandLine.Text; -using log4net; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { - + /// + /// Class for managing multiple commands. + /// public class Prompt { - public Prompt() { } + private const string ExitCommand = "exit"; - public int RunCommand(GameInstanceManager manager, object raw_options) + /// + /// Run the 'prompt' command. + /// + /// The manager to provide game instances. + /// The command line arguments handled by the parser. + /// An code. + public int RunCommand(GameInstanceManager manager, object args, string[] argStrings) { - CommonOptions opts = raw_options as CommonOptions; + var opts = (PromptOptions)args; // Print an intro if not in headless mode if (!(opts?.Headless ?? false)) { @@ -29,22 +35,27 @@ public int RunCommand(GameInstanceManager manager, object raw_options) { Console.Write( manager.CurrentInstance != null - ? $"CKAN {Meta.GetVersion()}: {manager.CurrentInstance.game.ShortName} {manager.CurrentInstance.Version()} ({manager.CurrentInstance.Name})> " - : $"CKAN {Meta.GetVersion()}> " + ? string.Format("CKAN {0}: {1} {2} ({3})> ", + Meta.GetVersion(), + manager.CurrentInstance.game.ShortName, + manager.CurrentInstance.Version(), + manager.CurrentInstance.Name) + : string.Format("CKAN {0}> ", Meta.GetVersion()) ); } + // Get input - string command = Console.ReadLine(); - if (command == null || command == exitCommand) + var command = Console.ReadLine(); + if (command == null || command == ExitCommand) { done = true; } - else if (command != "") + else if (command != string.Empty) { // Parse input as if it was a normal command line, - // but with a persistent GameInstanceManager object. - int cmdExitCode = MainClass.Execute(manager, opts, command.Split(' ')); - if ((opts?.Headless ?? false) && cmdExitCode != Exit.OK) + // but with a persistent GameInstanceManager object + var cmdExitCode = MainClass.Execute(manager, command.Split(' '), argStrings); + if ((opts?.Headless ?? false) && cmdExitCode != Exit.Ok) { // Pass failure codes to calling process in headless mode // (in interactive mode the user can see the error and try again) @@ -52,10 +63,11 @@ public int RunCommand(GameInstanceManager manager, object raw_options) } } } - return Exit.OK; - } - private const string exitCommand = "exit"; + return Exit.Ok; + } } + [Verb("prompt", HelpText = "Run CKAN prompt for executing multiple commands in a row")] + internal class PromptOptions : CommonOptions { } } diff --git a/Cmdline/Action/Remove.cs b/Cmdline/Action/Remove.cs index d25a186707..e80a41d862 100644 --- a/Cmdline/Action/Remove.cs +++ b/Cmdline/Action/Remove.cs @@ -1,119 +1,135 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using CommandLine; using log4net; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { + /// + /// Class for managing the removal of mods. + /// public class Remove : ICommand { - private static readonly ILog log = LogManager.GetLogger(typeof(Remove)); + private static readonly ILog Log = LogManager.GetLogger(typeof(Remove)); - public IUser user { get; set; } - private GameInstanceManager manager; + private readonly GameInstanceManager _manager; + private readonly IUser _user; /// - /// Initialize the remove command object + /// Initializes a new instance of the class. /// - /// GameInstanceManager containing our instances - /// IUser object for interaction - public Remove(GameInstanceManager mgr, IUser user) + /// The manager to provide game instances. + /// The current to raise messages to the user. + public Remove(GameInstanceManager manager, IUser user) { - manager = mgr; - this.user = user; + _manager = manager; + _user = user; } /// - /// Uninstalls a module, if it exists. + /// Run the 'remove' command. /// - /// Game instance from which to remove - /// Command line options object - /// - /// Exit code for shell environment - /// - public int RunCommand(CKAN.GameInstance ksp, object raw_options) + /// + public int RunCommand(CKAN.GameInstance inst, object args) { - RemoveOptions options = (RemoveOptions) raw_options; - RegistryManager regMgr = RegistryManager.Instance(ksp); + var opts = (RemoveOptions)args; + if (!opts.Mods.Any()) + { + _user.RaiseMessage("remove [ ...] - argument(s) missing, perhaps you forgot it?"); + return Exit.BadOpt; + } + + var regMgr = RegistryManager.Instance(inst); // Use one (or more!) regex to select the modules to remove - if (options.regex) + if (opts.Regex) { - log.Debug("Attempting Regex"); + Log.Debug("Attempting Regex..."); + // Parse every "module" as a grumpy regex - var justins = options.modules.Select(s => new Regex(s)); + var justins = opts.Mods.Select(s => new Regex(s)); // Modules that have been selected by one regex - List selectedModules = new List(); + var selectedModules = new List(); // Get the list of installed modules // Try every regex on every installed module: // if it matches, select for removal - foreach (string mod in regMgr.registry.InstalledModules.Select(mod => mod.identifier)) + foreach (var mod in regMgr.registry.InstalledModules.Select(mod => mod.identifier)) { if (justins.Any(re => re.IsMatch(mod))) + { selectedModules.Add(mod); + } } // Replace the regular expressions with the selected modules // and continue removal as usual - options.modules = selectedModules; + opts.Mods = selectedModules; } - if (options.rmall) + if (opts.All) { - log.Debug("Removing all mods"); + Log.Debug("Removing all mods..."); + // Add the list of installed modules to the list that should be uninstalled - options.modules.AddRange( + opts.Mods.ToList().AddRange( regMgr.registry.InstalledModules .Where(mod => !mod.Module.IsDLC) .Select(mod => mod.identifier) ); } - if (options.modules != null && options.modules.Count > 0) + try { - try - { - HashSet possibleConfigOnlyDirs = null; - var installer = new ModuleInstaller(ksp, manager.Cache, user); - Search.AdjustModulesCase(ksp, options.modules); - installer.UninstallList(options.modules, ref possibleConfigOnlyDirs, regMgr); - user.RaiseMessage(""); - } - catch (ModNotInstalledKraken kraken) - { - user.RaiseMessage("I can't do that, {0} isn't installed.", kraken.mod); - user.RaiseMessage("Try `ckan list` for a list of installed mods."); - return Exit.BADOPT; - } - catch (ModuleIsDLCKraken kraken) - { - user.RaiseMessage($"CKAN can't remove expansion '{kraken.module.name}' for you."); - var res = kraken?.module?.resources; - var storePagesMsg = new Uri[] { res?.store, res?.steamstore } - .Where(u => u != null) - .Aggregate("", (a, b) => $"{a}\r\n- {b}"); - if (!string.IsNullOrEmpty(storePagesMsg)) - { - user.RaiseMessage($"To remove this expansion, follow the instructions for the store page from which you purchased it:\r\n{storePagesMsg}"); - } - return Exit.BADOPT; - } - catch (CancelledActionKraken k) + HashSet possibleConfigOnlyDirs = null; + var installer = new ModuleInstaller(inst, _manager.Cache, _user); + Search.AdjustModulesCase(inst, opts.Mods.ToList()); + installer.UninstallList(opts.Mods, ref possibleConfigOnlyDirs, regMgr); + _user.RaiseMessage(""); + } + catch (ModNotInstalledKraken kraken) + { + _user.RaiseMessage("Can't remove \"{0}\" because it isn't installed.\r\nTry 'ckan list' for a list of installed mods.", kraken.mod); + return Exit.Error; + } + catch (ModuleIsDLCKraken kraken) + { + _user.RaiseMessage("Can't remove the expansion \"{0}\".", kraken.module.name); + var res = kraken?.module?.resources; + var storePagesMsg = new[] { res?.store, res?.steamstore } + .Where(u => u != null) + .Aggregate("", (a, b) => $"{a}\r\n- {b}"); + + if (!string.IsNullOrEmpty(storePagesMsg)) { - user.RaiseMessage("Remove aborted: {0}", k.Message); - return Exit.ERROR; + _user.RaiseMessage("To remove this expansion, follow the instructions for the store page from which you purchased it:\r\n {0}", storePagesMsg); } + + return Exit.Error; } - else + catch (CancelledActionKraken kraken) { - user.RaiseMessage("No mod selected, nothing to do"); - return Exit.BADOPT; + _user.RaiseMessage("Remove aborted: {0}", kraken.Message); + return Exit.Error; } - return Exit.OK; + _user.RaiseMessage("Successfully removed requested mods."); + return Exit.Ok; } } + + [Verb("remove", HelpText = "Remove an installed mod")] + internal class RemoveOptions : InstanceSpecificOptions + { + [Option("re", HelpText = "Parse arguments as regular expressions")] + public bool Regex { get; set; } + + [Option("all", HelpText = "Remove all installed mods")] + public bool All { get; set; } + + [Value(0, MetaName = "Mod name(s)", HelpText = "The mod name(s) to remove")] + public IEnumerable Mods { get; set; } + } } diff --git a/Cmdline/Action/Repair.cs b/Cmdline/Action/Repair.cs index fa8eeeb4c7..85f363e3d4 100644 --- a/Cmdline/Action/Repair.cs +++ b/Cmdline/Action/Repair.cs @@ -1,92 +1,76 @@ using CommandLine; -using CommandLine.Text; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { + /// + /// Class for managing repair tasks. + /// public class Repair : ISubCommand { - public Repair() { } + private GameInstanceManager _manager; + private IUser _user; - internal class RepairSubOptions : VerbCommandOptions + /// + /// Run the 'repair' command. + /// + /// + public int RunCommand(GameInstanceManager manager, object args) { - [VerbOption("registry", HelpText = "Try to repair the CKAN registry")] - public InstanceSpecificOptions Registry { get; set; } + var s = args.ToString(); + var opts = s.Replace(s.Substring(0, s.LastIndexOf('.') + 1), "").Split('+'); + + CommonOptions options = new CommonOptions(); + _user = new ConsoleUser(options.Headless); + _manager = manager ?? new GameInstanceManager(_user); + var exitCode = options.Handle(_manager, _user); - [HelpVerbOption] - public string GetUsage(string verb) + if (exitCode != Exit.Ok) + return exitCode; + + switch (opts[1]) { - HelpText ht = HelpText.AutoBuild(this, verb); - // Add a usage prefix line - ht.AddPreOptionsLine(" "); - if (string.IsNullOrEmpty(verb)) - { - ht.AddPreOptionsLine("ckan repair - Attempt various automatic repairs"); - ht.AddPreOptionsLine($"Usage: ckan repair [options]"); - } - else - { - ht.AddPreOptionsLine("repair " + verb + " - " + GetDescription(verb)); - switch (verb) - { - // Commands with only --flag type options - case "registry": - default: - ht.AddPreOptionsLine($"Usage: ckan repair {verb} [options]"); - break; - } - } - return ht; + case "RepairRegistry": + exitCode = Registry(MainClass.GetGameInstance(_manager)); + break; + default: + exitCode = Exit.BadOpt; + break; } + + return exitCode; } - public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCommandOptions unparsed) + /// + public string GetUsage(string prefix, string[] args) { - int exitCode = Exit.OK; - // Parse and process our sub-verbs - Parser.Default.ParseArgumentsStrict(unparsed.options.ToArray(), new RepairSubOptions(), (string option, object suboptions) => - { - // ParseArgumentsStrict calls us unconditionally, even with bad arguments - if (!string.IsNullOrEmpty(option) && suboptions != null) - { - CommonOptions options = (CommonOptions)suboptions; - options.Merge(opts); - User = new ConsoleUser(options.Headless); - if (manager == null) - { - manager = new GameInstanceManager(User); - } - exitCode = options.Handle(manager, User); - if (exitCode != Exit.OK) - return; + if (args.Length == 1) + return $"{prefix} {args[0]} [options]"; - switch (option) - { - case "registry": - exitCode = Registry(MainClass.GetGameInstance(manager)); - break; - - default: - User.RaiseMessage("Unknown command: repair {0}", option); - exitCode = Exit.BADOPT; - break; - } - } - }, () => { exitCode = MainClass.AfterHelp(); }); - return exitCode; + switch (args[1]) + { + case "registry": + return $"{prefix} {args[0]} {args[1]} [options]"; + default: + return $"{prefix} {args[0]} [options]"; + } } - private IUser User { get; set; } - - /// - /// Try to repair our registry. - /// - private int Registry(CKAN.GameInstance ksp) + private int Registry(CKAN.GameInstance inst) { - RegistryManager manager = RegistryManager.Instance(ksp); + var manager = RegistryManager.Instance(inst); manager.registry.Repair(); manager.Save(); - User.RaiseMessage("Registry repairs attempted. Hope it helped."); - return Exit.OK; + _user.RaiseMessage("Attempted various registry repairs. Hope it helped."); + return Exit.Ok; } } + + [Verb("repair", HelpText = "Attempt various automatic repairs")] + [ChildVerbs(typeof(RepairRegistry))] + internal class RepairOptions + { + [VerbExclude] + [Verb("registry", HelpText = "Try to repair the CKAN registry")] + internal class RepairRegistry : InstanceSpecificOptions { } + } } diff --git a/Cmdline/Action/Replace.cs b/Cmdline/Action/Replace.cs index 8631e72a79..4ee273636f 100644 --- a/Cmdline/Action/Replace.cs +++ b/Cmdline/Action/Replace.cs @@ -1,177 +1,205 @@ using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; -using log4net; using CKAN.Versioning; +using CommandLine; +using log4net; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { + /// + /// Class for managing the replacing of mods. + /// public class Replace : ICommand { - private static readonly ILog log = LogManager.GetLogger(typeof(Replace)); + private static readonly ILog Log = LogManager.GetLogger(typeof(Replace)); - public IUser User { get; set; } + private readonly GameInstanceManager _manager; + private readonly IUser _user; - public Replace(CKAN.GameInstanceManager mgr, IUser user) + /// + /// Initializes a new instance of the class. + /// + /// The manager to provide game instances. + /// The current to raise messages to the user. + public Replace(GameInstanceManager manager, IUser user) { - manager = mgr; - User = user; + _manager = manager; + _user = user; } - private GameInstanceManager manager; - - public int RunCommand(CKAN.GameInstance ksp, object raw_options) + /// + /// Run the 'replace' command. + /// + /// + public int RunCommand(CKAN.GameInstance inst, object args) { - ReplaceOptions options = (ReplaceOptions) raw_options; - - if (options.ckan_file != null) + var opts = (ReplaceOptions)args; + if (!opts.Mods.Any() && !opts.All) { - options.modules.Add(MainClass.LoadCkanFromFile(ksp, options.ckan_file).identifier); + _user.RaiseMessage("replace [ ...] - argument(s) missing, perhaps you forgot it?"); + _user.RaiseMessage("If you want to replace all mods, use: ckan replace --all"); + return Exit.BadOpt; } - if (options.modules.Count == 0 && ! options.replace_all) + if (opts.CkanFile != null) { - // What? No mods specified? - User.RaiseMessage("Usage: ckan replace Mod [Mod2, ...]"); - User.RaiseMessage(" or ckan replace --all"); - return Exit.BADOPT; + opts.Mods.ToList().Add(MainClass.LoadCkanFromFile(inst, opts.CkanFile).identifier); } - // Prepare options. Can these all be done in the new() somehow? - var replace_ops = new RelationshipResolverOptions - { - with_all_suggests = options.with_all_suggests, - with_suggests = options.with_suggests, - with_recommends = !options.no_recommends, - allow_incompatible = options.allow_incompatible - }; - - var regMgr = RegistryManager.Instance(ksp); + var regMgr = RegistryManager.Instance(inst); var registry = regMgr.registry; - var to_replace = new List(); + var toReplace = new List(); - if (options.replace_all) + if (opts.All) { - log.Debug("Running Replace all"); + Log.Debug("Running replace all..."); var installed = new Dictionary(registry.Installed()); - foreach (KeyValuePair mod in installed) + foreach (var mod in installed) { - ModuleVersion current_version = mod.Value; - - if ((current_version is ProvidesModuleVersion) || (current_version is UnmanagedModuleVersion)) + var currentVersion = mod.Value; + if (currentVersion is ProvidesModuleVersion || currentVersion is UnmanagedModuleVersion) { continue; } - else + + try { - try - { - log.DebugFormat("Testing {0} {1} for possible replacement", mod.Key, mod.Value); - // Check if replacement is available + Log.DebugFormat("Testing \"{0}\" {1} for possible replacement...", mod.Key, mod.Value); - ModuleReplacement replacement = registry.GetReplacement(mod.Key, ksp.VersionCriteria()); - if (replacement != null) - { - // Replaceable - log.InfoFormat("Replacement {0} {1} found for {2} {3}", - replacement.ReplaceWith.identifier, replacement.ReplaceWith.version, - replacement.ToReplace.identifier, replacement.ToReplace.version); - to_replace.Add(replacement); - } - } - catch (ModuleNotFoundKraken) + // Check if replacement is available + var replacement = registry.GetReplacement(mod.Key, inst.VersionCriteria()); + if (replacement != null) { - log.InfoFormat("{0} is installed, but it or its replacement is not in the registry", - mod.Key); + // Replaceable + Log.InfoFormat("Replacement \"{0}\" {1} found for \"{2}\" {3}.", + replacement.ReplaceWith.identifier, replacement.ReplaceWith.version, + replacement.ToReplace.identifier, replacement.ToReplace.version); + toReplace.Add(replacement); } } + catch (ModuleNotFoundKraken) + { + Log.InfoFormat("\"{0}\" is installed, but it, or its replacement, is not in the registry.", mod.Key); + } } } else { - foreach (string mod in options.modules) + foreach (var mod in opts.Mods) { try { - log.DebugFormat("Checking that {0} is installed", mod); - CkanModule modToReplace = registry.GetInstalledVersion(mod); + Log.DebugFormat("Checking that \"{0}\" is installed...", mod); + var modToReplace = registry.GetInstalledVersion(mod); if (modToReplace != null) { - log.DebugFormat("Testing {0} {1} for possible replacement", modToReplace.identifier, modToReplace.version); + Log.DebugFormat("Testing \"{0}\" {1} for possible replacement...", modToReplace.identifier, modToReplace.version); try { // Check if replacement is available - ModuleReplacement replacement = registry.GetReplacement(modToReplace.identifier, ksp.VersionCriteria()); + var replacement = registry.GetReplacement(modToReplace.identifier, inst.VersionCriteria()); if (replacement != null) { // Replaceable - log.InfoFormat("Replacement {0} {1} found for {2} {3}", + Log.InfoFormat("Replacement \"{0}\" {1} found for \"{2}\" {3}.", replacement.ReplaceWith.identifier, replacement.ReplaceWith.version, replacement.ToReplace.identifier, replacement.ToReplace.version); - to_replace.Add(replacement); + toReplace.Add(replacement); } + if (modToReplace.replaced_by != null) { - log.InfoFormat("Attempt to replace {0} failed, replacement {1} is not compatible", - mod, modToReplace.replaced_by.name); + Log.InfoFormat("The attempt to replace \"{0}\" failed, the replacement \"{1}\" is not compatible with your version of {2}.", mod, modToReplace.replaced_by.name, inst.game.ShortName); } else { - log.InfoFormat("Mod {0} has no replacement defined for the current version {1}", - modToReplace.identifier, modToReplace.version); + Log.InfoFormat("The mod \"{0}\" has no replacement defined for the current version {1}.", modToReplace.identifier, modToReplace.version); } } catch (ModuleNotFoundKraken) { - log.InfoFormat("{0} is installed, but its replacement {1} is not in the registry", - mod, modToReplace.replaced_by.name); + Log.InfoFormat("\"{0}\" is installed, but its replacement \"{1}\" is not in the registry.", mod, modToReplace.replaced_by.name); } } } catch (ModuleNotFoundKraken kraken) { - User.RaiseMessage("Module {0} not found", kraken.module); + _user.RaiseMessage("The mod \"{0}\" could not found.", kraken.module); } } } - if (to_replace.Count() != 0) + + if (toReplace.Count != 0) { - User.RaiseMessage("\r\nReplacing modules...\r\n"); - foreach (ModuleReplacement r in to_replace) + _user.RaiseMessage("\r\nReplacing modules...\r\n"); + foreach (var r in toReplace) { - User.RaiseMessage("Replacement {0} {1} found for {2} {3}", + _user.RaiseMessage("Replacement \"{0}\" {1} found for \"{2}\" {3}.", r.ReplaceWith.identifier, r.ReplaceWith.version, r.ToReplace.identifier, r.ToReplace.version); } - bool ok = User.RaiseYesNoDialog("Continue?"); + var ok = _user.RaiseYesNoDialog("Continue?"); if (!ok) { - User.RaiseMessage("Replacements canceled at user request."); - return Exit.ERROR; + _user.RaiseMessage("Replacements canceled at user request."); + return Exit.Error; } - // TODO: These instances all need to go. + var replaceOpts = new RelationshipResolverOptions + { + with_all_suggests = opts.WithAllSuggests, + with_suggests = opts.WithSuggests, + with_recommends = !opts.NoRecommends, + allow_incompatible = opts.AllowIncompatible + }; + + // TODO: These instances all need to go try { HashSet possibleConfigOnlyDirs = null; - new ModuleInstaller(ksp, manager.Cache, User).Replace(to_replace, replace_ops, new NetAsyncModulesDownloader(User, manager.Cache), ref possibleConfigOnlyDirs, regMgr); - User.RaiseMessage(""); + new ModuleInstaller(inst, _manager.Cache, _user).Replace(toReplace, replaceOpts, new NetAsyncModulesDownloader(_user, _manager.Cache), ref possibleConfigOnlyDirs, regMgr); + _user.RaiseMessage(""); } - catch (DependencyNotSatisfiedKraken ex) + catch (DependencyNotSatisfiedKraken kraken) { - User.RaiseMessage("Dependencies not satisfied for replacement, {0} requires {1} {2} but it is not listed in the index, or not available for your version of KSP.", ex.parent, ex.module, ex.version); + _user.RaiseMessage("Dependencies not satisfied for replacement, \"{0}\" requires \"{1}\" {2} but it is not listed in the index, or not available for your version of {3}.", kraken.parent, kraken.module, kraken.version, inst.game.ShortName); } } else { - User.RaiseMessage("No replacements found."); - return Exit.OK; + _user.RaiseMessage("No replacements found."); + return Exit.Ok; } - return Exit.OK; + return Exit.Ok; } } + + [Verb("replace", HelpText = "Replace list of replaceable mods")] + internal class ReplaceOptions : InstanceSpecificOptions + { + [Option('c', "ckanfile", HelpText = "Local CKAN file to process")] + public string CkanFile { get; set; } + + [Option("no-recommends", HelpText = "Do not install recommended mods")] + public bool NoRecommends { get; set; } + + [Option("with-suggests", HelpText = "Install suggested mods")] + public bool WithSuggests { get; set; } + + [Option("with-all-suggests", HelpText = "Install suggested mods all the way down")] + public bool WithAllSuggests { get; set; } + + [Option("allow-incompatible", HelpText = "Install mods that are not compatible with the current game version")] + public bool AllowIncompatible { get; set; } + + [Option("all", HelpText = "Replace all available replaced mods")] + public bool All { get; set; } + + [Value(0, MetaName = "Mod name(s)", HelpText = "The mod name(s) to replace")] + public IEnumerable Mods { get; set; } + } } diff --git a/Cmdline/Action/Repo.cs b/Cmdline/Action/Repo.cs index a3b4b2b545..308256422b 100644 --- a/Cmdline/Action/Repo.cs +++ b/Cmdline/Action/Repo.cs @@ -1,345 +1,283 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Net; -using Newtonsoft.Json; using CommandLine; -using CommandLine.Text; using log4net; +using Newtonsoft.Json; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { - + /// + /// Class for managing CKAN repositories. + /// public class Repo : ISubCommand { - public Repo() { } + private static readonly ILog Log = LogManager.GetLogger(typeof(Repo)); - internal class RepoSubOptions : VerbCommandOptions - { - [VerbOption("available", HelpText = "List (canonical) available repositories")] - public AvailableOptions AvailableOptions { get; set; } + private GameInstanceManager _manager; + private IUser _user; - [VerbOption("list", HelpText = "List repositories")] - public ListOptions ListOptions { get; set; } - - [VerbOption("add", HelpText = "Add a repository")] - public AddOptions AddOptions { get; set; } + /// + /// Run the 'repo' command. + /// + /// + public int RunCommand(GameInstanceManager manager, object args) + { + var s = args.ToString(); + var opts = s.Replace(s.Substring(0, s.LastIndexOf('.') + 1), "").Split('+'); - [VerbOption("forget", HelpText = "Forget a repository")] - public ForgetOptions ForgetOptions { get; set; } + CommonOptions options = new CommonOptions(); + _user = new ConsoleUser(options.Headless); + _manager = manager ?? new GameInstanceManager(_user); + var exitCode = options.Handle(_manager, _user); - [VerbOption("default", HelpText = "Set the default repository")] - public DefaultOptions DefaultOptions { get; set; } + if (exitCode != Exit.Ok) + return exitCode; - [HelpVerbOption] - public string GetUsage(string verb) + switch (opts[1]) { - HelpText ht = HelpText.AutoBuild(this, verb); - // Add a usage prefix line - ht.AddPreOptionsLine(" "); - if (string.IsNullOrEmpty(verb)) - { - ht.AddPreOptionsLine("ckan repo - Manage CKAN repositories"); - ht.AddPreOptionsLine($"Usage: ckan repo [options]"); - } - else - { - ht.AddPreOptionsLine("repo " + verb + " - " + GetDescription(verb)); - switch (verb) - { - // First the commands with two arguments - case "add": - ht.AddPreOptionsLine($"Usage: ckan repo {verb} [options] name url"); - break; - - // Then the commands with one argument - case "remove": - case "forget": - case "default": - ht.AddPreOptionsLine($"Usage: ckan repo {verb} [options] name"); - break; - - // Now the commands with only --flag type options - case "available": - case "list": - default: - ht.AddPreOptionsLine($"Usage: ckan repo {verb} [options]"); - break; - } - } - return ht; + case "AddRepo": + exitCode = AddRepository(args); + break; + case "AvailableRepo": + exitCode = AvailableRepositories(); + break; + case "DefaultRepo": + exitCode = DefaultRepository(args); + break; + case "ForgetRepo": + exitCode = ForgetRepository(args); + break; + case "ListRepo": + exitCode = ListRepositories(); + break; + case "ResetRepo": + exitCode = ResetRepository(); + break; + default: + exitCode = Exit.BadOpt; + break; } - } - - internal class AvailableOptions : CommonOptions { } - internal class ListOptions : InstanceSpecificOptions { } - internal class AddOptions : InstanceSpecificOptions - { - [ValueOption(0)] public string name { get; set; } - [ValueOption(1)] public string uri { get; set; } + return exitCode; } - internal class DefaultOptions : InstanceSpecificOptions + /// + public string GetUsage(string prefix, string[] args) { - [ValueOption(0)] public string uri { get; set; } - } + if (args.Length == 1) + return $"{prefix} {args[0]} [options]"; - internal class ForgetOptions : InstanceSpecificOptions - { - [ValueOption(0)] public string name { get; set; } + switch (args[1]) + { + case "add": + return $"{prefix} {args[0]} {args[1]} [options] "; + case "default": + return $"{prefix} {args[0]} {args[1]} [options] "; + case "forget": + return $"{prefix} {args[0]} {args[1]} [options] "; + case "available": + case "list": + case "reset": + return $"{prefix} {args[0]} {args[1]} [options]"; + default: + return $"{prefix} {args[0]} [options]"; + } } - // This is required by ISubCommand - public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCommandOptions unparsed) + private int AddRepository(object args) { - string[] args = unparsed.options.ToArray(); - - #region Aliases + var opts = (RepoOptions.AddRepo)args; + if (opts.Name == null) + { + _user.RaiseMessage("add - argument(s) missing, perhaps you forgot it?"); + return Exit.BadOpt; + } - for (int i = 0; i < args.Length; i++) + if (opts.Url == null) { - switch (args[i]) + RepositoryList repositoryList; + try { - case "remove": - args[i] = "forget"; - break; + repositoryList = FetchMasterRepositoryList(); + } + catch + { + _user.RaiseError("Couldn't fetch CKAN repositories master list from \"{0}\".", _manager.CurrentInstance.game.RepositoryListURL.ToString()); + return Exit.Error; } - } - - #endregion - - int exitCode = Exit.OK; - // Parse and process our sub-verbs - Parser.Default.ParseArgumentsStrict(args, new RepoSubOptions(), (string option, object suboptions) => - { - // ParseArgumentsStrict calls us unconditionally, even with bad arguments - if (!string.IsNullOrEmpty(option) && suboptions != null) + foreach (var candidate in repositoryList.repositories) { - CommonOptions options = (CommonOptions)suboptions; - options.Merge(opts); - User = new ConsoleUser(options.Headless); - Manager = manager ?? new GameInstanceManager(User); - exitCode = options.Handle(Manager, User); - if (exitCode != Exit.OK) - return; - - switch (option) + if (string.Equals(candidate.name, opts.Name, StringComparison.OrdinalIgnoreCase)) { - case "available": - exitCode = AvailableRepositories(); - break; - - case "list": - exitCode = ListRepositories(); - break; - - case "add": - exitCode = AddRepository((AddOptions)suboptions); - break; - - case "remove": - case "forget": - exitCode = ForgetRepository((ForgetOptions)suboptions); - break; - - case "default": - exitCode = DefaultRepository((DefaultOptions)suboptions); - break; - - default: - User.RaiseMessage("Unknown command: repo {0}", option); - exitCode = Exit.BADOPT; - break; + opts.Name = candidate.name; + opts.Url = candidate.uri.ToString(); } } - }, () => { exitCode = MainClass.AfterHelp(); }); - return exitCode; - } - private RepositoryList FetchMasterRepositoryList(Uri master_uri = null) - { - if (master_uri == null) + // Nothing found in the master list? + if (opts.Url == null) + { + _user.RaiseMessage("add - argument(s) missing, perhaps you forgot it?"); + return Exit.BadOpt; + } + } + + Log.DebugFormat("About to add the repository \"{0}\" - \"{1}\".", opts.Name, opts.Url); + var manager = RegistryManager.Instance(MainClass.GetGameInstance(_manager)); + var repositories = manager.registry.Repositories; + + if (repositories.ContainsKey(opts.Name)) { - master_uri = MainClass.GetGameInstance(Manager).game.RepositoryListURL; + _user.RaiseMessage("A repository with the name \"{0}\" already exists, aborting...", opts.Name); + return Exit.BadOpt; } - string json = Net.DownloadText(master_uri); - return JsonConvert.DeserializeObject(json); + repositories.Add(opts.Name, new Repository(opts.Name, opts.Url)); + + _user.RaiseMessage("Successfully added the repository \"{0}\" - \"{1}\".", opts.Name, opts.Url); + manager.Save(); + + return Exit.Ok; } private int AvailableRepositories() { - User.RaiseMessage("Listing all (canonical) available CKAN repositories:"); + _user.RaiseMessage("Listing all (canonical) available CKAN repositories:"); RepositoryList repositories; - try { repositories = FetchMasterRepositoryList(); } catch { - User.RaiseError("Couldn't fetch CKAN repositories master list from {0}", MainClass.GetGameInstance(Manager).game.RepositoryListURL.ToString()); - return Exit.ERROR; + _user.RaiseError("Couldn't fetch CKAN repositories master list from \"{0}\"", MainClass.GetGameInstance(_manager).game.RepositoryListURL.ToString()); + return Exit.Error; } - int maxNameLen = 0; - foreach (Repository repository in repositories.repositories) + var maxNameLen = 0; + foreach (var repository in repositories.repositories) { maxNameLen = Math.Max(maxNameLen, repository.name.Length); } - foreach (Repository repository in repositories.repositories) + foreach (var repository in repositories.repositories) { - User.RaiseMessage(" {0}: {1}", repository.name.PadRight(maxNameLen), repository.uri); + _user.RaiseMessage(" {0}: {1}", repository.name.PadRight(maxNameLen), repository.uri); } - return Exit.OK; + return Exit.Ok; } - private int ListRepositories() + private int DefaultRepository(object args) { - var manager = RegistryManager.Instance(MainClass.GetGameInstance(Manager)); - User.RaiseMessage("Listing all known repositories:"); - SortedDictionary repositories = manager.registry.Repositories; - - int maxNameLen = 0; - foreach (Repository repository in repositories.Values) + var opts = (RepoOptions.DefaultRepo)args; + if (opts.Url == null) { - maxNameLen = Math.Max(maxNameLen, repository.name.Length); + _user.RaiseMessage("default - argument missing, perhaps you forgot it?"); + return Exit.BadOpt; } - foreach (Repository repository in repositories.Values) + Log.DebugFormat("About to set \"{0}\" as the {1} repository.", opts.Url, Repository.default_ckan_repo_name); + var manager = RegistryManager.Instance(MainClass.GetGameInstance(_manager)); + var repositories = manager.registry.Repositories; + + if (repositories.ContainsKey(Repository.default_ckan_repo_name)) { - User.RaiseMessage(" {0}: {1}: {2}", repository.name.PadRight(maxNameLen), repository.priority, repository.uri); + repositories.Remove(Repository.default_ckan_repo_name); } - return Exit.OK; + repositories.Add(Repository.default_ckan_repo_name, new Repository(Repository.default_ckan_repo_name, opts.Url)); + + _user.RaiseMessage("Set the {0} repository to \"{1}\".", Repository.default_ckan_repo_name, opts.Url); + manager.Save(); + + return Exit.Ok; } - private int AddRepository(AddOptions options) + private int ForgetRepository(object args) { - RegistryManager manager = RegistryManager.Instance(MainClass.GetGameInstance(Manager)); - - if (options.name == null) + var opts = (RepoOptions.ForgetRepo)args; + if (opts.Name == null) { - User.RaiseMessage("add [ ] - argument missing, perhaps you forgot it?"); - return Exit.BADOPT; + _user.RaiseMessage("forget - argument missing, perhaps you forgot it?"); + return Exit.BadOpt; } - if (options.uri == null) - { - RepositoryList repositoryList; - - try - { - repositoryList = FetchMasterRepositoryList(); - } - catch - { - User.RaiseError("Couldn't fetch CKAN repositories master list from {0}", Manager.CurrentInstance.game.RepositoryListURL.ToString()); - return Exit.ERROR; - } - - foreach (Repository candidate in repositoryList.repositories) - { - if (String.Equals(candidate.name, options.name, StringComparison.OrdinalIgnoreCase)) - { - options.name = candidate.name; - options.uri = candidate.uri.ToString(); - } - } + Log.DebugFormat("About to forget the repository \"{0}\".", opts.Name); + var manager = RegistryManager.Instance(MainClass.GetGameInstance(_manager)); + var repositories = manager.registry.Repositories; - // Nothing found in the master list? - if (options.uri == null) + var name = opts.Name; + if (!repositories.ContainsKey(opts.Name)) + { + name = repositories.Keys.FirstOrDefault(repo => repo.Equals(opts.Name, StringComparison.OrdinalIgnoreCase)); + if (name == null) { - User.RaiseMessage("Name {0} not found in master list, please provide name and uri.", options.name); - return Exit.BADOPT; + _user.RaiseMessage("Couldn't find a repository with the name \"{0}\", aborting...", opts.Name); + return Exit.BadOpt; } - } - - log.DebugFormat("About to add repository '{0}' - '{1}'", options.name, options.uri); - SortedDictionary repositories = manager.registry.Repositories; - if (repositories.ContainsKey(options.name)) - { - User.RaiseMessage("Repository with name \"{0}\" already exists, aborting..", options.name); - return Exit.BADOPT; + _user.RaiseMessage("Removing insensitive match \"{0}\".", name); } - repositories.Add(options.name, new Repository(options.name, options.uri)); + repositories.Remove(name); - User.RaiseMessage("Added repository '{0}' - '{1}'", options.name, options.uri); + _user.RaiseMessage("Successfully removed \"{0}\".", opts.Name); manager.Save(); - return Exit.OK; + return Exit.Ok; } - private int ForgetRepository(ForgetOptions options) + private int ListRepositories() { - if (options.name == null) + _user.RaiseMessage("Listing all known repositories:"); + var manager = RegistryManager.Instance(MainClass.GetGameInstance(_manager)); + var repositories = manager.registry.Repositories; + + var maxNameLen = 0; + foreach (var repository in repositories.Values) { - User.RaiseError("forget - argument missing, perhaps you forgot it?"); - return Exit.BADOPT; + maxNameLen = Math.Max(maxNameLen, repository.name.Length); } - RegistryManager manager = RegistryManager.Instance(MainClass.GetGameInstance(Manager)); - var registry = manager.registry; - log.DebugFormat("About to forget repository '{0}'", options.name); - - var repos = registry.Repositories; - - string name = options.name; - if (!repos.ContainsKey(options.name)) + foreach (var repository in repositories.Values) { - name = repos.Keys.FirstOrDefault(repo => repo.Equals(options.name, StringComparison.OrdinalIgnoreCase)); - if (name == null) - { - User.RaiseMessage("Couldn't find repository with name \"{0}\", aborting..", options.name); - return Exit.BADOPT; - } - User.RaiseMessage("Removing insensitive match \"{0}\"", name); + _user.RaiseMessage(" {0}: {1}: {2}", repository.name.PadRight(maxNameLen), repository.priority, repository.uri); } - registry.Repositories.Remove(name); - User.RaiseMessage("Successfully removed \"{0}\"", options.name); - manager.Save(); - - return Exit.OK; + return Exit.Ok; } - private int DefaultRepository(DefaultOptions options) + private int ResetRepository() { - RegistryManager manager = RegistryManager.Instance(MainClass.GetGameInstance(Manager)); - - if (options.uri == null) - { - User.RaiseMessage("default - argument missing, perhaps you forgot it?"); - return Exit.BADOPT; - } - - log.DebugFormat("About to add repository '{0}' - '{1}'", Repository.default_ckan_repo_name, options.uri); - SortedDictionary repositories = manager.registry.Repositories; + Log.DebugFormat("About to reset the {0} repository.", Repository.default_ckan_repo_name); + var manager = RegistryManager.Instance(MainClass.GetGameInstance(_manager)); + var repositories = manager.registry.Repositories; if (repositories.ContainsKey(Repository.default_ckan_repo_name)) { repositories.Remove(Repository.default_ckan_repo_name); } - repositories.Add(Repository.default_ckan_repo_name, new Repository( - Repository.default_ckan_repo_name, options.uri)); + repositories.Add(Repository.default_ckan_repo_name, new Repository(Repository.default_ckan_repo_name, MainClass.GetGameInstance(_manager).game.DefaultRepositoryURL)); - User.RaiseMessage("Set {0} repository to '{1}'", Repository.default_ckan_repo_name, options.uri); + _user.RaiseMessage("Reset the {0} repository to \"{1}\".", Repository.default_ckan_repo_name, MainClass.GetGameInstance(_manager).game.DefaultRepositoryURL); manager.Save(); - return Exit.OK; + return Exit.Ok; } - private GameInstanceManager Manager { get; set; } - private IUser User { get; set; } + private RepositoryList FetchMasterRepositoryList(Uri masterUrl = null) + { + if (masterUrl == null) + { + masterUrl = MainClass.GetGameInstance(_manager).game.RepositoryListURL; + } - private static readonly ILog log = LogManager.GetLogger(typeof (Repo)); + var json = Net.DownloadText(masterUrl); + return JsonConvert.DeserializeObject(json); + } } public struct RepositoryList @@ -347,4 +285,47 @@ public struct RepositoryList public Repository[] repositories; } + [Verb("repo", HelpText = "Manage CKAN repositories")] + [ChildVerbs(typeof(AddRepo), typeof(AvailableRepo), typeof(DefaultRepo), typeof(ForgetRepo), typeof(ListRepo), typeof(ResetRepo))] + internal class RepoOptions + { + [VerbExclude] + [Verb("add", HelpText = "Add a repository")] + internal class AddRepo : InstanceSpecificOptions + { + [Value(0, MetaName = "Name", HelpText = "The name of the new repository")] + public string Name { get; set; } + + [Value(1, MetaName = "Url", HelpText = "The url of the new repository")] + public string Url { get; set; } + } + + [VerbExclude] + [Verb("available", HelpText = "List (canonical) available repositories")] + internal class AvailableRepo : CommonOptions { } + + [VerbExclude] + [Verb("default", HelpText = "Set the default repository")] + internal class DefaultRepo : InstanceSpecificOptions + { + [Value(0, MetaName = "Url", HelpText = "The url of the repository to make the default")] + public string Url { get; set; } + } + + [VerbExclude] + [Verb("forget", HelpText = "Forget a repository")] + internal class ForgetRepo : InstanceSpecificOptions + { + [Value(0, MetaName = "Name", HelpText = "The name of the repository to remove")] + public string Name { get; set; } + } + + [VerbExclude] + [Verb("list", HelpText = "List repositories")] + internal class ListRepo : InstanceSpecificOptions { } + + [VerbExclude] + [Verb("reset", HelpText = "Set the default repository to the default")] + internal class ResetRepo : InstanceSpecificOptions { } + } } diff --git a/Cmdline/Action/Search.cs b/Cmdline/Action/Search.cs index 4064480f01..db0a8ff161 100644 --- a/Cmdline/Action/Search.cs +++ b/Cmdline/Action/Search.cs @@ -1,203 +1,217 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using CKAN.Games; +using CKAN.Versioning; +using CommandLine; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { + /// + /// Class for searching mods. + /// public class Search : ICommand { - public IUser user { get; set; } + private readonly IUser _user; + /// + /// Initializes a new instance of the class. + /// + /// The current to raise messages to the user. public Search(IUser user) { - this.user = user; + _user = user; } - public int RunCommand(CKAN.GameInstance ksp, object raw_options) + /// + /// Run the 'search' command. + /// + /// + public int RunCommand(CKAN.GameInstance inst, object args) { - SearchOptions options = (SearchOptions)raw_options; - - // Check the input. - if (String.IsNullOrWhiteSpace(options.search_term) && String.IsNullOrWhiteSpace(options.author_term)) + var opts = (SearchOptions)args; + if (string.IsNullOrWhiteSpace(opts.SearchTerm) && string.IsNullOrWhiteSpace(opts.Author)) { - user.RaiseError("No search term?"); - - return Exit.BADOPT; + _user.RaiseMessage("search - argument missing, perhaps you forgot it?"); + _user.RaiseMessage("If you just want to search by author, use: ckan search --author "); + return Exit.BadOpt; } - List matching_compatible = PerformSearch(ksp, options.search_term, options.author_term, false); - List matching_incompatible = new List(); - if (options.all) + var matchingCompatible = PerformSearch(inst, opts.SearchTerm, opts.Author); + var matchingIncompatible = new List(); + if (opts.All) { - matching_incompatible = PerformSearch(ksp, options.search_term, options.author_term, true); + matchingIncompatible = PerformSearch(inst, opts.SearchTerm, opts.Author, true); } - // Show how many matches we have. - if (options.all && !String.IsNullOrWhiteSpace(options.author_term)) - { - user.RaiseMessage("Found {0} compatible and {1} incompatible mods matching \"{2}\" by \"{3}\".", - matching_compatible.Count().ToString(), - matching_incompatible.Count().ToString(), - options.search_term, - options.author_term); - } - else if (options.all && String.IsNullOrWhiteSpace(options.author_term)) + string message; + + // Return if no mods were found + if (!matchingCompatible.Any() && !matchingIncompatible.Any()) { - user.RaiseMessage("Found {0} compatible and {1} incompatible mods matching \"{2}\".", - matching_compatible.Count().ToString(), - matching_incompatible.Count().ToString(), - options.search_term); + message = "Couldn't find any mod"; + + if (!string.IsNullOrWhiteSpace(opts.SearchTerm)) + { + message += string.Format(" matching \"{0}\"", opts.SearchTerm); + } + + if (!string.IsNullOrWhiteSpace(opts.Author)) + { + message += string.Format(" by \"{0}\"", opts.Author); + } + + _user.RaiseMessage("{0}.", message); + return Exit.Ok; } - else if (!options.all && !String.IsNullOrWhiteSpace(options.author_term)) + + // Show how many matches we have + message = string.Format("Found {0} compatible", matchingCompatible.Count.ToString()); + + if (opts.All) { - user.RaiseMessage("Found {0} compatible mods matching \"{1}\" by \"{2}\".", - matching_compatible.Count().ToString(), - options.search_term, - options.author_term); + message += string.Format(" and {0} incompatible", matchingIncompatible.Count.ToString()); } - else if (!options.all && String.IsNullOrWhiteSpace(options.author_term)) + + message += " mods"; + + if (!string.IsNullOrWhiteSpace(opts.SearchTerm)) { - user.RaiseMessage("Found {0} compatible mods matching \"{1}\".", - matching_compatible.Count().ToString(), - options.search_term); + message += string.Format(" matching \"{0}\"", opts.SearchTerm); } - // Present the results. - if (!matching_compatible.Any() && (!options.all || !matching_incompatible.Any())) + if (!string.IsNullOrWhiteSpace(opts.Author)) { - return Exit.OK; + message += string.Format(" by \"{0}\"", opts.Author); } - if (options.detail) + _user.RaiseMessage("{0}.", message); + + // Show detailed information of matches + if (opts.Detail) { - user.RaiseMessage("Matching compatible mods:"); - foreach (CkanModule mod in matching_compatible) + if (matchingCompatible.Any()) { - user.RaiseMessage("* {0} ({1}) - {2} by {3} - {4}", - mod.identifier, - mod.version, - mod.name, - mod.author == null ? "N/A" : String.Join(", ", mod.author), - mod.@abstract); + _user.RaiseMessage("Matching compatible mods:"); + foreach (var mod in matchingCompatible) + { + _user.RaiseMessage("* {0} ({1}) - {2} by {3} - {4}", + mod.identifier, + mod.version, + mod.name, + mod.author == null ? "N/A" : string.Join(", ", mod.author), + mod.@abstract); + } } - if (matching_incompatible.Any()) + if (matchingIncompatible.Any()) { - user.RaiseMessage("Matching incompatible mods:"); - foreach (CkanModule mod in matching_incompatible) + _user.RaiseMessage("Matching incompatible mods:"); + foreach (var mod in matchingIncompatible) { - Registry.GetMinMaxVersions(new List { mod } , out _, out _, out var minKsp, out var maxKsp); - string GameVersion = Versioning.GameVersionRange.VersionSpan(ksp.game, minKsp, maxKsp).ToString(); + Registry.GetMinMaxVersions(new List { mod }, out _, out _, out GameVersion minVer, out GameVersion maxVer); + var gameVersion = GameVersionRange.VersionSpan(inst.game, minVer, maxVer); - user.RaiseMessage("* {0} ({1} - {2}) - {3} by {4} - {5}", + _user.RaiseMessage("* {0} ({1} - {2}) - {3} by {4} - {5}", mod.identifier, mod.version, - GameVersion, + gameVersion, mod.name, - mod.author == null ? "N/A" : String.Join(", ", mod.author), + mod.author == null ? "N/A" : string.Join(", ", mod.author), mod.@abstract); } } } else { - List matching = matching_compatible.Concat(matching_incompatible).ToList(); + var matching = matchingCompatible.Concat(matchingIncompatible).ToList(); matching.Sort((x, y) => string.Compare(x.identifier, y.identifier, StringComparison.Ordinal)); - foreach (CkanModule mod in matching) + foreach (var mod in matching) { - user.RaiseMessage(mod.identifier); + _user.RaiseMessage(mod.identifier); } } - return Exit.OK; + return Exit.Ok; } /// - /// Searches for the term in the list of compatible or incompatible modules for the ksp instance. - /// Looks in name, identifier and description fields, and if given, restricts to authors matching the author term. + /// Searches for the in the list of compatible or incompatible modules for the game instance. + /// Looks in name, identifier and description fields, and if given, restricts to authors matching the term. /// - /// List of matching modules. - /// The KSP instance to perform the search for. - /// The search term. Case insensitive. - public List PerformSearch(CKAN.GameInstance ksp, string term, string author = null, bool searchIncompatible = false) + /// The game instance which to handle with mods. + /// The string to search for in mods. This is case insensitive. + /// The author to search for. Default is . + /// Boolean to also search for incompatible mods or not. Default is . + /// A of matching modules as a . + public List PerformSearch(CKAN.GameInstance inst, string term, string author = null, bool searchIncompatible = false) { - // Remove spaces and special characters from the search term. - term = String.IsNullOrWhiteSpace(term) ? string.Empty : CkanModule.nonAlphaNums.Replace(term, ""); - author = String.IsNullOrWhiteSpace(author) ? string.Empty : CkanModule.nonAlphaNums.Replace(author, ""); + // Remove spaces and special characters from the search term + term = string.IsNullOrWhiteSpace(term) ? string.Empty : CkanModule.nonAlphaNums.Replace(term, ""); + author = string.IsNullOrWhiteSpace(author) ? string.Empty : CkanModule.nonAlphaNums.Replace(author, ""); - var registry = RegistryManager.Instance(ksp).registry; + var registry = RegistryManager.Instance(inst).registry; if (!searchIncompatible) { return registry - .CompatibleModules(ksp.VersionCriteria()) - .Where((module) => - { - // Look for a match in each string. - return (module.SearchableName.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 - || module.SearchableIdentifier.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 - || module.SearchableAbstract.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 - || module.SearchableDescription.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1) - && module.SearchableAuthors.Any((auth) => auth.IndexOf(author, StringComparison.OrdinalIgnoreCase) > -1); - }).ToList(); + .CompatibleModules(inst.VersionCriteria()) + .Where(module => + { + // Look for a match in each string + return (module.SearchableName.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableIdentifier.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableAbstract.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableDescription.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1) + && module.SearchableAuthors.Any(auth => auth.IndexOf(author, StringComparison.OrdinalIgnoreCase) > -1); + }).ToList(); } - else - { - return registry - .IncompatibleModules(ksp.VersionCriteria()) - .Where((module) => + + return registry + .IncompatibleModules(inst.VersionCriteria()) + .Where(module => { - // Look for a match in each string. + // Look for a match in each string return (module.SearchableName.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 - || module.SearchableIdentifier.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 - || module.SearchableAbstract.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 - || module.SearchableDescription.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1) - && module.SearchableAuthors.Any((auth) => auth.IndexOf(author, StringComparison.OrdinalIgnoreCase) > -1); + || module.SearchableIdentifier.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableAbstract.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableDescription.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1) + && module.SearchableAuthors.Any(auth => auth.IndexOf(author, StringComparison.OrdinalIgnoreCase) > -1); }).ToList(); - } } - /// - /// Find the proper capitalization of an identifier - /// - /// List of valid mods from the registry - /// Identifier to be treated as case insensitive - /// - /// The mod's properly capitalized identifier, or the original string if it doesn't exist - /// private static string CaseInsensitiveExactMatch(List mods, string module) { // Look for a matching mod with a case insensitive search - CkanModule found = mods.FirstOrDefault( - (CkanModule m) => string.Equals(m.identifier, module, StringComparison.OrdinalIgnoreCase) - ); + var found = mods.FirstOrDefault(m => string.Equals(m.identifier, module, StringComparison.OrdinalIgnoreCase)); + // If we don't find anything, use the original string so the main code can raise errors return found?.identifier ?? module; } /// - /// Convert case insensitive mod names from the user to case sensitive identifiers + /// Convert case insensitive mod names from the user to case sensitive identifiers. /// - /// Game instance forgetting the mods - /// List of strings to convert, format 'identifier' or 'identifier=version' - public static void AdjustModulesCase(CKAN.GameInstance ksp, List modules) + /// The game instance which to handle with mods. + /// The of strings to convert. + public static void AdjustModulesCase(CKAN.GameInstance inst, List modules) { - IRegistryQuerier registry = RegistryManager.Instance(ksp).registry; + IRegistryQuerier registry = RegistryManager.Instance(inst).registry; + // Get the list of all compatible and incompatible mods - List mods = registry.CompatibleModules(ksp.VersionCriteria()).ToList(); - mods.AddRange(registry.IncompatibleModules(ksp.VersionCriteria())); - for (int i = 0; i < modules.Count; ++i) + var mods = registry.CompatibleModules(inst.VersionCriteria()).ToList(); + mods.AddRange(registry.IncompatibleModules(inst.VersionCriteria())); + + for (var i = 0; i < modules.Count; ++i) { - Match match = CkanModule.idAndVersionMatcher.Match(modules[i]); + var match = CkanModule.idAndVersionMatcher.Match(modules[i]); if (match.Success) { - // Handle name=version format - string ident = match.Groups["mod"].Value; - string version = match.Groups["version"].Value; - modules[i] = $"{CaseInsensitiveExactMatch(mods, ident)}={version}"; + // Handle 'name=version' format + var ident = match.Groups["mod"].Value; + var version = match.Groups["version"].Value; + modules[i] = string.Format("{0}={1}", CaseInsensitiveExactMatch(mods, ident), version); } else { @@ -205,6 +219,21 @@ public static void AdjustModulesCase(CKAN.GameInstance ksp, List modules } } } + } + + [Verb("search", HelpText = "Search for mods")] + internal class SearchOptions : InstanceSpecificOptions + { + [Option("detail", HelpText = "Show full name, latest compatible version and short description of each module")] + public bool Detail { get; set; } + + [Option("all", HelpText = "Show incompatible mods too")] + public bool All { get; set; } + + [Option("author", HelpText = "Limit search results to mods by matching author")] + public string Author { get; set; } + [Value(0, MetaName = "Search term", HelpText = "The term to search for")] + public string SearchTerm { get; set; } } } diff --git a/Cmdline/Action/Show.cs b/Cmdline/Action/Show.cs index e7c55d6ec9..d92f972497 100644 --- a/Cmdline/Action/Show.cs +++ b/Cmdline/Action/Show.cs @@ -2,90 +2,98 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using CommandLine; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { + /// + /// Class for showing information about a mod. + /// public class Show : ICommand { - public IUser user { get; set; } + private readonly IUser _user; + /// + /// Initializes a new instance of the class. + /// + /// The current to raise messages to the user. public Show(IUser user) { - this.user = user; + _user = user; } - public int RunCommand(CKAN.GameInstance ksp, object raw_options) + /// + /// Run the 'show' command. + /// + /// + public int RunCommand(CKAN.GameInstance inst, object args) { - ShowOptions options = (ShowOptions) raw_options; - - if (options.Modname == null) + var opts = (ShowOptions)args; + if (string.IsNullOrWhiteSpace(opts.ModName)) { - // empty argument - user.RaiseMessage("show - module name argument missing, perhaps you forgot it?"); - return Exit.BADOPT; + _user.RaiseMessage("show - argument missing, perhaps you forgot it?"); + return Exit.BadOpt; } - // Check installed modules for an exact match. - var registry = RegistryManager.Instance(ksp).registry; - var installedModuleToShow = registry.InstalledModule(options.Modname); + // Check installed modules for an exact match + var registry = RegistryManager.Instance(inst).registry; + var installedModuleToShow = registry.InstalledModule(opts.ModName); if (installedModuleToShow != null) { - // Show the installed module. + // Show the installed module return ShowMod(installedModuleToShow); } // Module was not installed, look for an exact match in the available modules, // either by "name" (the user-friendly display name) or by identifier - CkanModule moduleToShow = registry - .CompatibleModules(ksp.VersionCriteria()) - .SingleOrDefault( - mod => mod.name == options.Modname - || mod.identifier == options.Modname - ); + var moduleToShow = registry + .CompatibleModules(inst.VersionCriteria()) + .SingleOrDefault( + mod => mod.name == opts.ModName + || mod.identifier == opts.ModName + ); if (moduleToShow == null) { - // No exact match found. Try to look for a close match for this KSP version. - user.RaiseMessage("{0} not found or installed.", options.Modname); - user.RaiseMessage("Looking for close matches in mods compatible with KSP {0}.", ksp.Version()); + // No exact match found. Try to look for a close match for this game version + _user.RaiseMessage("\"{0}\" was not found or installed.\r\nLooking for close matches in mods compatible with {1} {2}.", opts.ModName, inst.game.ShortName, inst.Version()); - Search search = new Search(user); - var matches = search.PerformSearch(ksp, options.Modname); + var search = new Search(_user); + var matches = search.PerformSearch(inst, opts.ModName); - // Display the results of the search. + // Display the results of the search if (!matches.Any()) { - // No matches found. - user.RaiseMessage("No close matches found."); - return Exit.BADOPT; + // No matches found + _user.RaiseMessage("No close matches found."); + return Exit.BadOpt; } - else if (matches.Count() == 1) - { - // If there is only 1 match, display it. - user.RaiseMessage("Found 1 close match: {0}", matches[0].name); - user.RaiseMessage(""); + if (matches.Count == 1) + { + // If there is only 1 match, display it + _user.RaiseMessage("Found 1 close match: \"{0}\".\r\n", matches[0].name); moduleToShow = matches[0]; } else { - // Display the found close matches. - string[] strings_matches = new string[matches.Count]; + // Display the found close matches + var stringsMatches = new string[matches.Count]; - for (int i = 0; i < matches.Count; i++) + for (var i = 0; i < matches.Count; i++) { - strings_matches[i] = matches[i].name; + stringsMatches[i] = matches[i].name; } - int selection = user.RaiseSelectionDialog("Close matches", strings_matches); + var selection = _user.RaiseSelectionDialog("Close matches", stringsMatches); if (selection < 0) { - return Exit.BADOPT; + return Exit.BadOpt; } - // Mark the selection as the one to show. + // Mark the selection as the one to show moduleToShow = matches[selection]; } } @@ -93,158 +101,167 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) return ShowMod(moduleToShow); } - /// - /// Shows information about the mod. - /// - /// Success status. - /// The module to show. - public int ShowMod(InstalledModule module) + private int ShowMod(InstalledModule module) { - // Display the basic info. - int return_value = ShowMod(module.Module); + // Display the basic info + var returnValue = ShowMod(module.Module); - // Display InstalledModule specific information. - ICollection files = module.Files as ICollection; - if (files == null) throw new InvalidCastException(); + // Display InstalledModule specific information + if (!(module.Files is ICollection files)) + { + throw new InvalidCastException(); + } if (!module.Module.IsDLC) { - user.RaiseMessage("\r\nShowing {0} installed files:", files.Count); - foreach (string file in files) + _user.RaiseMessage("\r\nShowing {0} installed files:", files.Count); + foreach (var file in files) { - user.RaiseMessage("- {0}", file); + _user.RaiseMessage("- {0}", file); } } - return return_value; + return returnValue; } - /// - /// Shows information about the mod. - /// - /// Success status. - /// The module to show. - public int ShowMod(CkanModule module) + private int ShowMod(CkanModule module) { - #region Abstract and description + // Abstract and description + if (!string.IsNullOrEmpty(module.@abstract)) { - user.RaiseMessage("{0}: {1}", module.name, module.@abstract); + _user.RaiseMessage("{0}: {1}", module.name, module.@abstract); } else { - user.RaiseMessage("{0}", module.name); + _user.RaiseMessage("{0}", module.name); } if (!string.IsNullOrEmpty(module.description)) { - user.RaiseMessage("\r\n{0}\r\n", module.description); + _user.RaiseMessage("\r\n{0}\r\n", module.description); } - #endregion - #region General info (author, version...) - user.RaiseMessage("\r\nModule info:"); - user.RaiseMessage("- version:\t{0}", module.version); + // General info (author, version...) + + _user.RaiseMessage("\r\nModule info:\r\n- version:\t{0}", module.version); if (module.author != null) { - user.RaiseMessage("- authors:\t{0}", string.Join(", ", module.author)); + _user.RaiseMessage("- authors:\t{0}", string.Join(", ", module.author)); } else { // Did you know that authors are optional in the spec? - // You do now. #673. - user.RaiseMessage("- authors:\tUNKNOWN"); + // You do now. #673 + _user.RaiseMessage("- authors:\tUNKNOWN"); } - user.RaiseMessage("- status:\t{0}", module.release_status); - user.RaiseMessage("- license:\t{0}", string.Join(", ", module.license)); - #endregion + _user.RaiseMessage("- status:\t{0}", module.release_status); + _user.RaiseMessage("- license:\t{0}", string.Join(", ", module.license)); + + // Relationships - #region Relationships if (module.depends != null && module.depends.Count > 0) { - user.RaiseMessage("\r\nDepends:"); - foreach (RelationshipDescriptor dep in module.depends) - user.RaiseMessage("- {0}", RelationshipToPrintableString(dep)); + _user.RaiseMessage("\r\nDepends:"); + foreach (var dep in module.depends) + { + _user.RaiseMessage("- {0}", RelationshipToPrintableString(dep)); + } } if (module.recommends != null && module.recommends.Count > 0) { - user.RaiseMessage("\r\nRecommends:"); - foreach (RelationshipDescriptor dep in module.recommends) - user.RaiseMessage("- {0}", RelationshipToPrintableString(dep)); + _user.RaiseMessage("\r\nRecommends:"); + foreach (var dep in module.recommends) + { + _user.RaiseMessage("- {0}", RelationshipToPrintableString(dep)); + } } if (module.suggests != null && module.suggests.Count > 0) { - user.RaiseMessage("\r\nSuggests:"); - foreach (RelationshipDescriptor dep in module.suggests) - user.RaiseMessage("- {0}", RelationshipToPrintableString(dep)); + _user.RaiseMessage("\r\nSuggests:"); + foreach (var dep in module.suggests) + { + _user.RaiseMessage("- {0}", RelationshipToPrintableString(dep)); + } } if (module.ProvidesList != null && module.ProvidesList.Count > 0) { - user.RaiseMessage("\r\nProvides:"); - foreach (string prov in module.ProvidesList) - user.RaiseMessage("- {0}", prov); + _user.RaiseMessage("\r\nProvides:"); + foreach (var prov in module.ProvidesList) + { + _user.RaiseMessage("- {0}", prov); + } } - #endregion - user.RaiseMessage("\r\nResources:"); + _user.RaiseMessage("\r\nResources:"); + if (module.resources != null) { if (module.resources.bugtracker != null) { - user.RaiseMessage("- bugtracker: {0}", Uri.EscapeUriString(module.resources.bugtracker.ToString())); + _user.RaiseMessage("- bugtracker: {0}", Uri.EscapeUriString(module.resources.bugtracker.ToString())); } + if (module.resources.homepage != null) { - user.RaiseMessage("- homepage: {0}", Uri.EscapeUriString(module.resources.homepage.ToString())); + _user.RaiseMessage("- homepage: {0}", Uri.EscapeUriString(module.resources.homepage.ToString())); } + if (module.resources.spacedock != null) { - user.RaiseMessage("- spacedock: {0}", Uri.EscapeUriString(module.resources.spacedock.ToString())); + _user.RaiseMessage("- spacedock: {0}", Uri.EscapeUriString(module.resources.spacedock.ToString())); } + if (module.resources.repository != null) { - user.RaiseMessage("- repository: {0}", Uri.EscapeUriString(module.resources.repository.ToString())); + _user.RaiseMessage("- repository: {0}", Uri.EscapeUriString(module.resources.repository.ToString())); } + if (module.resources.curse != null) { - user.RaiseMessage("- curse: {0}", Uri.EscapeUriString(module.resources.curse.ToString())); + _user.RaiseMessage("- curse: {0}", Uri.EscapeUriString(module.resources.curse.ToString())); } + if (module.resources.store != null) { - user.RaiseMessage("- store: {0}", Uri.EscapeUriString(module.resources.store.ToString())); + _user.RaiseMessage("- store: {0}", Uri.EscapeUriString(module.resources.store.ToString())); } + if (module.resources.steamstore != null) { - user.RaiseMessage("- steamstore: {0}", Uri.EscapeUriString(module.resources.steamstore.ToString())); + _user.RaiseMessage("- steamstore: {0}", Uri.EscapeUriString(module.resources.steamstore.ToString())); } } if (!module.IsDLC) { - // Compute the CKAN filename. - string file_uri_hash = NetFileCache.CreateURLHash(module.download); - string file_name = CkanModule.StandardName(module.identifier, module.version); - - user.RaiseMessage("\r\nFilename: {0}", file_uri_hash + "-" + file_name); + // Compute the CKAN filename + var fileUriHash = NetFileCache.CreateURLHash(module.download); + var fileName = CkanModule.StandardName(module.identifier, module.version); + + _user.RaiseMessage("\r\nFilename: {0}", fileUriHash + "-" + fileName); } - return Exit.OK; + return Exit.Ok; } - /// - /// Formats a RelationshipDescriptor into a user-readable string: - /// Name, version: x, min: x, max: x - /// private static string RelationshipToPrintableString(RelationshipDescriptor dep) { - StringBuilder sb = new StringBuilder(); - sb.Append(dep.ToString()); + var sb = new StringBuilder(); + sb.Append(dep); return sb.ToString(); } } + + [Verb("show", HelpText = "Show information about a mod")] + internal class ShowOptions : InstanceSpecificOptions + { + [Value(0, MetaName = "Mod name", HelpText = "The mod name to show information about")] + public string ModName { get; set; } + } } diff --git a/Cmdline/Action/Update.cs b/Cmdline/Action/Update.cs index cbb8d5bd8b..fd60067f67 100644 --- a/Cmdline/Action/Update.cs +++ b/Cmdline/Action/Update.cs @@ -1,127 +1,116 @@ using System.Collections.Generic; using System.Linq; +using CommandLine; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { + /// + /// Class for updating the list of mods. + /// public class Update : ICommand { - public IUser user { get; set; } - private GameInstanceManager manager; + private readonly GameInstanceManager _manager; + private readonly IUser _user; /// - /// Initialize the update command object + /// Initializes a new instance of the class. /// - /// GameInstanceManager containing our instances - /// IUser object for interaction - public Update(GameInstanceManager mgr, IUser user) + /// The manager to provide game instances. + /// The current to raise messages to the user. + public Update(GameInstanceManager manager, IUser user) { - manager = mgr; - this.user = user; + _manager = manager; + _user = user; } /// - /// Update the registry + /// Run the 'update' command. /// - /// Game instance to update - /// Command line options object - /// - /// Exit code for shell environment - /// - public int RunCommand(CKAN.GameInstance ksp, object raw_options) + /// + public int RunCommand(CKAN.GameInstance inst, object args) { - UpdateOptions options = (UpdateOptions) raw_options; + var opts = (UpdateOptions)args; - List compatible_prior = null; + List compatiblePrior = null; - if (options.list_changes) + if (opts.ListChanges) { - // Get a list of compatible modules prior to the update. - var registry = RegistryManager.Instance(ksp).registry; - compatible_prior = registry.CompatibleModules(ksp.VersionCriteria()).ToList(); + // Get a list of compatible modules prior to the update + var registry = RegistryManager.Instance(inst).registry; + compatiblePrior = registry.CompatibleModules(inst.VersionCriteria()).ToList(); } - // If no repository is selected, select all. - if (options.repo == null) + // If no repository is selected, select all + if (opts.Repo == null) { - options.update_all = true; + opts.All = true; } try { - if (options.update_all) + if (opts.All) { - UpdateRepository(ksp); + UpdateRepository(inst); } else { - UpdateRepository(ksp, options.repo); + UpdateRepository(inst, opts.Repo); } } catch (ReinstallModuleKraken rmk) { - Upgrade.UpgradeModules(manager, user, ksp, false, rmk.Modules); + Upgrade.UpgradeModules(_manager, _user, inst, false, rmk.Modules); } catch (MissingCertificateKraken kraken) { - // Handling the kraken means we have prettier output. - user.RaiseMessage(kraken.ToString()); - return Exit.ERROR; + // Handling the kraken means we have prettier output + _user.RaiseMessage(kraken.ToString()); + return Exit.Error; } - if (options.list_changes) + if (opts.ListChanges) { - var registry = RegistryManager.Instance(ksp).registry; - PrintChanges(compatible_prior, registry.CompatibleModules(ksp.VersionCriteria()).ToList()); + var registry = RegistryManager.Instance(inst).registry; + PrintChanges(compatiblePrior, registry.CompatibleModules(inst.VersionCriteria()).ToList()); } - return Exit.OK; + return Exit.Ok; } - /// - /// Locates the changes between the prior and post state of the modules.. - /// - /// List of the compatible modules prior to the update. - /// List of the compatible modules after the update. - private void PrintChanges(List modules_prior, List modules_post) + private void PrintChanges(List modulesPrior, List modulesPost) { - var prior = new HashSet(modules_prior, new NameComparer()); - var post = new HashSet(modules_post, new NameComparer()); - + var prior = new HashSet(modulesPrior, new NameComparer()); + var post = new HashSet(modulesPost, new NameComparer()); var added = new HashSet(post.Except(prior, new NameComparer())); var removed = new HashSet(prior.Except(post, new NameComparer())); - - var unchanged = post.Intersect(prior);//Default compare includes versions + // Default compare includes versions + var unchanged = post.Intersect(prior); var updated = post.Except(unchanged).Except(added).Except(removed).ToList(); - // Print the changes. - user.RaiseMessage("Found {0} new modules, {1} removed modules and {2} updated modules.", added.Count(), removed.Count(), updated.Count()); + // Print the changes + _user.RaiseMessage("Found {0} new mods, {1} removed mods and {2} updated mods.", added.Count, removed.Count, updated.Count); if (added.Count > 0) { - PrintModules("New modules [Name (CKAN identifier)]:", added); + PrintModules("New mods [Name (CKAN identifier)]:", added); } if (removed.Count > 0) { - PrintModules("Removed modules [Name (CKAN identifier)]:", removed); + PrintModules("Removed mods [Name (CKAN identifier)]:", removed); } if (updated.Count > 0) { - PrintModules("Updated modules [Name (CKAN identifier)]:", updated); + PrintModules("Updated mods [Name (CKAN identifier)]:", updated); } } - /// - /// Prints a message and a list of modules. Ends with a blank line. - /// - /// The message to print. - /// The modules to list. private void PrintModules(string message, IEnumerable modules) { - // Check input. + // Check input if (message == null) { throw new BadCommandKraken("Message cannot be null."); @@ -132,30 +121,38 @@ private void PrintModules(string message, IEnumerable modules) throw new BadCommandKraken("List of modules cannot be null."); } - user.RaiseMessage(message); + _user.RaiseMessage(message); - foreach (CkanModule module in modules) + foreach (var module in modules) { - user.RaiseMessage("{0} ({1})", module.name, module.identifier); + _user.RaiseMessage("{0} ({1})", module.name, module.identifier); } - user.RaiseMessage(""); + _user.RaiseMessage(""); } - /// - /// Updates the repository. - /// - /// The KSP instance to work on. - /// Repository to update. If null all repositories are used. - private void UpdateRepository(CKAN.GameInstance ksp, string repository = null) + private void UpdateRepository(CKAN.GameInstance inst, string repository = null) { - RegistryManager registry_manager = RegistryManager.Instance(ksp); + var registryManager = RegistryManager.Instance(inst); - var updated = repository == null - ? CKAN.Repo.UpdateAllRepositories(registry_manager, ksp, manager.Cache, user) != CKAN.RepoUpdateResult.Failed - : CKAN.Repo.Update(registry_manager, ksp, user, repository); + _ = repository == null + ? CKAN.Repo.UpdateAllRepositories(registryManager, inst, _manager.Cache, _user) != RepoUpdateResult.Failed + : CKAN.Repo.Update(registryManager, inst, _user, repository); - user.RaiseMessage("Updated information on {0} compatible modules", registry_manager.registry.CompatibleModules(ksp.VersionCriteria()).Count()); + _user.RaiseMessage("Updated information on {0} compatible mods.", registryManager.registry.CompatibleModules(inst.VersionCriteria()).Count()); } } + + [Verb("update", HelpText = "Update list of available mods")] + internal class UpdateOptions : InstanceSpecificOptions + { + [Option('r', "repo", HelpText = "CKAN repository to use (experimental!)")] + public string Repo { get; set; } + + [Option("all", HelpText = "Upgrade all available updated modules")] + public bool All { get; set; } + + [Option("list-changes", HelpText = "List new and removed modules")] + public bool ListChanges { get; set; } + } } diff --git a/Cmdline/Action/Upgrade.cs b/Cmdline/Action/Upgrade.cs index b0bac76861..7225a402af 100644 --- a/Cmdline/Action/Upgrade.cs +++ b/Cmdline/Action/Upgrade.cs @@ -1,104 +1,103 @@ -using System; +using System.Collections.Generic; using System.Linq; -using System.Collections.Generic; using System.Transactions; -using log4net; using CKAN.Versioning; +using CommandLine; +using log4net; -namespace CKAN.CmdLine +namespace CKAN.CmdLine.Action { + /// + /// Class for managing the upgrading of mods. + /// public class Upgrade : ICommand { - private static readonly ILog log = LogManager.GetLogger(typeof(Upgrade)); + private static readonly ILog Log = LogManager.GetLogger(typeof(Upgrade)); - public IUser User { get; set; } - private GameInstanceManager manager; + private readonly GameInstanceManager _manager; + private readonly IUser _user; /// - /// Initialize the upgrade command object + /// Initializes a new instance of the class. /// - /// GameInstanceManager containing our instances - /// IUser object for interaction - public Upgrade(GameInstanceManager mgr, IUser user) + /// The manager to provide game instances. + /// The current to raise messages to the user. + public Upgrade(GameInstanceManager manager, IUser user) { - manager = mgr; - User = user; + _manager = manager; + _user = user; } /// - /// Upgrade an installed module + /// Run the 'upgrade' command. /// - /// Game instance from which to remove - /// Command line options object - /// - /// Exit code for shell environment - /// - public int RunCommand(CKAN.GameInstance ksp, object raw_options) + /// + public int RunCommand(CKAN.GameInstance inst, object args) { - UpgradeOptions options = (UpgradeOptions) raw_options; - - if (options.ckan_file != null) - { - options.modules.Add(MainClass.LoadCkanFromFile(ksp, options.ckan_file).identifier); - } - - if (options.modules.Count == 0 && !options.upgrade_all) + var opts = (UpgradeOptions)args; + if (!opts.Mods.Any() && !opts.All) { - // What? No files specified? - User.RaiseMessage("Usage: ckan upgrade Mod [Mod2, ...]"); - User.RaiseMessage(" or ckan upgrade --all"); + _user.RaiseMessage("upgrade [ ...] - argument(s) missing, perhaps you forgot it?"); + _user.RaiseMessage("If you want to upgrade all mods, use: ckan upgrade --all"); if (AutoUpdate.CanUpdate) { - User.RaiseMessage(" or ckan upgrade ckan"); + _user.RaiseMessage("To update CKAN itself, use: ckan upgrade ckan"); } - return Exit.BADOPT; + + return Exit.BadOpt; + } + + if (opts.CkanFile != null) + { + opts.Mods.ToList().Add(MainClass.LoadCkanFromFile(inst, opts.CkanFile).identifier); } - if (!options.upgrade_all && options.modules[0] == "ckan" && AutoUpdate.CanUpdate) + if (!opts.All && opts.Mods.ToList()[0] == "ckan" && AutoUpdate.CanUpdate) { - User.RaiseMessage("Querying the latest CKAN version"); + _user.RaiseMessage("Getting the latest CKAN version..."); AutoUpdate.Instance.FetchLatestReleaseInfo(); + var latestVersion = AutoUpdate.Instance.latestUpdate.Version; var currentVersion = new ModuleVersion(Meta.GetVersion(VersionFormat.Short)); if (latestVersion.IsGreaterThan(currentVersion)) { - User.RaiseMessage("New CKAN version available - " + latestVersion); var releaseNotes = AutoUpdate.Instance.latestUpdate.ReleaseNotes; - User.RaiseMessage(releaseNotes); - User.RaiseMessage("\r\n"); - if (User.RaiseYesNoDialog("Proceed with install?")) + _user.RaiseMessage("There is a new CKAN version available: {0} ", latestVersion); + _user.RaiseMessage("{0}\r\n", releaseNotes); + + if (_user.RaiseYesNoDialog("Proceed with install?")) { - User.RaiseMessage("Upgrading CKAN, please wait.."); + _user.RaiseMessage("Upgrading CKAN, please wait..."); AutoUpdate.Instance.StartUpdateProcess(false); } } else { - User.RaiseMessage("You already have the latest version."); + _user.RaiseMessage("You already have the latest version of CKAN."); } - return Exit.OK; + return Exit.Ok; } try { - var regMgr = RegistryManager.Instance(ksp); + var regMgr = RegistryManager.Instance(inst); var registry = regMgr.registry; - if (options.upgrade_all) + if (opts.All) { - var to_upgrade = new List(); + var toUpgrade = new List(); - foreach (KeyValuePair mod in registry.Installed(false)) + foreach (var mod in registry.Installed(false)) { try { // Check if upgrades are available - var latest = registry.LatestAvailable(mod.Key, ksp.VersionCriteria()); + var latest = registry.LatestAvailable(mod.Key, inst.VersionCriteria()); - // This may be an unindexed mod. If so, - // skip rather than crash. See KSP-CKAN/CKAN#841. + // This may be an un-indexed mod. If so, + // skip rather than crash. See KSP-CKAN/CKAN#841 if (latest == null || latest.IsDLC) { continue; @@ -107,83 +106,86 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) if (latest.version.IsGreaterThan(mod.Value)) { // Upgradable - log.InfoFormat("New version {0} found for {1}", - latest.version, latest.identifier); - to_upgrade.Add(latest); + Log.InfoFormat("Found a new version for \"{0}\".", latest.identifier); + toUpgrade.Add(latest); } - } catch (ModuleNotFoundKraken) { - log.InfoFormat("{0} is installed, but no longer in the registry", - mod.Key); + Log.InfoFormat("\"{0}\" is installed, but is no longer in the registry.", mod.Key); } } - UpgradeModules(manager, User, ksp, true, to_upgrade); + + UpgradeModules(_manager, _user, inst, true, toUpgrade); } else { - Search.AdjustModulesCase(ksp, options.modules); - UpgradeModules(manager, User, ksp, options.modules); + Search.AdjustModulesCase(inst, opts.Mods.ToList()); + UpgradeModules(_manager, _user, inst, opts.Mods.ToList()); } - User.RaiseMessage(""); + + _user.RaiseMessage(""); } - catch (CancelledActionKraken k) + catch (CancelledActionKraken kraken) { - User.RaiseMessage("Upgrade aborted: {0}", k.Message); - return Exit.ERROR; + _user.RaiseMessage("Upgrade aborted: {0}.", kraken.Message); + return Exit.Error; } catch (ModuleNotFoundKraken kraken) { - User.RaiseMessage("Module {0} not found", kraken.module); - return Exit.ERROR; + _user.RaiseMessage("Could not find \"{0}\".", kraken.module); + return Exit.Error; } catch (InconsistentKraken kraken) { - User.RaiseMessage(kraken.ToString()); - return Exit.ERROR; + _user.RaiseMessage(kraken.ToString()); + return Exit.Error; } catch (ModuleIsDLCKraken kraken) { - User.RaiseMessage($"CKAN can't upgrade expansion '{kraken.module.name}' for you."); + _user.RaiseMessage("Can't upgrade the expansion \"{0}\".", kraken.module.name); var res = kraken?.module?.resources; - var storePagesMsg = new Uri[] { res?.store, res?.steamstore } + var storePagesMsg = new[] { res?.store, res?.steamstore } .Where(u => u != null) .Aggregate("", (a, b) => $"{a}\r\n- {b}"); + if (!string.IsNullOrEmpty(storePagesMsg)) { - User.RaiseMessage($"To upgrade this expansion, download any updates from the store page from which you purchased it:\r\n{storePagesMsg}"); + _user.RaiseMessage("To upgrade this expansion, download any updates from the store page from which you purchased it:\r\n {0}", storePagesMsg); } - return Exit.ERROR; + + return Exit.Error; } - return Exit.OK; + _user.RaiseMessage("Successfully upgraded requested mods."); + return Exit.Ok; } /// - /// Upgrade some modules by their CkanModules + /// Upgrade some modules by their s. /// - /// Game instance manager to use - /// IUser object for output - /// Game instance to use - /// List of modules to upgrade - public static void UpgradeModules(GameInstanceManager manager, IUser user, CKAN.GameInstance ksp, bool ConfirmPrompt, List modules) + /// The manager to provide game instances. + /// The current to raise messages to the user. + /// The game instance which to handle with mods. + /// Whether to confirm the prompt. + /// List of modules to upgrade. + public static void UpgradeModules(GameInstanceManager manager, IUser user, CKAN.GameInstance ksp, bool confirmPrompt, List modules) { UpgradeModules(manager, user, ksp, (ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet possibleConfigOnlyDirs) => installer.Upgrade(modules, downloader, - ref possibleConfigOnlyDirs, regMgr, true, true, ConfirmPrompt), + ref possibleConfigOnlyDirs, regMgr, true, true, confirmPrompt), m => modules.Add(m) ); } /// - /// Upgrade some modules by their identifier and (optional) version + /// Upgrade some modules by their identifier and (optional) version. /// - /// Game instance manager to use - /// IUser object for output - /// Game instance to use - /// List of identifier[=version] to upgrade + /// The manager to provide game instances. + /// The current to raise messages to the user. + /// The game instance which to handle with mods. + /// List of identifier[=version] to upgrade. public static void UpgradeModules(GameInstanceManager manager, IUser user, CKAN.GameInstance ksp, List identsAndVersions) { UpgradeModules(manager, user, ksp, @@ -198,27 +200,23 @@ public static void UpgradeModules(GameInstanceManager manager, IUser user, CKAN. private delegate void AttemptUpgradeAction(ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet possibleConfigOnlyDirs); /// - /// The core of the module upgrading logic, with callbacks to - /// support different input formats managed by the calling code. - /// Handles transactions, creating commonly required objects, - /// looping logic, prompting for TooManyModsProvideKraken resolution. + /// The core of the module upgrading logic, with callbacks to support different input formats managed by the calling code. + /// Handles transactions, creating commonly required objects, looping logic, prompting for resolution. /// - /// Game instance manager to use - /// IUser object for output - /// Game instance to use - /// Function to call to try to perform the actual upgrade, may throw TooManyModsProvideKraken - /// Function to call when the user has requested a new module added to the change set in response to TooManyModsProvideKraken - private static void UpgradeModules( - GameInstanceManager manager, IUser user, CKAN.GameInstance ksp, - AttemptUpgradeAction attemptUpgradeCallback, - System.Action addUserChoiceCallback) + /// The manager to provide game instances. + /// The current to raise messages to the user. + /// The game instance which to handle with mods. + /// Function to call to try to perform the actual upgrade, may throw . + /// Function to call when the user has requested a new module added to the change set in response to . + private static void UpgradeModules(GameInstanceManager manager, IUser user, CKAN.GameInstance ksp, AttemptUpgradeAction attemptUpgradeCallback, System.Action addUserChoiceCallback) { using (TransactionScope transact = CkanTransaction.CreateTransactionScope()) { - var installer = new ModuleInstaller(ksp, manager.Cache, user); + var installer = new ModuleInstaller(ksp, manager.Cache, user); var downloader = new NetAsyncModulesDownloader(user, manager.Cache); - var regMgr = RegistryManager.Instance(ksp); + var regMgr = RegistryManager.Instance(ksp); HashSet possibleConfigOnlyDirs = null; bool done = false; + while (!done) { try @@ -232,6 +230,7 @@ private static void UpgradeModules( int choice = user.RaiseSelectionDialog( $"Choose a module to provide {k.requested}:", k.modules.Select(m => $"{m.identifier} ({m.name})").ToArray()); + if (choice < 0) { return; @@ -246,4 +245,26 @@ private static void UpgradeModules( } } + + [Verb("upgrade", HelpText = "Upgrade an installed mod")] + internal class UpgradeOptions : InstanceSpecificOptions + { + [Option('c', "ckanfile", HelpText = "Local CKAN file to process")] + public string CkanFile { get; set; } + + [Option("no-recommends", HelpText = "Do not install recommended mods")] + public bool NoRecommends { get; set; } + + [Option("with-suggests", HelpText = "Install suggested mods")] + public bool WithSuggests { get; set; } + + [Option("with-all-suggests", HelpText = "Install suggested mods all the way down")] + public bool WithAllSuggests { get; set; } + + [Option("all", HelpText = "Upgrade all available updated mods")] + public bool All { get; set; } + + [Value(0, MetaName = "Mod name(s)", HelpText = "The mod name(s) to upgrade")] + public IEnumerable Mods { get; set; } + } } diff --git a/Cmdline/CKAN-cmdline.csproj b/Cmdline/CKAN-cmdline.csproj index bf7e0b3da6..d3e06f923c 100644 --- a/Cmdline/CKAN-cmdline.csproj +++ b/Cmdline/CKAN-cmdline.csproj @@ -38,7 +38,7 @@ - + @@ -75,10 +75,9 @@ - - + diff --git a/Cmdline/ConsoleUser.cs b/Cmdline/ConsoleUser.cs index 1f0c511e8d..551a08b80a 100644 --- a/Cmdline/ConsoleUser.cs +++ b/Cmdline/ConsoleUser.cs @@ -1,40 +1,38 @@ using System; -using System.Linq; +using System.Linq; using System.Text.RegularExpressions; using log4net; namespace CKAN.CmdLine { /// - /// The commandline implementation of the IUser interface. + /// The commandline implementation of the interface. /// public class ConsoleUser : IUser { - /// - /// A logger for this class. - /// ONLY FOR INTERNAL USE! - /// - private static readonly ILog log = LogManager.GetLogger(typeof(ConsoleUser)); + private static readonly ILog Log = LogManager.GetLogger(typeof(ConsoleUser)); + + private int _previousPercent = -1; + private bool _atStartOfLine = true; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// If set to true, supress interactive dialogs like Yes/No-Dialog or SelectionDialog - public ConsoleUser (bool headless) + /// If set to , suppresses interactive dialogs like the Yes/No-Dialog or the SelectionDialog. + public ConsoleUser(bool headless) { Headless = headless; } /// - /// Gets a value indicating whether this is headless + /// Gets a value indicating whether this is headless. /// - /// true if headless; otherwise, false public bool Headless { get; } /// - /// Ask the user for a yes or no input + /// Asks the user for a yes or no input. /// - /// Question + /// The question to display. public bool RaiseYesNoDialog(string question) { if (Headless) @@ -49,7 +47,7 @@ public bool RaiseYesNoDialog(string question) if (input == null) { - log.ErrorFormat("No console available for input, assuming no."); + Log.Error("No console available for input, assuming no."); return false; } @@ -59,13 +57,15 @@ public bool RaiseYesNoDialog(string question) { return true; } + if (input.Equals("n") || input.Equals("no")) { return false; } + if (input.Equals(string.Empty)) { - // User pressed enter without any text, assuming default choice. + // User pressed enter without any text, assuming default choice return true; } @@ -74,52 +74,53 @@ public bool RaiseYesNoDialog(string question) } /// - /// Ask the user to select one of the elements of the array. + /// Asks the user to select one of the elements of the array. /// The output is index 0 based. /// To supply a default option, make the first option an integer indicating the index of it. /// - /// The selection dialog - /// Message - /// Array of available options + /// The user inputted integer of the selection dialog. + /// The message to display. + /// The arguments to format the message. + /// Thrown if or is . public int RaiseSelectionDialog(string message, params object[] args) { - const int return_cancel = -1; + const int returnCancel = -1; - // Check for the headless flag. + // Check for the headless flag if (Headless) { - // Return that the user cancelled the selection process. - return return_cancel; + // Return that the user cancelled the selection process + return returnCancel; } - // Validate input. - if (String.IsNullOrWhiteSpace(message)) + // Validate input + if (string.IsNullOrWhiteSpace(message)) { - throw new Kraken("Passed message string must be non-empty."); + throw new Kraken("The passed message string must be non-empty."); } if (args.Length == 0) { - throw new Kraken("Passed list of selection candidates must be non-empty."); + throw new Kraken("The passed list of selection candidates must be non-empty."); } - // Check if we have a default selection. - int defaultSelection = -1; + // Check if we have a default selection + var defaultSelection = -1; - if (args[0] is int) + if (args[0] is int @int) { - // Check that the default selection makes sense. - defaultSelection = (int)args[0]; + // Check that the default selection makes sense + defaultSelection = @int; if (defaultSelection < 0 || defaultSelection > args.Length - 1) { - throw new Kraken("Passed default arguments is out of range of the selection candidates."); + throw new Kraken("The passed default arguments are out of range of the selection candidates."); } - // Extract the relevant arguments. - object[] newArgs = new object[args.Length - 1]; + // Extract the relevant arguments + var newArgs = new object[args.Length - 1]; - for (int i = 1; i < args.Length; i++) + for (var i = 1; i < args.Length; i++) { newArgs[i - 1] = args[i]; } @@ -127,10 +128,10 @@ public int RaiseSelectionDialog(string message, params object[] args) args = newArgs; } - // Further data validation. - foreach (object argument in args) + // Further data validation + foreach (var argument in args) { - if (String.IsNullOrWhiteSpace(argument.ToString())) + if (string.IsNullOrWhiteSpace(argument.ToString())) { throw new Kraken("Candidate may not be empty."); } @@ -139,67 +140,70 @@ public int RaiseSelectionDialog(string message, params object[] args) // Write passed message RaiseMessage(message); - // List options. - for (int i = 0; i < args.Length; i++) + // List options + for (var i = 0; i < args.Length; i++) { - string CurrentRow = String.Format("{0}", i + 1); + var currentRow = string.Format("{0}", i + 1); if (i == defaultSelection) { - CurrentRow += "*"; + currentRow += "*"; } - CurrentRow += String.Format(") {0}", args[i]); + currentRow += string.Format(") {0}", args[i]); - RaiseMessage(CurrentRow); + RaiseMessage(currentRow); } - // Create message string. - string output = String.Format("Enter a number between {0} and {1} (To cancel press \"c\" or \"n\".", 1, args.Length); + // Create message string + var output = "\r\n"; + + output += args.Length == 1 + ? string.Format("Enter the number {0} or press \"Enter\" to select {0}", 1) + : string.Format("Enter a number between {0} and {1} to select an instance", 1, args.Length); + + output += ". To cancel, press \"c\" or \"n\"."; if (defaultSelection >= 0) { - output += String.Format(" \"Enter\" will select {0}.", defaultSelection + 1); + output += string.Format(" \"Enter\" will select {0}.", defaultSelection + 1); } - output += "): "; - RaiseMessage(output); - bool valid = false; - int result = 0; + var valid = false; + var result = 0; while (!valid) { - // Wait for input from the command line. - string input = Console.In.ReadLine(); + // Wait for input from the command line + var input = Console.In.ReadLine(); if (input == null) { - // No console present, cancel the process. - return return_cancel; + // No console present, cancel the process + return returnCancel; } input = input.Trim().ToLower(); - // Check for default selection. - if (String.IsNullOrEmpty(input)) + // Check for default selection + if (string.IsNullOrEmpty(input)) { - if (defaultSelection >= 0) + if (defaultSelection >= 0 || args.Length == 1) { return defaultSelection; } } - // Check for cancellation characters. + // Check for cancellation characters if (input == "c" || input == "n") { RaiseMessage("Selection cancelled."); - - return return_cancel; + return returnCancel; } - // Attempt to parse the input. + // Attempt to parse the input try { result = Convert.ToInt32(input); @@ -215,26 +219,23 @@ public int RaiseSelectionDialog(string message, params object[] args) continue; } - // Check the input against the boundaries. + // Check the input against the boundaries if (result > args.Length) { - RaiseMessage("The number in the input is too large."); - RaiseMessage(output); - + RaiseMessage("The number in the input is too large.\r\n{0}", output); continue; } - else if (result < 1) - { - RaiseMessage("The number in the input is too small."); - RaiseMessage(output); + if (result < 1) + { + RaiseMessage("The number in the input is too small.\r\n{0}", output); continue; } - // The list we provide is index 1 based, but the array is index 0 based. + // The list we provide is index 1 based, but the array is index 0 based result--; - // We have checked for all errors and have gotten a valid result. Stop the input loop. + // We have checked for all errors and have gotten a valid result. Stop the input loop valid = true; } @@ -242,46 +243,43 @@ public int RaiseSelectionDialog(string message, params object[] args) } /// - /// Write an error to the console + /// Writes an error message to the console. /// - /// Message - /// Possible arguments to format the message + /// The message to display. + /// The arguments to format the message. public void RaiseError(string message, params object[] args) { GoToStartOfLine(); if (Headless) { - // Special GitHub Action formatting for mutli-line errors - log.ErrorFormat( - message.Replace("\r\n", "%0A"), - args.Select(a => a.ToString().Replace("\r\n", "%0A")).ToArray() - ); + // Special GitHub Action formatting for multi-line errors + Log.ErrorFormat(message.Replace("\r\n", "%0A"), args.Select(a => a.ToString().Replace("\r\n", "%0A")).ToArray()); } else { Console.Error.WriteLine(message, args); } - atStartOfLine = true; + + _atStartOfLine = true; } /// - /// Write a progress message including the percentage to the console. + /// Writes a progress message including the percentage to the console. /// Rewrites the line, so the console is not cluttered by progress messages. /// - /// Message - /// Progress in percent + /// The message to display. + /// The current progress in percent. public void RaiseProgress(string message, int percent) { if (Regex.IsMatch(message, "download", RegexOptions.IgnoreCase)) { // In headless mode, only print a new message if the percent has changed, // to reduce clutter in Jenkins for large downloads - if (!Headless || percent != previousPercent) + if (!Headless || percent != _previousPercent) { // The \r at the front here causes download messages to *overwrite* each other. - Console.Write( - "\r{0} - {1}% ", message, percent); - previousPercent = percent; + Console.Write("\r{0} - {1}% ", message, percent); + _previousPercent = percent; } } else @@ -292,37 +290,31 @@ public void RaiseProgress(string message, int percent) GoToStartOfLine(); Console.Write("{0}", message); } + // These messages leave the cursor at the end of a line of text - atStartOfLine = false; + _atStartOfLine = false; } /// - /// Needed for + /// Writes an informative message to the console. /// - private int previousPercent = -1; - - /// - /// Writes a message to the console - /// - /// Message - /// Arguments to format the message + /// The message to display. + /// The arguments to format the message. public void RaiseMessage(string message, params object[] args) { GoToStartOfLine(); Console.WriteLine(message, args); - atStartOfLine = true; + _atStartOfLine = true; } private void GoToStartOfLine() { - if (!atStartOfLine) + if (!_atStartOfLine) { // Carriage return Console.WriteLine(""); - atStartOfLine = true; + _atStartOfLine = true; } } - - private bool atStartOfLine = true; } } diff --git a/Cmdline/Exit.cs b/Cmdline/Exit.cs deleted file mode 100644 index 12c1418582..0000000000 --- a/Cmdline/Exit.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CKAN.CmdLine -{ - public static class Exit - { - public static readonly int OK = 0; - public static readonly int ERROR = 1; - public static readonly int BADOPT = 2; - } -} - diff --git a/Cmdline/Main.cs b/Cmdline/Main.cs index 78fe0887e3..3803ff48cd 100644 --- a/Cmdline/Main.cs +++ b/Cmdline/Main.cs @@ -6,25 +6,27 @@ using System; using System.Net; using System.Diagnostics; -using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; using CKAN.CmdLine.Action; +using CommandLine; +using CommandLine.Text; using log4net; using log4net.Core; namespace CKAN.CmdLine { - internal class MainClass + public class MainClass { - private static readonly ILog log = LogManager.GetLogger(typeof (MainClass)); + private static readonly ILog Log = LogManager.GetLogger(typeof(MainClass)); + + private static GameInstanceManager _manager; + private static IUser _user; /* * When the STAThread is applied, it changes the apartment state of the current thread to be single threaded. * Without getting into a huge discussion about COM and threading, - * this attribute ensures the communication mechanism between the current thread an + * this attribute ensures the communication mechanism between the current thread and * other threads that may want to talk to it via COM. When you're using Windows Forms, * depending on the feature you're using, it may be using COM interop in order to communicate with * operating system components. Good examples of this are the Clipboard and the File Dialogs. @@ -32,32 +34,53 @@ internal class MainClass [STAThread] public static int Main(string[] args) { - // Launch debugger if the "--debugger" flag is present in the command line arguments. - // We want to do this as early as possible so just check the flag manually, rather than doing the - // more robust argument parsing. - if (args.Any(i => i == "--debugger")) - { - Debugger.Launch(); - } - - // Default to GUI if there are no command line args or if the only args are flags rather than commands. - if (args.All(a => a == "--verbose" || a == "--debug" || a == "--asroot" || a == "--show-console")) + try { - var guiCommand = args.ToList(); - guiCommand.Insert(0, "gui"); - args = guiCommand.ToArray(); - } - - Logging.Initialize(); - log.Info("CKAN started."); + // Launch debugger if the "--debugger" flag is present in the command line arguments. + // We want to do this as early as possible so just check the flag manually, rather than doing the + // more robust argument parsing. + if (args.Any(i => i == "--debugger")) + { + Debugger.Launch(); + } - // Force-allow TLS 1.2 for HTTPS URLs, because GitHub requires it. - // This is on by default in .NET 4.6, but not in 4.5. - ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; + // Default to GUI if there are no command line args or if the only args are flags rather than commands. + if (args.All(a => a == "-v" || a == "--verbose" || a == "-d" || a == "--debug" || a == "--asroot" || a == "--show-console" || a == "--debugger")) + { + var guiCommand = args.ToList(); + guiCommand.Insert(0, "gui"); + args = guiCommand.ToArray(); + } - try - { - return Execute(null, null, args); + var types = LoadVerbs(); + var parser = new Parser(c => c.HelpWriter = null).ParseVerbs(args, types); + var result = parser.MapResult(opts => Execute(_manager, opts, args), errs => + { + if (errs.IsVersion()) + { + Console.WriteLine(Meta.GetVersion(VersionFormat.Full)); + } + else + { + var ht = HelpText.AutoBuild(parser, h => + { + h.AddDashesToOption = true; // Add dashes to options + h.AddNewLineBetweenHelpSections = true; // Add blank line between heading and usage + h.AutoHelp = false; // Hide built-in help option + h.AutoVersion = false; // Hide built-in version option + h.Heading = $"CKAN {Meta.GetVersion(VersionFormat.Full)}"; // Create custom heading + h.Copyright = $"Copyright © 2014-{DateTime.Now.Year}"; // Create custom copyright + h.AddPreOptionsLine(GetUsage(args)); // Show usage + return HelpText.DefaultParsingErrorsHandler(parser, h); + }, e => e, true); + + Console.WriteLine(ht); + } + + return Exit.Ok; + }); + + return result; } finally { @@ -65,292 +88,274 @@ public static int Main(string[] args) } } - public static int Execute(GameInstanceManager manager, CommonOptions opts, string[] args) + /// + /// This is purely made for the tests to be able to pass over a . + /// ONLY FOR INTERNAL USE !!! + /// + /// The command line arguments handled by the parser. + /// The dummy manager to provide dummy game instances. + /// An code. + public static int MainForTests(string[] args, GameInstanceManager manager = null) { - // We shouldn't instantiate Options if it's a subcommand. - // It breaks command-specific help, for starters. - try - { - switch (args[0]) - { - case "repair": - return (new Repair()).RunSubCommand(manager, opts, new SubCommandOptions(args)); + _manager = manager; + return Main(args); + } - case "instance": - return (new GameInstance()).RunSubCommand(manager, opts, new SubCommandOptions(args)); + private static Type[] LoadVerbs() + { + return Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.GetCustomAttribute() != null) + .Except(Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.GetCustomAttribute() != null) + .ToArray()) + .ToArray(); + } - case "compat": - return (new Compat()).RunSubCommand(manager, opts, new SubCommandOptions(args)); + /// + /// Executes the provided command. + /// + /// The manager to provide game instances. + /// The command line arguments handled by the parser. + /// An code. + public static int Execute(GameInstanceManager manager, object args, string[] argStrings) + { + var s = args.ToString(); + var opts = s.Replace(s.Substring(0, s.LastIndexOf('.') + 1), "").Split('+'); - case "repo": - return (new Repo()).RunSubCommand(manager, opts, new SubCommandOptions(args)); + try + { + Logging.Initialize(); + Log.Info("CKAN started."); - case "authtoken": - return (new AuthToken()).RunSubCommand(manager, opts, new SubCommandOptions(args)); + // Force-allow TLS 1.2 for HTTPS URLs, because GitHub requires it. + // This is on by default in .NET 4.6, but not in 4.5. + ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; - case "cache": - return (new Cache()).RunSubCommand(manager, opts, new SubCommandOptions(args)); - - case "mark": - return (new Mark()).RunSubCommand(manager, opts, new SubCommandOptions(args)); + switch (opts[0]) + { + case "AuthTokenOptions": + return new AuthToken().RunCommand(manager, args); + case "CacheOptions": + return new Cache().RunCommand(manager, args); + case "CompatOptions": + return new Compat().RunCommand(manager, args); + case "KspOptions": + return new Action.GameInstance().RunCommand(manager, args); + case "MarkOptions": + return new Mark().RunCommand(manager, args); + case "RepairOptions": + return new Repair().RunCommand(manager, args); + case "RepoOptions": + return new Action.Repo().RunCommand(manager, args); } } catch (NoGameInstanceKraken) { - return printMissingInstanceError(new ConsoleUser(false)); + return PrintMissingInstanceError(new ConsoleUser(false)); } finally { - log.Info("CKAN exiting."); + Log.Info("CKAN exiting."); } - Options cmdline; - try - { - cmdline = new Options(args); - } - catch (BadCommandKraken) - { - return AfterHelp(); - } - finally - { - log.Info("CKAN exiting."); - } - - // Process commandline options. - CommonOptions options = (CommonOptions)cmdline.options; - options.Merge(opts); - IUser user = new ConsoleUser(options.Headless); + CommonOptions options = new CommonOptions(); + _user = new ConsoleUser(options.Headless); if (manager == null) { - manager = new GameInstanceManager(user); + manager = new GameInstanceManager(_user); } else { - manager.User = user; + manager.User = _user; } try { - int exitCode = options.Handle(manager, user); - if (exitCode != Exit.OK) + var exitCode = options.Handle(manager, _user); + if (exitCode != Exit.Ok) return exitCode; - // Don't bother with instances or registries yet because some commands don't need them. - return RunSimpleAction(cmdline, options, args, user, manager); - } - finally - { - log.Info("CKAN exiting."); - } - } - public static int AfterHelp() - { - // Our help screen will already be shown. Let's add some extra data. - new ConsoleUser(false).RaiseMessage("You are using CKAN version {0}", Meta.GetVersion(VersionFormat.Full)); - return Exit.BADOPT; - } - - public static CKAN.GameInstance GetGameInstance(GameInstanceManager manager) - { - CKAN.GameInstance inst = manager.CurrentInstance - ?? manager.GetPreferredInstance(); - if (inst == null) - { - throw new NoGameInstanceKraken(); - } - return inst; - } - - /// - /// Run whatever action the user has provided - /// - /// The exit status that should be returned to the system. - private static int RunSimpleAction(Options cmdline, CommonOptions options, string[] args, IUser user, GameInstanceManager manager) - { - try - { - switch (cmdline.action) + var instance = GetGameInstance(manager); + switch (opts[0]) { - case "gui": - return Gui(manager, (GuiOptions)options, args); - - case "consoleui": - return ConsoleUi(manager, (ConsoleUIOptions)options, args); - - case "prompt": - return new Prompt().RunCommand(manager, cmdline.options); - - case "version": - return Version(user); - - case "update": - return (new Update(manager, user)).RunCommand(GetGameInstance(manager), cmdline.options); - - case "available": - return (new Available(user)).RunCommand(GetGameInstance(manager), cmdline.options); - - case "add": - case "install": - Scan(GetGameInstance(manager), user, cmdline.action); - return (new Install(manager, user)).RunCommand(GetGameInstance(manager), cmdline.options); - - case "scan": - return Scan(GetGameInstance(manager), user); - - case "list": - return (new List(user)).RunCommand(GetGameInstance(manager), cmdline.options); - - case "show": - return (new Show(user)).RunCommand(GetGameInstance(manager), cmdline.options); - - case "replace": - Scan(GetGameInstance(manager), user, cmdline.action); - return (new Replace(manager, user)).RunCommand(GetGameInstance(manager), (ReplaceOptions)cmdline.options); - - case "upgrade": - Scan(GetGameInstance(manager), user, cmdline.action); - return (new Upgrade(manager, user)).RunCommand(GetGameInstance(manager), cmdline.options); - - case "search": - return (new Search(user)).RunCommand(GetGameInstance(manager), options); - - case "uninstall": - case "remove": - return (new Remove(manager, user)).RunCommand(GetGameInstance(manager), cmdline.options); - - case "import": - return (new Import(manager, user)).RunCommand(GetGameInstance(manager), options); - - case "clean": - return Clean(manager.Cache); - - case "compare": - return (new Compare(user)).RunCommand(cmdline.options); - + case "AvailableOptions": + return new Available(_user).RunCommand(instance, args); + case "CompareOptions": + return new Compare(_user).RunCommand(instance, args); + case "ConsoleUiOptions": + return ConsoleUi(manager, args); + case "GuiOptions": + return Gui(manager, args, argStrings); + case "ImportOptions": + return new Import(manager, _user).RunCommand(instance, args); + case "InstallOptions": + Scan(instance, _user, "install"); + return new Install(manager, _user).RunCommand(instance, args); + case "ListOptions": + return new List(_user).RunCommand(instance, args); + case "PromptOptions": + return new Prompt().RunCommand(manager, args, argStrings); + case "RemoveOptions": + return new Remove(manager, _user).RunCommand(instance, args); + case "ReplaceOptions": + Scan(instance, _user, "replace"); + return new Replace(manager, _user).RunCommand(instance, args); + case "ScanOptions": + return Scan(instance, _user); + case "SearchOptions": + return new Search(_user).RunCommand(instance, args); + case "ShowOptions": + return new Show(_user).RunCommand(instance, args); + case "UpdateOptions": + return new Update(manager, _user).RunCommand(instance, args); + case "UpgradeOptions": + Scan(instance, _user, "upgrade"); + return new Upgrade(manager, _user).RunCommand(instance, args); default: - user.RaiseMessage("Unknown command, try --help"); - return Exit.BADOPT; + return Exit.BadOpt; } } - catch (NoGameInstanceKraken) - { - return printMissingInstanceError(user); - } finally { - RegistryManager.DisposeAll(); + Log.Info("CKAN exiting."); } } - internal static CkanModule LoadCkanFromFile(CKAN.GameInstance current_instance, string ckan_file) + private static string GetUsage(string[] args) { - CkanModule module = CkanModule.FromFile(ckan_file); - - // We'll need to make some registry changes to do this. - RegistryManager registry_manager = RegistryManager.Instance(current_instance); - - // Remove this version of the module in the registry, if it exists. - registry_manager.registry.RemoveAvailable(module); - - // Sneakily add our version in... - registry_manager.registry.AddAvailable(module); - - return module; + const string prefix = "USAGE:\r\n ckan"; + switch (args[0]) + { + case "authtoken": + return new AuthToken().GetUsage(prefix, args); + case "cache": + return new Cache().GetUsage(prefix, args); + case "compat": + return new Compat().GetUsage(prefix, args); + case "ksp": + return new Action.GameInstance().GetUsage(prefix, args); + case "mark": + return new Mark().GetUsage(prefix, args); + case "repair": + return new Repair().GetUsage(prefix, args); + case "repo": + return new Action.Repo().GetUsage(prefix, args); + case "install": + case "remove": + case "replace": + case "upgrade": + return $"{prefix} {args[0]} [options] [ ...]"; + case "show": + return $"{prefix} {args[0]} [options] "; + case "compare": + return $"{prefix} {args[0]} [options] "; + case "import": + return $"{prefix} {args[0]} [options] [ ...]"; + case "search": + return $"{prefix} {args[0]} [options] "; + case "available": + case "consoleui": + case "gui": + case "list": + case "prompt": + case "scan": + case "update": + return $"{prefix} {args[0]} [options]"; + default: + return $"{prefix} [options]"; + } } - private static int printMissingInstanceError(IUser user) + private static int PrintMissingInstanceError(IUser user) { - user.RaiseMessage("I don't know where a game instance is installed."); - user.RaiseMessage("Use 'ckan instance help' for assistance in setting this."); - return Exit.ERROR; + user.RaiseMessage("I don't know where KSP is installed."); + user.RaiseMessage("Use 'ckan ksp --help' for assistance in setting this."); + return Exit.Error; } - private static int Gui(GameInstanceManager manager, GuiOptions options, string[] args) + /// + /// Gets the current, or preferred, game instance to manipulate. + /// + /// The manager to provide game instances. + /// The current instance. + /// Throws if no valid instance was found. + public static GameInstance GetGameInstance(GameInstanceManager manager) { - // TODO: Sometimes when the GUI exits, we get a System.ArgumentException, - // but trying to catch it here doesn't seem to help. Dunno why. - - GUI.Main_(args, manager, options.ShowConsole); + GameInstance instance = manager.CurrentInstance ?? manager.GetPreferredInstance(); + if (instance == null) + { + throw new NoGameInstanceKraken(null); + } - return Exit.OK; + return instance; } - private static int ConsoleUi(GameInstanceManager manager, ConsoleUIOptions opts, string[] args) + private static int ConsoleUi(GameInstanceManager manager, object args) { // Debug/verbose output just messes up the screen LogManager.GetRepository().Threshold = Level.Warn; - return CKAN.ConsoleUI.ConsoleUI.Main_(args, manager, - opts.Theme ?? Environment.GetEnvironmentVariable("CKAN_CONSOLEUI_THEME") ?? "default", - opts.Debug); + + var opts = (ConsoleUiOptions)args; + _user.RaiseMessage("Starting ConsoleUI, please wait..."); + return ConsoleUI.ConsoleUI.Main_(args.ToString().Split(), manager, opts.Theme ?? Environment.GetEnvironmentVariable("CKAN_CONSOLEUI_THEME") ?? "default", opts.Debug); } - private static int Version(IUser user) + private static int Gui(GameInstanceManager manager, object args, string[] argStrings) { - user.RaiseMessage(Meta.GetVersion(VersionFormat.Full)); + // TODO: Sometimes when the GUI exits, we get a System.ArgumentException, + // but trying to catch it here doesn't seem to help. Dunno why. - return Exit.OK; + var opts = (GuiOptions)args; + _user.RaiseMessage("Starting GUI, please wait..."); + GUI.Main_(argStrings, manager, opts.ShowConsole); + return Exit.Ok; } - /// - /// Scans the game instance. Detects installed mods to mark as auto-detected and checks the consistency - /// - /// The instance to scan - /// - /// Changes the output message if set. - /// Exit.OK if instance is consistent, Exit.ERROR otherwise - private static int Scan(CKAN.GameInstance inst, IUser user, string next_command = null) + private static int Scan(GameInstance instance, IUser user, string nextCommand = null) { try { - inst.Scan(); - return Exit.OK; + instance.Scan(); + return Exit.Ok; } catch (InconsistentKraken kraken) { - - if (next_command == null) + if (nextCommand == null) { user.RaiseError(kraken.InconsistenciesPretty); user.RaiseError("The repo has not been saved."); } else { - user.RaiseMessage("Preliminary scanning shows that the install is in a inconsistent state."); - user.RaiseMessage("Use ckan.exe scan for more details"); - user.RaiseMessage("Proceeding with {0} in case it fixes it.\r\n", next_command); + user.RaiseMessage("Preliminary scanning shows that the install is in an inconsistent state."); + user.RaiseMessage("Use 'ckan scan --help' for more details."); + user.RaiseMessage("Proceeding with {0} in case it fixes it.\r\n", nextCommand); } - return Exit.ERROR; + return Exit.Error; } } - private static int Clean(NetModuleCache cache) + /// + /// Loads a .ckan file from the given path, reads it and creates a from it. + /// + /// The current instance to modify the module. + /// The path to the .ckan file. + /// A . + internal static CkanModule LoadCkanFromFile(GameInstance currentInstance, string ckanFile) { - cache.RemoveAll(); - return Exit.OK; - } - } + CkanModule module = CkanModule.FromFile(ckanFile); - public class NoGameInstanceKraken : Kraken - { - public NoGameInstanceKraken() { } - } + // We'll need to make some registry changes to do this + RegistryManager registryManager = RegistryManager.Instance(currentInstance); - public class CmdLineUtil - { - public static uint GetUID() - { - if (Platform.IsUnix || Platform.IsMac) - { - return getuid(); - } + // Remove this version of the module in the registry, if it exists + registryManager.registry.RemoveAvailable(module); - return 1; - } + // Sneakily add our version in... + registryManager.registry.AddAvailable(module); - [DllImport("libc")] - private static extern uint getuid(); + return module; + } } } diff --git a/Cmdline/Options.cs b/Cmdline/Options.cs index 0e88b1a902..5bee05fe83 100644 --- a/Cmdline/Options.cs +++ b/Cmdline/Options.cs @@ -1,221 +1,45 @@ using System; using System.IO; using System.Reflection; -using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Text.RegularExpressions; +using CommandLine; using log4net; using log4net.Core; -using CommandLine; -using CommandLine.Text; namespace CKAN.CmdLine { - // Look, parsing options is so easy and beautiful I made - // it into a special class for you to admire! - - public class Options - { - public string action { get; set; } - public object options { get; set; } - - /// - /// Returns an options object on success. Prints a default help - /// screen and throws a BadCommandKraken on failure. - /// - public Options(string[] args) - { - Parser.Default.ParseArgumentsStrict - ( - args, new Actions(), (verb, suboptions) => - { - action = verb; - options = suboptions; - }, - delegate - { - throw new BadCommandKraken(); - } - ); - } - } - - // Actions supported by our client go here. - - internal class Actions : VerbCommandOptions - { - [VerbOption("gui", HelpText = "Start the CKAN GUI")] - public GuiOptions GuiOptions { get; set; } - - [VerbOption("consoleui", HelpText = "Start the CKAN console UI")] - public ConsoleUIOptions ConsoleUIOptions { get; set; } - - [VerbOption("prompt", HelpText = "Run CKAN prompt for executing multiple commands in a row")] - public CommonOptions PromptOptions { get; set; } - - [VerbOption("search", HelpText = "Search for mods")] - public SearchOptions SearchOptions { get; set; } - - [VerbOption("upgrade", HelpText = "Upgrade an installed mod")] - public UpgradeOptions Upgrade { get; set; } - - [VerbOption("update", HelpText = "Update list of available mods")] - public UpdateOptions Update { get; set; } - - [VerbOption("available", HelpText = "List available mods")] - public AvailableOptions Available { get; set; } - - [VerbOption("install", HelpText = "Install a mod")] - public InstallOptions Install { get; set; } - - [VerbOption("remove", HelpText = "Remove an installed mod")] - public RemoveOptions Remove { get; set; } - - [VerbOption("import", HelpText = "Import manually downloaded mods")] - public ImportOptions Import { get; set; } - - [VerbOption("scan", HelpText = "Scan for manually installed mods")] - public ScanOptions Scan { get; set; } - - [VerbOption("list", HelpText = "List installed modules")] - public ListOptions List { get; set; } - - [VerbOption("show", HelpText = "Show information about a mod")] - public ShowOptions Show { get; set; } - - [VerbOption("clean", HelpText = "Clean away downloaded files from the cache")] - public CleanOptions Clean { get; set; } - - [VerbOption("repair", HelpText = "Attempt various automatic repairs")] - public SubCommandOptions Repair { get; set; } - - [VerbOption("replace", HelpText = "Replace list of replaceable mods")] - public ReplaceOptions Replace { get; set; } - - [VerbOption("repo", HelpText = "Manage CKAN repositories")] - public SubCommandOptions Repo { get; set; } - - [VerbOption("mark", HelpText = "Edit flags on modules")] - public SubCommandOptions Mark { get; set; } - - [VerbOption("instance", HelpText = "Manage game instances")] - public SubCommandOptions Instance { get; set; } - - [VerbOption("authtoken", HelpText = "Manage authentication tokens")] - public AuthTokenSubOptions AuthToken { get; set; } - - [VerbOption("cache", HelpText = "Manage download cache path")] - public SubCommandOptions Cache { get; set; } - - [VerbOption("compat", HelpText = "Manage game version compatibility")] - public SubCommandOptions Compat { get; set; } - - [VerbOption("compare", HelpText = "Compare version strings")] - public CompareOptions Compare { get; set; } - - [VerbOption("version", HelpText = "Show the version of the CKAN client being used")] - public VersionOptions Version { get; set; } - - [HelpVerbOption] - public string GetUsage(string verb) - { - HelpText ht = HelpText.AutoBuild(this, verb); - - // Add a usage prefix line - ht.AddPreOptionsLine(" "); - if (string.IsNullOrEmpty(verb)) - { - ht.AddPreOptionsLine($"Usage: ckan [options]"); - } - else - { - ht.AddPreOptionsLine(verb + " - " + GetDescription(verb)); - switch (verb) - { - // First the commands that deal with mods - case "add": - case "install": - case "remove": - case "uninstall": - case "upgrade": - ht.AddPreOptionsLine($"Usage: ckan {verb} [options] modules"); - break; - case "show": - ht.AddPreOptionsLine($"Usage: ckan {verb} [options] module"); - break; - - // Now the commands with other string arguments - case "search": - ht.AddPreOptionsLine($"Usage: ckan {verb} [options] substring"); - break; - case "compare": - ht.AddPreOptionsLine($"Usage: ckan {verb} [options] version1 version2"); - break; - case "import": - ht.AddPreOptionsLine($"Usage: ckan {verb} [options] paths"); - break; - - // Now the commands with only --flag type options - case "gui": - case "available": - case "list": - case "update": - case "scan": - case "clean": - case "version": - default: - ht.AddPreOptionsLine($"Usage: ckan {verb} [options]"); - break; - } - } - return ht; - } - - } - - public abstract class VerbCommandOptions + /// + /// Common options for all commands. + /// + internal class CommonOptions { - protected string GetDescription(string verb) - { - var info = this.GetType().GetProperties(); - foreach (var property in info) - { - BaseOptionAttribute attrib = (BaseOptionAttribute)Attribute.GetCustomAttribute( - property, typeof(BaseOptionAttribute), false); - if (attrib != null && attrib.LongName == verb) - return attrib.HelpText; - } - return ""; - } - } - - // Options common to all classes. + private static readonly ILog Log = LogManager.GetLogger(typeof(CommonOptions)); - public class CommonOptions - { - [Option('v', "verbose", DefaultValue = false, HelpText = "Show more of what's going on when running.")] + [Option('v', "verbose", HelpText = "Show more of what's going on when running")] public bool Verbose { get; set; } - [Option('d', "debug", DefaultValue = false, HelpText = "Show debugging level messages. Implies verbose")] + [Option('d', "debug", HelpText = "Show debugging level messages. Implies verbose")] public bool Debug { get; set; } - [Option("debugger", DefaultValue = false, HelpText = "Launch debugger at start")] + [Option("debugger", HelpText = "Launch debugger at start")] public bool Debugger { get; set; } [Option("net-useragent", HelpText = "Set the default user-agent string for HTTP requests")] public string NetUserAgent { get; set; } - [Option("headless", DefaultValue = false, HelpText = "Set to disable all prompts")] + [Option("headless", HelpText = "Set to disable all prompts")] public bool Headless { get; set; } - [Option("asroot", DefaultValue = false, HelpText = "Allows CKAN to run as root on Linux-based systems")] + [Option("asroot", HelpText = "Allows CKAN to run as root on Linux-based systems")] public bool AsRoot { get; set; } - [HelpVerbOption] - public string GetUsage(string verb) - { - return HelpText.AutoBuild(this, verb); - } - + /// + /// Handle the common options. + /// + /// The manager to provide game instances. + /// The current to raise messages to the user. + /// An code. public virtual int Handle(GameInstanceManager manager, IUser user) { CheckMonoVersion(user, 3, 1, 0); @@ -223,84 +47,65 @@ public virtual int Handle(GameInstanceManager manager, IUser user) // Processes in Docker containers normally run as root. // If we are running in a Docker container, do not require --asroot. // Docker creates a .dockerenv file in the root of each container. - if ((Platform.IsUnix || Platform.IsMac) && CmdLineUtil.GetUID() == 0 && !File.Exists("/.dockerenv")) + if ((Platform.IsUnix || Platform.IsMac) && GetUid() == 0 && !File.Exists("/.dockerenv")) { if (!AsRoot) { - user.RaiseError("You are trying to run CKAN as root.\r\nThis is a bad idea and there is absolutely no good reason to do it. Please run CKAN from a user account (or use --asroot if you are feeling brave)."); - return Exit.ERROR; - } - else - { - user.RaiseMessage("Warning: Running CKAN as root!"); + user.RaiseError("You are trying to run CKAN as root.\r\nThis is a bad idea and there is absolutely no good reason to do it. Please run CKAN from an user account (or use --asroot if you are feeling brave)."); + return Exit.Error; } + + user.RaiseMessage("Warning: Running CKAN as root!"); } if (Debug) { LogManager.GetRepository().Threshold = Level.Debug; - log.Info("Debug logging enabled"); + Log.Info("Debug logging enabled"); } else if (Verbose) { LogManager.GetRepository().Threshold = Level.Info; - log.Info("Verbose logging enabled"); + Log.Info("Verbose logging enabled"); } // Assign user-agent string if user has given us one - if (NetUserAgent != null) + if (!string.IsNullOrWhiteSpace(NetUserAgent)) { Net.UserAgentString = NetUserAgent; } - return Exit.OK; + return Exit.Ok; } - /// - /// Combine two options objects. - /// This is mainly to ensure that --headless carries through for prompt. - /// - /// Options object to merge into this one - public void Merge(CommonOptions otherOpts) - { - if (otherOpts != null) - { - Verbose = Verbose || otherOpts.Verbose; - Debug = Debug || otherOpts.Debug; - Debugger = Debugger || otherOpts.Debugger; - NetUserAgent = NetUserAgent ?? otherOpts.NetUserAgent; - Headless = Headless || otherOpts.Headless; - AsRoot = AsRoot || otherOpts.AsRoot; - } - } - - private static void CheckMonoVersion(IUser user, int rec_major, int rec_minor, int rec_patch) + private static void CheckMonoVersion(IUser user, int recMajor, int recMinor, int recPatch) { try { - Type type = Type.GetType("Mono.Runtime"); - if (type == null) return; + var type = Type.GetType("Mono.Runtime"); + if (type == null) + return; - MethodInfo display_name = type.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static); - if (display_name != null) + var displayName = type.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static); + if (displayName != null) { - var version_string = (string) display_name.Invoke(null, null); - var match = Regex.Match(version_string, @"^\D*(?[\d]+)\.(?\d+)\.(?\d+).*$"); + var versionString = (string)displayName.Invoke(null, null); + var match = Regex.Match(versionString, @"^\D*(?[\d]+)\.(?\d+)\.(?\d+).*$"); if (match.Success) { - int major = Int32.Parse(match.Groups["major"].Value); - int minor = Int32.Parse(match.Groups["minor"].Value); - int patch = Int32.Parse(match.Groups["revision"].Value); + var major = int.Parse(match.Groups["major"].Value); + var minor = int.Parse(match.Groups["minor"].Value); + var patch = int.Parse(match.Groups["revision"].Value); - if (major < rec_major || (major == rec_major && minor < rec_minor)) + if (major < recMajor || major == recMajor && minor < recMinor) { user.RaiseMessage( - "Warning. Detected mono runtime of {0} is less than the recommended version of {1}\r\n", - String.Join(".", major, minor, patch), - String.Join(".", rec_major, rec_minor, rec_patch) - ); - user.RaiseMessage("Update recommend\r\n"); + "Warning. Detected Mono version {0}, which is less than the recommended version of {1}.", + string.Join(".", major, minor, patch), + string.Join(".", recMajor, recMinor, recPatch) + ); + user.RaiseMessage("Please update Mono via https://www.mono-project.com/download/stable/"); } } } @@ -311,235 +116,86 @@ private static void CheckMonoVersion(IUser user, int rec_major, int rec_minor, i } } - protected static readonly ILog log = LogManager.GetLogger(typeof(CommonOptions)); + private static uint GetUid() + { + if (Platform.IsUnix || Platform.IsMac) + { + return getuid(); + } + + return 1; + } + + [DllImport("libc")] + private static extern uint getuid(); } - public class InstanceSpecificOptions : CommonOptions + /// + /// Instance specific options for commands dealing with mods. + /// + internal class InstanceSpecificOptions : CommonOptions { - [Option("instance", HelpText = "Game instance to use")] - public string Instance { get; set; } + // Different 'SetName' properties make the options mutually exclusive + [Option("ksp", HelpText = "KSP install to use", SetName = "ksp")] + public string KSP { get; set; } - [Option("gamedir", HelpText = "Game dir to use")] - public string Gamedir { get; set; } + [Option("kspdir", HelpText = "KSP directory to use", SetName = "dir")] + public string KSPdir { get; set; } + /// + /// Handle the instance specific options. This also handles the common options. + /// + /// The manager to provide game instances. + /// The current to raise messages to the user. + /// An code. public override int Handle(GameInstanceManager manager, IUser user) { - int exitCode = base.Handle(manager, user); - if (exitCode == Exit.OK) - { - // User provided game instance - if (Gamedir != null && Instance != null) - { - user.RaiseMessage("--instance and --gamedir can't be specified at the same time"); - return Exit.BADOPT; - } + var exitCode = base.Handle(manager, user); + if (exitCode != Exit.Ok) + return exitCode; - try - { - if (!string.IsNullOrEmpty(Instance)) - { - // Set a game directory by its alias. - manager.SetCurrentInstance(Instance); - } - else if (!string.IsNullOrEmpty(Gamedir)) - { - // Set a game directory by its path - manager.SetCurrentInstanceByPath(Gamedir); - } - } - catch (NotKSPDirKraken k) + try + { + if (!string.IsNullOrWhiteSpace(KSP)) { - user.RaiseMessage("Sorry, {0} does not appear to be a game instance", k.path); - return Exit.BADOPT; + // Set a KSP directory by its alias. + manager.SetCurrentInstance(KSP); } - catch (InvalidKSPInstanceKraken k) + else if (!string.IsNullOrWhiteSpace(KSPdir)) { - user.RaiseMessage("Invalid game instance specified \"{0}\", use '--gamedir' to specify by path, or 'instance list' to see known game instances", k.instance); - return Exit.BADOPT; + // Set a KSP directory by its path + manager.SetCurrentInstanceByPath(KSPdir); } } + catch (NotKSPDirKraken kraken) + { + user.RaiseMessage("Sorry, \"{0}\" does not appear to be a KSP directory.", kraken.path); + return Exit.BadOpt; + } + catch (InvalidKSPInstanceKraken kraken) + { + user.RaiseMessage("Invalid KSP installation specified \"{0}\", use '--kspdir' to specify by path, or use 'ckan ksp list' to see known KSP installations.", kraken.instance); + return Exit.BadOpt; + } + return exitCode; } } - /// - /// For things which are subcommands ('instance', 'repair' etc), we just grab a list - /// we can pass on. - /// - public class SubCommandOptions : CommonOptions + [Verb("consoleui", HelpText = "Start the CKAN console UI")] + internal class ConsoleUiOptions : InstanceSpecificOptions { - [ValueList(typeof(List))] - public List options { get; set; } - - public SubCommandOptions() { } - - public SubCommandOptions(string[] args) - { - options = new List(args).GetRange(1, args.Length - 1); - } - } - - // Each action defines its own options that it supports. - // Don't forget to cast to this type when you're processing them later on. - - internal class InstallOptions : InstanceSpecificOptions - { - [OptionArray('c', "ckanfiles", HelpText = "Local CKAN files to process")] - public string[] ckan_files { get; set; } - - [Option("no-recommends", DefaultValue = false, HelpText = "Do not install recommended modules")] - public bool no_recommends { get; set; } - - [Option("with-suggests", DefaultValue = false, HelpText = "Install suggested modules")] - public bool with_suggests { get; set; } - - [Option("with-all-suggests", DefaultValue = false, HelpText = "Install suggested modules all the way down")] - public bool with_all_suggests { get; set; } - - [Option("allow-incompatible", DefaultValue = false, HelpText = "Install modules that are not compatible with the current game version")] - public bool allow_incompatible { get; set; } - - [ValueList(typeof(List))] - public List modules { get; set; } - } - - internal class UpgradeOptions : InstanceSpecificOptions - { - [Option('c', "ckanfile", HelpText = "Local CKAN file to process")] - public string ckan_file { get; set; } - - [Option("no-recommends", DefaultValue = false, HelpText = "Do not install recommended modules")] - public bool no_recommends { get; set; } - - [Option("with-suggests", DefaultValue = false, HelpText = "Install suggested modules")] - public bool with_suggests { get; set; } - - [Option("with-all-suggests", DefaultValue = false, HelpText = "Install suggested modules all the way down")] - public bool with_all_suggests { get; set; } - - [Option("all", DefaultValue = false, HelpText = "Upgrade all available updated modules")] - public bool upgrade_all { get; set; } - - [ValueList(typeof (List))] - public List modules { get; set; } - } - - internal class ReplaceOptions : InstanceSpecificOptions - { - [Option('c', "ckanfile", HelpText = "Local CKAN file to process")] - public string ckan_file { get; set; } - - [Option("no-recommends", HelpText = "Do not install recommended modules")] - public bool no_recommends { get; set; } - - [Option("with-suggests", HelpText = "Install suggested modules")] - public bool with_suggests { get; set; } - - [Option("with-all-suggests", HelpText = "Install suggested modules all the way down")] - public bool with_all_suggests { get; set; } - - [Option("allow-incompatible", DefaultValue = false, HelpText = "Install modules that are not compatible with the current game version")] - public bool allow_incompatible { get; set; } - - [Option("all", HelpText = "Replace all available replaced modules")] - public bool replace_all { get; set; } - - // TODO: How do we provide helptext on this? - [ValueList(typeof (List))] - public List modules { get; set; } - } - - internal class ScanOptions : InstanceSpecificOptions - { - } - - internal class ListOptions : InstanceSpecificOptions - { - [Option("porcelain", HelpText = "Dump raw list of modules, good for shell scripting")] - public bool porcelain { get; set; } - - [Option("export", HelpText = "Export list of modules in specified format to stdout")] - public string export { get; set; } - } - - internal class VersionOptions : CommonOptions { } - internal class CleanOptions : InstanceSpecificOptions { } - - internal class AvailableOptions : InstanceSpecificOptions - { - [Option("detail", HelpText = "Show short description of each module")] - public bool detail { get; set; } + [Option("theme", HelpText = "Name of color scheme to use, falls back to environment variable CKAN_CONSOLEUI_THEME")] + public string Theme { get; set; } } + [Verb("gui", HelpText = "Start the CKAN GUI")] internal class GuiOptions : InstanceSpecificOptions { [Option("show-console", HelpText = "Shows the console while running the GUI")] public bool ShowConsole { get; set; } } - internal class ConsoleUIOptions : InstanceSpecificOptions - { - [Option("theme", HelpText = "Name of color scheme to use, falls back to environment variable CKAN_CONSOLEUI_THEME")] - public string Theme { get; set; } - } - - internal class UpdateOptions : InstanceSpecificOptions - { - // This option is really meant for devs testing their CKAN-meta forks. - [Option('r', "repo", HelpText = "CKAN repository to use (experimental!)")] - public string repo { get; set; } - - [Option("all", DefaultValue = false, HelpText = "Upgrade all available updated modules")] - public bool update_all { get; set; } - - [Option("list-changes", DefaultValue = false, HelpText = "List new and removed modules")] - public bool list_changes { get; set; } - } - - internal class RemoveOptions : InstanceSpecificOptions - { - [Option("re", HelpText = "Parse arguments as regular expressions")] - public bool regex { get; set; } - - [ValueList(typeof(List))] - public List modules { get; set; } - - [Option("all", DefaultValue = false, HelpText = "Remove all installed mods.")] - public bool rmall { get; set; } - } - - internal class ImportOptions : InstanceSpecificOptions - { - [ValueList(typeof(List))] - public List paths { get; set; } - } - - internal class ShowOptions : InstanceSpecificOptions - { - [ValueOption(0)] public string Modname { get; set; } - } - - internal class SearchOptions : InstanceSpecificOptions - { - [Option("detail", HelpText = "Show full name, latest compatible version and short description of each module")] - public bool detail { get; set; } - - [Option("all", HelpText = "Show incompatible mods too")] - public bool all { get; set; } - - [Option("author", HelpText = "Limit search results to mods by matching authors")] - public string author_term { get; set; } - - [ValueOption(0)] - public string search_term { get; set; } - } - - internal class CompareOptions : CommonOptions - { - [Option("machine-readable", HelpText = "Output in a machine readable format: -1, 0 or 1")] - public bool machine_readable { get; set;} - - [ValueOption(0)] public string Left { get; set; } - [ValueOption(1)] public string Right { get; set; } - } + [Verb("scan", HelpText = "Scan for manually installed KSP mods")] + internal class ScanOptions : InstanceSpecificOptions { } } diff --git a/Cmdline/ParserExtensions.cs b/Cmdline/ParserExtensions.cs new file mode 100644 index 0000000000..7a2373eec3 --- /dev/null +++ b/Cmdline/ParserExtensions.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using CommandLine; + +namespace CKAN.CmdLine +{ + /// + /// Attribute to let the help screen recognize what the nested verbs are. + /// + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class ChildVerbsAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// A array used to supply nested verb alternatives. + /// Thrown if the array is . + /// Thrown if array is empty. + public ChildVerbsAttribute(params Type[] types) + { + if (types == null) + throw new ArgumentNullException(nameof(types)); + + if (types.Length == 0) + throw new ArgumentOutOfRangeException(nameof(types)); + + Types = types; + } + + /// + /// Gets the types of the nested verbs. + /// + public Type[] Types { get; } + } + + /// + /// Attribute to exclude the nested verbs from the main help screen. + /// + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class VerbExclude : Attribute { } + + /// + /// Extension methods to allow multi level verb parsing. + /// + public static class ParserVerbExtensions + { + /// + /// Parses a string array of arguments into the command line. + /// + /// A instance. + /// A array of command line arguments, to parse into the command line. + /// A array used to supply verb alternatives. + /// A containing the appropriate instance with parsed values as a and a sequence of . + /// Thrown if the , one or more arguments, or if the array is . + /// Thrown if array is empty. + public static ParserResult ParseVerbs(this Parser parser, IEnumerable args, params Type[] types) + { + if (parser == null) + throw new ArgumentNullException(nameof(parser)); + + if (args == null) + throw new ArgumentNullException(nameof(args)); + + if (types == null) + throw new ArgumentNullException(nameof(types)); + + if (types.Length == 0) + throw new ArgumentOutOfRangeException(nameof(types)); + + var argsArray = args as string[] ?? args.ToArray(); + if (argsArray.Length == 0 || argsArray[0].StartsWith("-")) + return parser.ParseArguments(argsArray, types); + + var verb = argsArray[0]; + foreach (var type in types) + { + var verbAttribute = type.GetCustomAttribute(); + if (verbAttribute == null || verbAttribute.Name != verb) + continue; + + var subVerbsAttribute = type.GetCustomAttribute(); + if (subVerbsAttribute != null) + return ParseVerbs(parser, argsArray.Skip(1).ToArray(), subVerbsAttribute.Types); + + break; + } + + return parser.ParseArguments(argsArray, types); + } + } +} diff --git a/Cmdline/ProgressReporter.cs b/Cmdline/ProgressReporter.cs deleted file mode 100644 index 655eddff18..0000000000 --- a/Cmdline/ProgressReporter.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Text.RegularExpressions; - -namespace CKAN.CmdLine -{ - /// - /// A simple class that manages progress report events for the CmdLine. - /// This will almost certainly need extra functionality if we deprecate the User class. - /// - public static class ProgressReporter - { - /// - /// Only shows download report messages, and nothing else. - /// - public static void FormattedDownloads(string message, int progress, IUser user) - { - if (Regex.IsMatch(message, "download", RegexOptions.IgnoreCase)) - { - user.RaiseMessage( - // The \r at the front here causes download messages to *overwrite* each other. - String.Format("\r{0} - {1}% ", message, progress) - ); - } - else - { - // The percent looks weird on non-download messages. - // The leading newline makes sure we don't end up with a mess from previous - // download messages. - user.RaiseMessage("\r\n{0}", message); - } - } - } -} - diff --git a/Cmdline/Properties/AssemblyInfo.cs b/Cmdline/Properties/AssemblyInfo.cs index 3e3faac6af..eb33e4b82e 100644 --- a/Cmdline/Properties/AssemblyInfo.cs +++ b/Cmdline/Properties/AssemblyInfo.cs @@ -1,4 +1,7 @@ using System.Reflection; +using System.Runtime.CompilerServices; -[assembly: AssemblyTitle("CKAN")] // TODO: Does not match assembly name +[assembly: AssemblyTitle("CmdLine")] [assembly: AssemblyDescription("CKAN CLI Client")] + +[assembly: InternalsVisibleTo("CKAN.Tests")] diff --git a/Core/Exit.cs b/Core/Exit.cs new file mode 100644 index 0000000000..fdcae1802b --- /dev/null +++ b/Core/Exit.cs @@ -0,0 +1,23 @@ +namespace CKAN +{ + /// + /// Exit codes for the command line interfaces. + /// + public static class Exit + { + /// + /// No errors. The program executed successfully. + /// + public static readonly int Ok = 0; + + /// + /// The program returned an error. + /// + public static readonly int Error = 1; + + /// + /// The command line parameters could not be parsed. + /// + public static readonly int BadOpt = 2; + } +} diff --git a/Core/Types/Kraken.cs b/Core/Types/Kraken.cs index 7ba2b8f5c9..38fe329ef9 100644 --- a/Core/Types/Kraken.cs +++ b/Core/Types/Kraken.cs @@ -99,6 +99,17 @@ public DependencyNotSatisfiedKraken(CkanModule parentModule, } } + public class NoGameInstanceKraken : Kraken + { + public readonly string path; + + public NoGameInstanceKraken(string path, string reason = null, Exception innerException = null) + : base(reason, innerException) + { + this.path = path; + } + } + public class NotKSPDirKraken : Kraken { public string path; diff --git a/Netkan/CKAN-netkan.csproj b/Netkan/CKAN-netkan.csproj index 5e90816999..339af44d9d 100644 --- a/Netkan/CKAN-netkan.csproj +++ b/Netkan/CKAN-netkan.csproj @@ -40,7 +40,7 @@ - + diff --git a/Netkan/CmdLineOptions.cs b/Netkan/CmdLineOptions.cs index f1f210727f..6ccdb3d368 100644 --- a/Netkan/CmdLineOptions.cs +++ b/Netkan/CmdLineOptions.cs @@ -7,16 +7,16 @@ namespace CKAN.NetKAN /// internal class CmdLineOptions { - [Option('v', "verbose", DefaultValue = false, HelpText = "Show more of what's going on when running")] + [Option('v', "verbose", HelpText = "Show more of what's going on when running")] public bool Verbose { get; set; } - [Option('d', "debug", DefaultValue = false, HelpText = "Show debugging level messages. Implies verbose")] + [Option('d', "debug", HelpText = "Show debugging level messages. Implies verbose")] public bool Debug { get; set; } [Option("debugger", HelpText = "Launch the debugger at start")] public bool Debugger { get; set; } - [Option("outputdir", DefaultValue = ".", HelpText = "Output directory")] + [Option("outputdir", Default = ".", HelpText = "Output directory")] public string OutputDir { get; set; } [Option("cachedir", HelpText = "Cache directory for downloaded mods")] @@ -25,22 +25,22 @@ internal class CmdLineOptions [Option("github-token", HelpText = "GitHub OAuth token for API access")] public string GitHubToken { get; set; } - [Option("net-useragent", DefaultValue = null, HelpText = "Set the default User-Agent string for HTTP requests")] + [Option("net-useragent", Default = null, HelpText = "Set the default User-Agent string for HTTP requests")] public string NetUserAgent { get; set; } - [Option("releases", DefaultValue = "1", HelpText = "Number of releases to inflate, or 'all'")] + [Option("releases", Default = "1", HelpText = "Number of releases to inflate, or 'all'")] public string Releases { get; set; } - [Option("skip-releases", DefaultValue = "0", HelpText = "Number of releases to skip / index of release to inflate.")] + [Option("skip-releases", Default = "0", HelpText = "Number of releases to skip / index of release to inflate.")] public string SkipReleases { get; set; } - [Option("prerelease", HelpText = "Index GitHub prereleases")] + [Option("prerelease", HelpText = "Index GitHub pre-releases")] public bool PreRelease { get; set; } [Option("overwrite-cache", HelpText = "Overwrite cached files")] public bool OverwriteCache { get; set; } - [Option("queues", HelpText = "Input,Output queue names for Queue Inflator mode")] + [Option("queues", HelpText = "Input / Output queue names for Queue Inflator mode")] public string Queues { get; set; } [Option("highest-version", HelpText = "Highest known version for auto-epoching")] @@ -49,10 +49,7 @@ internal class CmdLineOptions [Option("validate-ckan", HelpText = "Name of .ckan file to check for errors")] public string ValidateCkan { get; set; } - [Option("version", HelpText = "Display the netkan version number and exit")] - public bool Version { get; set; } - - [ValueOption(0)] + [Value(0, MetaName = "File", HelpText = "The .netkan file to inflate. This creates a .ckan file")] public string File { get; set; } } } diff --git a/Netkan/Program.cs b/Netkan/Program.cs index 5ee214129f..c3b31f6ea8 100644 --- a/Netkan/Program.cs +++ b/Netkan/Program.cs @@ -4,8 +4,9 @@ using System.Linq; using System.Net; using System.Text; -using CommandLine; +using CommandLine; +using CommandLine.Text; using log4net; using log4net.Core; using Newtonsoft.Json; @@ -21,32 +22,65 @@ namespace CKAN.NetKAN { public static class Program { - private const int ExitOk = 0; - private const int ExitBadOpt = 1; - private const int ExitError = 2; - private static readonly ILog Log = LogManager.GetLogger(typeof(Program)); private static CmdLineOptions Options { get; set; } public static int Main(string[] args) { + var parser = new Parser(c => c.HelpWriter = null).ParseArguments(args); + return parser.MapResult(opts => Run(opts), errs => + { + if (errs.IsVersion()) + { + Console.WriteLine(Meta.GetVersion(VersionFormat.Full)); + } + else + { + var ht = HelpText.AutoBuild(parser, h => + { + h.AddDashesToOption = true; // Add dashes to options + h.AddNewLineBetweenHelpSections = true; // Add blank line between heading and usage + h.AutoHelp = false; // Hide built-in help option + h.AutoVersion = false; // Hide built-in version option + h.Heading = $"NetKAN {Meta.GetVersion(VersionFormat.Full)}"; // Create custom heading + h.Copyright = $"Copyright © 2014-{DateTime.Now.Year}"; // Create custom copyright + h.AddPreOptionsLine("USAGE:\n netkan [options]"); // Show usage + return HelpText.DefaultParsingErrorsHandler(parser, h); + }, e => e, true); + Console.WriteLine(ht); + } + + return Exit.Ok; + }); + } + + private static int Run(CmdLineOptions options) + { + Options = options; try { - ProcessArgs(args); + if (Options.Debugger) + { + Debugger.Launch(); + } - // Force-allow TLS 1.2 for HTTPS URLs, because GitHub requires it. - // This is on by default in .NET 4.6, but not in 4.5. - ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; + Logging.Initialize(); + + LogManager.GetRepository().Threshold = + Options.Verbose ? Level.Info + : Options.Debug ? Level.Debug + : Level.Warn; - // If we see the --version flag, then display our build info - // and exit. - if (Options.Version) + if (Options.NetUserAgent != null) { - Console.WriteLine(Meta.GetVersion(VersionFormat.Full)); - return ExitOk; + Net.UserAgentString = Options.NetUserAgent; } + // Force-allow TLS 1.2 for HTTPS URLs, because GitHub requires it. + // This is on by default in .NET 4.6, but not in 4.5. + ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; + if (!string.IsNullOrEmpty(Options.ValidateCkan)) { var ckan = new Metadata(JObject.Parse(File.ReadAllText(Options.ValidateCkan))); @@ -57,15 +91,13 @@ public static int Main(string[] args) Options.PreRelease ); inf.ValidateCkan(ckan); - Console.WriteLine(QueueHandler.serializeCkan( - new PropertySortTransformer().Transform(ckan, null).First() - )); - return ExitOk; + Console.WriteLine(QueueHandler.serializeCkan(new PropertySortTransformer().Transform(ckan, null).First())); + return Exit.Ok; } if (!string.IsNullOrEmpty(Options.Queues)) { - var queues = Options.Queues.Split(new char[] { ',' }, 2); + var queues = Options.Queues.Split(new[] { ',' }, 2); var qh = new QueueHandler( queues[0], queues[1], @@ -75,7 +107,7 @@ public static int Main(string[] args) Options.PreRelease ); qh.Process(); - return ExitOk; + return Exit.Ok; } if (Options.File != null) @@ -91,6 +123,7 @@ public static int Main(string[] args) Options.GitHubToken, Options.PreRelease ); + var ckans = inf.Inflate( Options.File, netkan, @@ -100,6 +133,7 @@ public static int Main(string[] args) ParseHighestVersion(Options.HighestVersion) ) ); + foreach (Metadata ckan in ckans) { WriteCkan(ckan); @@ -107,10 +141,8 @@ public static int Main(string[] args) } else { - Log.Fatal( - "Usage: netkan [--verbose|--debug] [--debugger] [--prerelease] [--outputdir=...] " - ); - return ExitBadOpt; + Console.WriteLine("There was no file provided, maybe you forgot it?\n\nUSAGE:\n netkan [options]"); + return Exit.BadOpt; } } catch (Exception e) @@ -124,10 +156,10 @@ public static int Main(string[] args) Log.Fatal(e.StackTrace); } - return ExitError; + return Exit.Error; } - return ExitOk; + return Exit.Ok; } private static int? ParseReleases(string val) @@ -145,29 +177,6 @@ private static ModuleVersion ParseHighestVersion(string val) return val == null ? null : new ModuleVersion(val); } - private static void ProcessArgs(string[] args) - { - if (args.Any(i => i == "--debugger")) - { - Debugger.Launch(); - } - - Options = new CmdLineOptions(); - Parser.Default.ParseArgumentsStrict(args, Options); - - Logging.Initialize(); - - LogManager.GetRepository().Threshold = - Options.Verbose ? Level.Info - : Options.Debug ? Level.Debug - : Level.Warn; - - if (Options.NetUserAgent != null) - { - Net.UserAgentString = Options.NetUserAgent; - } - } - private static Metadata ReadNetkan() { if (!Options.File.EndsWith(".netkan")) @@ -211,6 +220,5 @@ private static void WriteCkan(Metadata metadata) Log.InfoFormat("Transformation written to {0}", finalPath); } - } }