diff --git a/Duplicati/CommandLine/BackendTester/Program.cs b/Duplicati/CommandLine/BackendTester/Program.cs index e10c1fc5d..cd9b4a848 100644 --- a/Duplicati/CommandLine/BackendTester/Program.cs +++ b/Duplicati/CommandLine/BackendTester/Program.cs @@ -73,7 +73,7 @@ namespace Duplicati.CommandLine.BackendTester Console.WriteLine(); List lines = new List(); foreach (Library.Interface.ICommandLineArgument arg in SupportedCommands) - Library.Interface.CommandLineArgument.PrintArgument(lines, arg, " "); + Library.Interface.CommandLineArgument.PrintArgument(lines, arg); foreach (string s in lines) Console.WriteLine(s); diff --git a/Duplicati/CommandLine/Duplicati.CommandLine.csproj b/Duplicati/CommandLine/Duplicati.CommandLine.csproj index b9f0d92fa..0d2529941 100644 --- a/Duplicati/CommandLine/Duplicati.CommandLine.csproj +++ b/Duplicati/CommandLine/Duplicati.CommandLine.csproj @@ -1,4 +1,4 @@ - + Debug @@ -61,6 +61,7 @@ + @@ -99,6 +100,10 @@ {FF2BF37C-E502-4C98-BEA0-701671DDFA08} Duplicati.Library.Backend.SSHv2 + + {C0270709-2A40-43B5-8CF1-69581B9FA2A1} + Duplicati.Library.Backend.TahoeLAFS + {BAE27510-8B5D-44B2-B33E-372A98908041} Duplicati.Library.Backend.WEBDAV @@ -185,6 +190,7 @@ win-tools\pscp.exe PreserveNewest + diff --git a/Duplicati/CommandLine/Help.cs b/Duplicati/CommandLine/Help.cs new file mode 100644 index 000000000..a1960f98f --- /dev/null +++ b/Duplicati/CommandLine/Help.cs @@ -0,0 +1,462 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Duplicati.CommandLine +{ + public static class Help + { + private static readonly Dictionary _document; + private const string RESOURCE_NAME = "Duplicati.CommandLine.help.txt"; + private static readonly System.Text.RegularExpressions.Regex NAMEDOPTION_REGEX = new System.Text.RegularExpressions.Regex("\\%OPTION\\:(?[^\\%]+)\\%"); + + static Help() + { + _document = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + using (System.IO.StreamReader sr = new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(RESOURCE_NAME))) + { + List keywords = new List(); + StringBuilder sb = new StringBuilder(); + foreach(var line in sr.ReadToEnd().Split(new string[] { "\r\n" }, StringSplitOptions.None)) + { + if (line.Trim().StartsWith("#")) + continue; + + if (line.Trim().StartsWith(">")) + { + if (sb.Length > 0) + { + string s = sb.ToString(); + foreach(var k in keywords) + _document[k] = s; + + keywords.Clear(); + sb.Clear(); + } + + string[] elems = line.Split(new string[] {" ", "\t"}, StringSplitOptions.RemoveEmptyEntries); + if (elems.Length >= 2 && string.Equals(elems[elems.Length - 2], "help", StringComparison.InvariantCultureIgnoreCase)) + keywords.Add(elems[elems.Length - 1]); + else if (elems.Length == 3 && string.Equals(elems[elems.Length - 1], "help", StringComparison.InvariantCultureIgnoreCase)) + keywords.Add("help"); + } + else + { + sb.AppendLine(line); + } + } + + if (sb.Length > 0) + { + string s = sb.ToString(); + foreach(var k in keywords) + _document[k] = s; + } + } + } + + public static void PrintUsage(string topic, IDictionary options) + { + try + { + //Force translation off + System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture; + System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.InvariantCulture; + } + catch { } + + if (string.IsNullOrWhiteSpace(topic)) + topic = "help"; + + if (string.Equals("help", topic, StringComparison.InvariantCultureIgnoreCase) && options.Count == 1) + topic = new List(options.Keys)[0]; + + + if (_document.ContainsKey(topic)) + { + string tp = _document[topic]; + Library.Main.Options opts = new Library.Main.Options(new Dictionary()); + + tp = tp.Replace("%VERSION%", License.VersionNumbers.Version); + tp = tp.Replace("%BACKENDS%", string.Join(", ", Library.DynamicLoader.BackendLoader.Keys)); + tp = tp.Replace("%MONO%", Library.Utility.Utility.IsMono ? "mono " : ""); + tp = tp.Replace("%APP_PATH%", System.IO.Path.GetFileName(System.Reflection.Assembly.GetExecutingAssembly().Location)); + tp = tp.Replace("%EXAMPLE_SOURCE_PATH%", Library.Utility.Utility.IsClientLinux ? "/source" : "D:\\source"); + tp = tp.Replace("%EXAMPLE_SOURCE_FILE%", Library.Utility.Utility.IsClientLinux ? "/source/myfile.txt" : "D:\\source\\file.txt"); + tp = tp.Replace("%EXAMPLE_RESTORE_PATH%", Library.Utility.Utility.IsClientLinux ? "/restore" : "D:\\restore"); + tp = tp.Replace("%ENCRYPTIONMODULES%", string.Join(", ", Library.DynamicLoader.EncryptionLoader.Keys)); + tp = tp.Replace("%COMPRESSIONMODULES%", string.Join(", ", Library.DynamicLoader.CompressionLoader.Keys)); + tp = tp.Replace("%DEFAULTENCRYPTIONMODULE%", opts.EncryptionModule); + tp = tp.Replace("%DEFAULTCOMPRESSIONMODULE%", opts.CompressionModule); + tp = tp.Replace("%GENERICMODULES%", string.Join(", ", Library.DynamicLoader.GenericLoader.Keys)); + + if (tp.Contains("%MAINOPTIONS%")) + { + List lines = new List(); + SortedList sorted = new SortedList(); + foreach (Library.Interface.ICommandLineArgument arg in opts.SupportedCommands) + sorted.Add(arg.Name, arg); + + foreach (Library.Interface.ICommandLineArgument arg in sorted.Values) + lines.Add(PrintArgSimple(arg, arg.Name)); + + tp = tp.Replace("%MAINOPTIONS%", string.Join(Environment.NewLine, lines.ToArray())); + } + + if (tp.Contains("%ALLOPTIONS%")) + { + List lines = new List(); + foreach (Library.Interface.ICommandLineArgument arg in opts.SupportedCommands) + Library.Interface.CommandLineArgument.PrintArgument(lines, arg, " "); + + lines.Add(""); + lines.Add(""); + lines.Add(Strings.Program.SupportedBackendsHeader); + foreach (Duplicati.Library.Interface.IBackend back in Library.DynamicLoader.BackendLoader.Backends) + PrintBackend(back, lines); + + lines.Add(""); + lines.Add(""); + lines.Add(Strings.Program.SupportedEncryptionModulesHeader); + foreach (Duplicati.Library.Interface.IEncryption mod in Library.DynamicLoader.EncryptionLoader.Modules) + PrintEncryptionModule(mod, lines); + + lines.Add(""); + lines.Add(""); + lines.Add(Strings.Program.SupportedCompressionModulesHeader); + foreach (Duplicati.Library.Interface.ICompression mod in Library.DynamicLoader.CompressionLoader.Modules) + PrintCompressionModule(mod, lines); + + lines.Add(""); + + lines.Add(""); + lines.Add(""); + lines.Add(Strings.Program.GenericModulesHeader); + foreach (Duplicati.Library.Interface.IGenericModule mod in Library.DynamicLoader.GenericLoader.Modules) + PrintGenericModule(mod, lines); + + lines.Add(""); + + tp = tp.Replace("%ALLOPTIONS%", string.Join(Environment.NewLine, lines.ToArray())); + } + + if (tp.Contains("%MODULEOPTIONS%")) + { + //Figure out which module we are in + IList args = null; + bool found = false; + foreach (Duplicati.Library.Interface.IBackend backend in Library.DynamicLoader.BackendLoader.Backends) + if (string.Equals(backend.ProtocolKey, topic, StringComparison.InvariantCultureIgnoreCase)) + { + args = backend.SupportedCommands; + found = true; + break; + } + + if (args == null) + foreach (Duplicati.Library.Interface.IEncryption module in Library.DynamicLoader.EncryptionLoader.Modules) + if (string.Equals(module.FilenameExtension, topic, StringComparison.InvariantCultureIgnoreCase)) + { + args = module.SupportedCommands; + found = true; + break; + } + + if (args == null) + foreach (Duplicati.Library.Interface.ICompression module in Library.DynamicLoader.CompressionLoader.Modules) + if (string.Equals(module.FilenameExtension, topic, StringComparison.InvariantCultureIgnoreCase)) + { + args = module.SupportedCommands; + found = true; + break; + } + + if (args == null) + foreach (Duplicati.Library.Interface.IGenericModule module in Library.DynamicLoader.GenericLoader.Modules) + if (string.Equals(module.Key, topic, StringComparison.InvariantCultureIgnoreCase)) + { + args = module.SupportedCommands; + found = true; + break; + } + + //If the module is not found, we do not display the description + if (found) + tp = tp.Replace("%MODULEOPTIONS%", PrintArgsSimple(args)); + else + { + Console.WriteLine("Topic not found: {0}", topic); + Console.WriteLine(); + PrintUsage("help", new Dictionary()); + return; + } + } + + if (NAMEDOPTION_REGEX.IsMatch(tp)) + tp = NAMEDOPTION_REGEX.Replace(tp, new Matcher().MathEvaluator); + + PrintFormatted(tp.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)); + } + else + { + List lines = new List(); + + foreach (Duplicati.Library.Interface.IBackend backend in Library.DynamicLoader.BackendLoader.Backends) + if (string.Equals(backend.ProtocolKey, topic, StringComparison.InvariantCultureIgnoreCase)) + { + PrintBackend(backend, lines); + break; + } + + if (lines.Count == 0) + foreach (Duplicati.Library.Interface.IEncryption mod in Library.DynamicLoader.EncryptionLoader.Modules) + if (string.Equals(mod.FilenameExtension, topic, StringComparison.InvariantCultureIgnoreCase)) + { + PrintEncryptionModule(mod, lines); + break; + } + + if (lines.Count == 0) + foreach (Duplicati.Library.Interface.ICompression mod in Library.DynamicLoader.CompressionLoader.Modules) + if (string.Equals(mod.FilenameExtension, topic, StringComparison.InvariantCultureIgnoreCase)) + { + PrintCompressionModule(mod, lines); + break; + } + + if (lines.Count == 0) + foreach (Duplicati.Library.Interface.IGenericModule mod in Library.DynamicLoader.GenericLoader.Modules) + if (string.Equals(mod.Key, topic, StringComparison.InvariantCultureIgnoreCase)) + { + PrintGenericModule(mod, lines); + break; + } + + + if (lines.Count == 0) + PrintArgumentIfFound(new Matcher().Values, topic, lines); + + if (lines.Count != 0) + { + PrintFormatted(lines); + } + else + { + Console.WriteLine("Topic not found: {0}", topic); + Console.WriteLine(); + PrintUsage("help", new Dictionary()); + } + } + } + + private static void PrintArgumentIfFound(IEnumerable args, string topic, List lines) + { + if (args == null) + return; + + foreach (Duplicati.Library.Interface.ICommandLineArgument arg in args) + { + if (string.Equals(arg.Name, topic, StringComparison.InvariantCultureIgnoreCase)) + { + Library.Interface.CommandLineArgument.PrintArgument(lines, arg, " "); + return; + } + + if (arg.Aliases != null) + foreach (string k in arg.Aliases) + { + if (string.Equals(k, topic, StringComparison.InvariantCultureIgnoreCase)) + { + Library.Interface.CommandLineArgument.PrintArgument(lines, arg, " "); + return; + } + } + } + } + + private static void PrintBackend(Duplicati.Library.Interface.IBackend back, List lines) + { + lines.Add(back.DisplayName + " (" + back.ProtocolKey + "):"); + lines.Add(" " + back.Description); + if (back.SupportedCommands != null && back.SupportedCommands.Count > 0) + { + lines.Add(" " + Strings.Program.SupportedOptionsHeader); + foreach (Library.Interface.ICommandLineArgument arg in back.SupportedCommands) + Library.Interface.CommandLineArgument.PrintArgument(lines, arg, " "); + } + lines.Add(""); + } + + private static void PrintEncryptionModule(Duplicati.Library.Interface.IEncryption mod, List lines) + { + lines.Add(mod.DisplayName + " (." + mod.FilenameExtension + "):"); + lines.Add(" " + mod.Description); + if (mod.SupportedCommands != null && mod.SupportedCommands.Count > 0) + { + lines.Add(" " + Strings.Program.SupportedOptionsHeader); + foreach (Library.Interface.ICommandLineArgument arg in mod.SupportedCommands) + Library.Interface.CommandLineArgument.PrintArgument(lines, arg, " "); + } + lines.Add(""); + } + + private static void PrintCompressionModule(Duplicati.Library.Interface.ICompression mod, List lines) + { + lines.Add(mod.DisplayName + " (." + mod.FilenameExtension + "):"); + lines.Add(" " + mod.Description); + if (mod.SupportedCommands != null && mod.SupportedCommands.Count > 0) + { + lines.Add(" " + Strings.Program.SupportedOptionsHeader); + foreach (Library.Interface.ICommandLineArgument arg in mod.SupportedCommands) + Library.Interface.CommandLineArgument.PrintArgument(lines, arg, " "); + } + lines.Add(""); + } + + private static void PrintGenericModule(Duplicati.Library.Interface.IGenericModule mod, List lines) + { + lines.Add(mod.DisplayName + " (" + mod.Key + "):"); + lines.Add(" " + mod.Description); + lines.Add(" " + (mod.LoadAsDefault ? Strings.Program.ModuleIsLoadedAutomatically : Strings.Program.ModuleIsNotLoadedAutomatically)); + if (mod.SupportedCommands != null && mod.SupportedCommands.Count > 0) + { + lines.Add(" " + Strings.Program.SupportedOptionsHeader); + foreach (Library.Interface.ICommandLineArgument arg in mod.SupportedCommands) + Library.Interface.CommandLineArgument.PrintArgument(lines, arg, " "); + } + lines.Add(""); + } + + private static string PrintArguments(IEnumerable args) + { + if (args == null) + return ""; + + List lines = new List(); + foreach (Library.Interface.ICommandLineArgument arg in args) + Library.Interface.CommandLineArgument.PrintArgument(lines, arg, " "); + + return string.Join(Environment.NewLine, lines.ToArray()); + + } + + private static void PrintFormatted(IEnumerable lines) + { + foreach (string s in lines) + { + if (string.IsNullOrEmpty(s) || s.Trim().Length == 0) + { + Console.WriteLine(); + continue; + } + + string c = s; + + string leadingSpaces = ""; + while (c.Length > 0 && c.StartsWith(" ")) + { + leadingSpaces += " "; + c = c.Remove(0, 1); + } + + bool extraIndent = c.StartsWith("--"); + + while (c.Length > 0) + { + int len = Math.Min(Console.WindowWidth - 2, leadingSpaces.Length + c.Length); + len -= leadingSpaces.Length; + if (len < c.Length) + { + int ix = c.LastIndexOf(" ", len); + if (ix > 0) + len = ix; + } + + Console.WriteLine(leadingSpaces + c.Substring(0, len).Trim()); + c = c.Remove(0, len); + if (extraIndent) + { + extraIndent = false; + leadingSpaces += " "; + } + } + } + } + + private class Matcher + { + Dictionary args = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + public Matcher() + { + Library.Main.Options opts = new Library.Main.Options(new Dictionary()); + List> foundArgs = new List>(); + foundArgs.Add(new Library.Main.Options(new Dictionary()).SupportedCommands); + foreach (Duplicati.Library.Interface.IBackend backend in Library.DynamicLoader.BackendLoader.Backends) + if (backend.SupportedCommands != null) + foundArgs.Add(backend.SupportedCommands); + foreach (Duplicati.Library.Interface.IEncryption mod in Library.DynamicLoader.EncryptionLoader.Modules) + if (mod.SupportedCommands != null) + foundArgs.Add(mod.SupportedCommands); + foreach (Duplicati.Library.Interface.ICompression mod in Library.DynamicLoader.CompressionLoader.Modules) + if (mod.SupportedCommands != null) + foundArgs.Add(mod.SupportedCommands); + foreach (Duplicati.Library.Interface.IGenericModule mod in Library.DynamicLoader.GenericLoader.Modules) + if (mod.SupportedCommands != null) + foundArgs.Add(mod.SupportedCommands); + + foreach (IEnumerable arglst in foundArgs) + if (arglst != null) + { + foreach (Library.Interface.ICommandLineArgument arg in arglst) + { + if (!args.ContainsKey(arg.Name)) + args[arg.Name] = arg; + + if (arg.Aliases != null) + foreach (string a in arg.Aliases) + if (!args.ContainsKey(a)) + args[a] = arg; + } + } + } + + public IEnumerable Values { get { return args.Values; } } + + public string MathEvaluator(System.Text.RegularExpressions.Match m) + { + if (!m.Success || !m.Groups["name"].Success) + return ""; + + Duplicati.Library.Interface.ICommandLineArgument arg; + if (!args.TryGetValue(m.Groups["name"].Value, out arg)) + return ""; + else + return PrintArgSimple(arg, m.Groups["name"].Value.ToLowerInvariant()); + } + } + + private static string PrintArgsSimple(IEnumerable args) + { + if (args == null) + return ""; + + List lines = new List(); + foreach(Duplicati.Library.Interface.ICommandLineArgument arg in args) + lines.Add(PrintArgSimple(arg, arg.Name)); + + return string.Join(Environment.NewLine, lines.ToArray()); + } + + + private static string PrintArgSimple(Duplicati.Library.Interface.ICommandLineArgument arg, string name) + { + if (string.IsNullOrEmpty(arg.DefaultValue)) + return string.Format(" --{0}{1} {2}", name, Environment.NewLine, arg.LongDescription); + else + return string.Format(" --{0} = {1}{2} {3}", name, arg.DefaultValue, Environment.NewLine, arg.LongDescription); + } + } +} diff --git a/Duplicati/CommandLine/Program.cs b/Duplicati/CommandLine/Program.cs index b524c7713..aa3f937a4 100644 --- a/Duplicati/CommandLine/Program.cs +++ b/Duplicati/CommandLine/Program.cs @@ -105,15 +105,12 @@ namespace Duplicati.CommandLine } } - if (cargs.Count < 2) + if (cargs.Count < 2 || cargs[0].Trim().Equals("help", StringComparison.InvariantCultureIgnoreCase)) { - if (cargs.Count == 0 || cargs[0].Trim().Equals("help", StringComparison.InvariantCultureIgnoreCase)) - PrintUsage(true); + if (cargs.Count < 2) + Help.PrintUsage("help", options); else - { - PrintUsage(false); - PrintWrongNumberOfArguments(cargs, 2); - } + Help.PrintUsage(cargs[1], options); return; @@ -130,6 +127,12 @@ namespace Duplicati.CommandLine options["restore"] = null; cargs.RemoveAt(0); } + else if (source.Trim().ToLower() == "backup" && cargs.Count == 3) + { + source = target; + target = cargs[2]; + cargs.RemoveAt(0); + } if (!options.ContainsKey("passphrase")) if (!string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("PASSPHRASE"))) @@ -455,7 +458,7 @@ namespace Duplicati.CommandLine } - private static void PrintUsage(bool extended) + private static void PrintOldUsage(bool extended) { bool isLinux = Library.Utility.Utility.IsClientLinux; diff --git a/Duplicati/CommandLine/help.txt b/Duplicati/CommandLine/help.txt new file mode 100644 index 000000000..b6b6dca52 --- /dev/null +++ b/Duplicati/CommandLine/help.txt @@ -0,0 +1,385 @@ +# This file is the help for the Duplicati Command Line Client. It shows what +# commands the CLI understands and what the outputs are. Sometimes we used +# placeholders for options. In that case descriptions are taken directly from +# the modules. Sometimes we described options here. The reason for that is, +# that we found the new description much better but did not want to create a +# need for translation as many options are described in the UI as well and +# they are translated there. +# ============================================================================= +> duplicati.commandline.exe +> duplicati.commandline.exe help + +See duplicati.commandline.exe help for more information. + + Examples: example + + Commands: backup, restore, delete-all-but-n-full, delete-all-but-n, cleanup, delete-older-than, list, list-current-files, list-actual-signature-files, collection-status, create-folder, find-last-version, verify + + Debugging: debug, logging + + Targets: %BACKENDS% + + Modules: %ENCRYPTIONMODULES%, %COMPRESSIONMODULES%, %GENERICMODULES% + + Formats: date, time, size, encryption, compression + + Advanced options: advanced, filter,