From 5eeea56bda776bbdda7ca9d7b481aaccf26c03c2 Mon Sep 17 00:00:00 2001
From: Dwayne Bent <dbb@dbb.io>
Date: Wed, 10 Jun 2015 20:13:05 -0400
Subject: [PATCH 01/13] Update export list

---
 .gitignore                   |  1 +
 Core/CKAN-core.csproj        |  3 ++-
 Core/Types/ExportFileType.cs | 12 +++++++++
 GUI/CKAN-GUI.csproj          |  1 +
 GUI/ExportOption.cs          | 27 ++++++++++++++++++++
 GUI/Main.cs                  | 49 ++++++++++++++++++++++++++++++------
 6 files changed, 85 insertions(+), 8 deletions(-)
 create mode 100644 Core/Types/ExportFileType.cs
 create mode 100644 GUI/ExportOption.cs

diff --git a/.gitignore b/.gitignore
index 0df9ea148c..575582a53a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 build/
+packages/
 CKAN/*/bin
 CKAN/*/obj
 Tests/bin
diff --git a/Core/CKAN-core.csproj b/Core/CKAN-core.csproj
index 08b32a12ab..2031050fcd 100644
--- a/Core/CKAN-core.csproj
+++ b/Core/CKAN-core.csproj
@@ -46,6 +46,7 @@
     <Compile Include="KSP.cs" />
     <Compile Include="ModuleInstaller.cs" />
     <Compile Include="CkanTransaction.cs" />
+    <Compile Include="Types\ExportFileType.cs" />
     <Compile Include="User.cs" />
     <Compile Include="Types\Version.cs" />
     <Compile Include="Types\KSPVersion.cs" />
@@ -83,4 +84,4 @@
   </ItemGroup>
   <ItemGroup />
   <ItemGroup />
-</Project>
+</Project>
\ No newline at end of file
diff --git a/Core/Types/ExportFileType.cs b/Core/Types/ExportFileType.cs
new file mode 100644
index 0000000000..87cf1ec99c
--- /dev/null
+++ b/Core/Types/ExportFileType.cs
@@ -0,0 +1,12 @@
+namespace CKAN.Types
+{
+    public enum ExportFileType
+    {
+        Ckan,
+        PlainText,
+        Markdown,
+        BbCode,
+        Csv,
+        Tsv
+    }
+}
diff --git a/GUI/CKAN-GUI.csproj b/GUI/CKAN-GUI.csproj
index c5f6454641..25ada8d56a 100644
--- a/GUI/CKAN-GUI.csproj
+++ b/GUI/CKAN-GUI.csproj
@@ -77,6 +77,7 @@
     <Compile Include="ChooseKSPInstance.Designer.cs">
       <DependentUpon>ChooseKSPInstance.cs</DependentUpon>
     </Compile>
+    <Compile Include="ExportOption.cs" />
     <Compile Include="IGUIPlugin.cs">
       <SubType>Code</SubType>
     </Compile>
diff --git a/GUI/ExportOption.cs b/GUI/ExportOption.cs
new file mode 100644
index 0000000000..3365de87fb
--- /dev/null
+++ b/GUI/ExportOption.cs
@@ -0,0 +1,27 @@
+using CKAN.Types;
+
+namespace CKAN
+{
+    internal sealed class ExportOption
+    {
+        private readonly string _string;
+
+        public ExportFileType ExportFileType { get; private set; }
+        public string FriendlyName { get; private set; }
+        public string Extension { get; private set; }
+
+        public ExportOption(ExportFileType exportFileType, string friendlyName, string extension)
+        {
+            ExportFileType = exportFileType;
+            FriendlyName = friendlyName;
+            Extension = extension;
+
+            _string = string.Format("{0}|{1}", FriendlyName, "*." + Extension);
+        }
+
+        public override string ToString()
+        {
+            return _string;
+        }
+    }
+}
diff --git a/GUI/Main.cs b/GUI/Main.cs
index 17e14acc73..79e8f0129b 100644
--- a/GUI/Main.cs
+++ b/GUI/Main.cs
@@ -9,6 +9,7 @@
 using System.Threading.Tasks;
 using System.Windows.Forms;
 using CKAN.Properties;
+using CKAN.Types;
 using log4net;
 using Timer = System.Windows.Forms.Timer;
 
@@ -926,16 +927,50 @@ private void installFromckanToolStripMenuItem_Click(object sender, EventArgs e)
         /// <param name="e"></param>
         private void exportModListToolStripMenuItem_Click(object sender, EventArgs e)
         {
-            var dlg = new SaveFileDialog();
-            dlg.Filter = Resources.CKANFileFilter;
-            dlg.Title = Resources.ExportInstalledModsDialogTitle;
+            var exportOptions = new List<ExportOption>
+            {
+                new ExportOption(Types.ExportFileType.Ckan, "CKAN metadata (*.ckan)", "ckan"),
+                new ExportOption(Types.ExportFileType.PlainText, "Plain text (*.txt)", "txt"),
+                new ExportOption(Types.ExportFileType.Markdown, "Markdown (*.md)", "md"),
+                new ExportOption(Types.ExportFileType.BbCode, "BBCode (*.txt)", "txt"),
+                new ExportOption(Types.ExportFileType.Csv, "Comma-seperated values (*.csv)", "csv"),
+                new ExportOption(Types.ExportFileType.Tsv, "Tab-seperated values (*.tsv)", "tsv")
+            };
+
+            var filter = string.Join("|", exportOptions.Select(i => i.ToString()).ToArray());
+
+            var dlg = new SaveFileDialog
+            {
+                Filter = filter,
+                Title = Resources.ExportInstalledModsDialogTitle
+            };
 
             if (dlg.ShowDialog() == DialogResult.OK) {
-                // Save, just to be certain that the installed-*.ckan metapackage is generated
-                RegistryManager.Instance(CurrentInstance).Save();
 
-                // TODO: The core might eventually save as something other than 'installed-default.ckan'
-                File.Copy(Path.Combine(CurrentInstance.CkanDir(), "installed-default.ckan"), dlg.FileName);
+                var exportOption = exportOptions[dlg.FilterIndex];
+
+                switch (exportOption.ExportFileType)
+                {
+                    case ExportFileType.Ckan:
+                        // Save, just to be certain that the installed-*.ckan metapackage is generated
+                        RegistryManager.Instance(CurrentInstance).Save();
+
+                        // TODO: The core might eventually save as something other than 'installed-default.ckan'
+                        File.Copy(Path.Combine(CurrentInstance.CkanDir(), "installed-default.ckan"), dlg.FileName);
+                        break;
+                    case ExportFileType.PlainText:
+                        break;
+                    case ExportFileType.Markdown:
+                        break;
+                    case ExportFileType.BbCode:
+                        break;
+                    case ExportFileType.Csv:
+                        break;
+                    case ExportFileType.Tsv:
+                        break;
+                    default:
+                        throw new ArgumentOutOfRangeException();
+                }
             }
         }
 

From f1beaf340e9d50d4454a7d9126025ef9321b7d92 Mon Sep 17 00:00:00 2001
From: Dwayne Bent <dbb@dbb.io>
Date: Wed, 10 Jun 2015 20:31:48 -0400
Subject: [PATCH 02/13] Implement PlainText export

---
 Core/CKAN-core.csproj               |  2 ++
 Core/Exporters/IExporter.cs         | 17 ++++++++++
 Core/Exporters/PlainTextExporter.cs | 17 ++++++++++
 GUI/Main.cs                         | 51 ++++++++++++++++-------------
 4 files changed, 65 insertions(+), 22 deletions(-)
 create mode 100644 Core/Exporters/IExporter.cs
 create mode 100644 Core/Exporters/PlainTextExporter.cs

diff --git a/Core/CKAN-core.csproj b/Core/CKAN-core.csproj
index 2031050fcd..5c60d3c3f7 100644
--- a/Core/CKAN-core.csproj
+++ b/Core/CKAN-core.csproj
@@ -41,6 +41,8 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="Exporters\IExporter.cs" />
+    <Compile Include="Exporters\PlainTextExporter.cs" />
     <Compile Include="Net\AutoUpdate.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="KSP.cs" />
diff --git a/Core/Exporters/IExporter.cs b/Core/Exporters/IExporter.cs
new file mode 100644
index 0000000000..7cb98b849d
--- /dev/null
+++ b/Core/Exporters/IExporter.cs
@@ -0,0 +1,17 @@
+using System.IO;
+
+namespace CKAN.Exporters
+{
+    /// <summary>
+    /// Represents an object that can 
+    /// </summary>
+    public interface IExporter
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="registry"></param>
+        /// <param name="writer"></param>
+        void Export(Registry registry, TextWriter writer);
+    }
+}
diff --git a/Core/Exporters/PlainTextExporter.cs b/Core/Exporters/PlainTextExporter.cs
new file mode 100644
index 0000000000..1e229f15df
--- /dev/null
+++ b/Core/Exporters/PlainTextExporter.cs
@@ -0,0 +1,17 @@
+using System.IO;
+
+namespace CKAN.Exporters
+{
+    public sealed class PlainTextExporter : IExporter
+    {
+        public void Export(Registry registry, TextWriter writer)
+        {
+            foreach (var mod in registry.InstalledModules)
+            {
+                writer.WriteLine(@"{0} ({1} {2})", mod.Module.name, mod.identifier, mod.Module.version);
+            }
+
+            writer.Flush();
+        }
+    }
+}
diff --git a/GUI/Main.cs b/GUI/Main.cs
index 79e8f0129b..ac825baff1 100644
--- a/GUI/Main.cs
+++ b/GUI/Main.cs
@@ -8,6 +8,7 @@
 using System.Threading;
 using System.Threading.Tasks;
 using System.Windows.Forms;
+using CKAN.Exporters;
 using CKAN.Properties;
 using CKAN.Types;
 using log4net;
@@ -945,31 +946,37 @@ private void exportModListToolStripMenuItem_Click(object sender, EventArgs e)
                 Title = Resources.ExportInstalledModsDialogTitle
             };
 
-            if (dlg.ShowDialog() == DialogResult.OK) {
-
-                var exportOption = exportOptions[dlg.FilterIndex];
+            if (dlg.ShowDialog() == DialogResult.OK)
+            {
+                var exportOption = exportOptions[dlg.FilterIndex - 1]; // FilterIndex is 1-indexed
 
-                switch (exportOption.ExportFileType)
+                using (var writer = new StreamWriter(new FileStream(dlg.FileName, FileMode.OpenOrCreate)))
                 {
-                    case ExportFileType.Ckan:
-                        // Save, just to be certain that the installed-*.ckan metapackage is generated
-                        RegistryManager.Instance(CurrentInstance).Save();
+                    var registry = RegistryManager.Instance(CurrentInstance).registry;
 
-                        // TODO: The core might eventually save as something other than 'installed-default.ckan'
-                        File.Copy(Path.Combine(CurrentInstance.CkanDir(), "installed-default.ckan"), dlg.FileName);
-                        break;
-                    case ExportFileType.PlainText:
-                        break;
-                    case ExportFileType.Markdown:
-                        break;
-                    case ExportFileType.BbCode:
-                        break;
-                    case ExportFileType.Csv:
-                        break;
-                    case ExportFileType.Tsv:
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException();
+                    switch (exportOption.ExportFileType)
+                    {
+                        case ExportFileType.Ckan:
+                            // Save, just to be certain that the installed-*.ckan metapackage is generated
+                            RegistryManager.Instance(CurrentInstance).Save();
+
+                            // TODO: The core might eventually save as something other than 'installed-default.ckan'
+                            File.Copy(Path.Combine(CurrentInstance.CkanDir(), "installed-default.ckan"), dlg.FileName);
+                            break;
+                        case ExportFileType.PlainText:
+                            new PlainTextExporter().Export(registry, writer);
+                            break;
+                        case ExportFileType.Markdown:
+                            break;
+                        case ExportFileType.BbCode:
+                            break;
+                        case ExportFileType.Csv:
+                            break;
+                        case ExportFileType.Tsv:
+                            break;
+                        default:
+                            throw new ArgumentOutOfRangeException();
+                    }
                 }
             }
         }

From 67322b8b5da8c21bb335dc9191ee1b7f02243465 Mon Sep 17 00:00:00 2001
From: Dwayne Bent <dbb@dbb.io>
Date: Wed, 10 Jun 2015 20:40:25 -0400
Subject: [PATCH 03/13] Implement MarkdownExporter

---
 Core/CKAN-core.csproj               |  1 +
 Core/Exporters/MarkdownExporter.cs  | 18 ++++++++++++++++++
 Core/Exporters/PlainTextExporter.cs |  3 ++-
 GUI/Main.cs                         |  5 ++++-
 4 files changed, 25 insertions(+), 2 deletions(-)
 create mode 100644 Core/Exporters/MarkdownExporter.cs

diff --git a/Core/CKAN-core.csproj b/Core/CKAN-core.csproj
index 5c60d3c3f7..cb64bcc86e 100644
--- a/Core/CKAN-core.csproj
+++ b/Core/CKAN-core.csproj
@@ -42,6 +42,7 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="Exporters\IExporter.cs" />
+    <Compile Include="Exporters\MarkdownExporter.cs" />
     <Compile Include="Exporters\PlainTextExporter.cs" />
     <Compile Include="Net\AutoUpdate.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
diff --git a/Core/Exporters/MarkdownExporter.cs b/Core/Exporters/MarkdownExporter.cs
new file mode 100644
index 0000000000..6edde4cfbd
--- /dev/null
+++ b/Core/Exporters/MarkdownExporter.cs
@@ -0,0 +1,18 @@
+using System.IO;
+using System.Linq;
+
+namespace CKAN.Exporters
+{
+    public sealed class MarkdownExporter : IExporter
+    {
+        public void Export(Registry registry, TextWriter writer)
+        {
+            foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
+            {
+                writer.WriteLine(@"- *{0}* `{1} {2}`", mod.Module.name, mod.identifier, mod.Module.version);
+            }
+
+            writer.Flush();
+        }
+    }
+}
diff --git a/Core/Exporters/PlainTextExporter.cs b/Core/Exporters/PlainTextExporter.cs
index 1e229f15df..481030def7 100644
--- a/Core/Exporters/PlainTextExporter.cs
+++ b/Core/Exporters/PlainTextExporter.cs
@@ -1,4 +1,5 @@
 using System.IO;
+using System.Linq;
 
 namespace CKAN.Exporters
 {
@@ -6,7 +7,7 @@ public sealed class PlainTextExporter : IExporter
     {
         public void Export(Registry registry, TextWriter writer)
         {
-            foreach (var mod in registry.InstalledModules)
+            foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
             {
                 writer.WriteLine(@"{0} ({1} {2})", mod.Module.name, mod.identifier, mod.Module.version);
             }
diff --git a/GUI/Main.cs b/GUI/Main.cs
index ac825baff1..d9596a3981 100644
--- a/GUI/Main.cs
+++ b/GUI/Main.cs
@@ -950,7 +950,9 @@ private void exportModListToolStripMenuItem_Click(object sender, EventArgs e)
             {
                 var exportOption = exportOptions[dlg.FilterIndex - 1]; // FilterIndex is 1-indexed
 
-                using (var writer = new StreamWriter(new FileStream(dlg.FileName, FileMode.OpenOrCreate)))
+                var fileMode = File.Exists(dlg.FileName) ? FileMode.Truncate : FileMode.CreateNew;
+
+                using (var writer = new StreamWriter(new FileStream(dlg.FileName, fileMode)))
                 {
                     var registry = RegistryManager.Instance(CurrentInstance).registry;
 
@@ -967,6 +969,7 @@ private void exportModListToolStripMenuItem_Click(object sender, EventArgs e)
                             new PlainTextExporter().Export(registry, writer);
                             break;
                         case ExportFileType.Markdown:
+                            new MarkdownExporter().Export(registry, writer);
                             break;
                         case ExportFileType.BbCode:
                             break;

From 19dfdcd517701427fee02ef41a87ccff0d24ec8b Mon Sep 17 00:00:00 2001
From: Dwayne Bent <dbb@dbb.io>
Date: Wed, 10 Jun 2015 20:42:51 -0400
Subject: [PATCH 04/13] Implement BbCodeExporter

---
 Core/CKAN-core.csproj            |  1 +
 Core/Exporters/BbCodeExporter.cs | 22 ++++++++++++++++++++++
 GUI/Main.cs                      |  1 +
 3 files changed, 24 insertions(+)
 create mode 100644 Core/Exporters/BbCodeExporter.cs

diff --git a/Core/CKAN-core.csproj b/Core/CKAN-core.csproj
index cb64bcc86e..d5f045a5ee 100644
--- a/Core/CKAN-core.csproj
+++ b/Core/CKAN-core.csproj
@@ -41,6 +41,7 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="Exporters\BbCodeExporter.cs" />
     <Compile Include="Exporters\IExporter.cs" />
     <Compile Include="Exporters\MarkdownExporter.cs" />
     <Compile Include="Exporters\PlainTextExporter.cs" />
diff --git a/Core/Exporters/BbCodeExporter.cs b/Core/Exporters/BbCodeExporter.cs
new file mode 100644
index 0000000000..f97edbade7
--- /dev/null
+++ b/Core/Exporters/BbCodeExporter.cs
@@ -0,0 +1,22 @@
+using System.IO;
+using System.Linq;
+
+namespace CKAN.Exporters
+{
+    public sealed class BbCodeExporter : IExporter
+    {
+        public void Export(Registry registry, TextWriter writer)
+        {
+            writer.WriteLine("[LIST]");
+
+            foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
+            {
+                writer.WriteLine(@"[*][I]{0}[/I] ({1} {2})", mod.Module.name, mod.identifier, mod.Module.version);
+            }
+
+            writer.WriteLine("[/LIST]");
+
+            writer.Flush();
+        }
+    }
+}
diff --git a/GUI/Main.cs b/GUI/Main.cs
index d9596a3981..0c3cf7e69d 100644
--- a/GUI/Main.cs
+++ b/GUI/Main.cs
@@ -972,6 +972,7 @@ private void exportModListToolStripMenuItem_Click(object sender, EventArgs e)
                             new MarkdownExporter().Export(registry, writer);
                             break;
                         case ExportFileType.BbCode:
+                            new BbCodeExporter().Export(registry, writer);
                             break;
                         case ExportFileType.Csv:
                             break;

From d09178487ad3834b05a57a4e97bb9b0eb6764693 Mon Sep 17 00:00:00 2001
From: Dwayne Bent <dbb@dbb.io>
Date: Wed, 10 Jun 2015 21:12:40 -0400
Subject: [PATCH 05/13] Implement DSVExporter

---
 Core/CKAN-core.csproj                         |   1 +
 .../DelimeterSeperatedValueExporter.cs        | 145 ++++++++++++++++++
 GUI/Main.cs                                   |   4 +
 3 files changed, 150 insertions(+)
 create mode 100644 Core/Exporters/DelimeterSeperatedValueExporter.cs

diff --git a/Core/CKAN-core.csproj b/Core/CKAN-core.csproj
index d5f045a5ee..47deb913f0 100644
--- a/Core/CKAN-core.csproj
+++ b/Core/CKAN-core.csproj
@@ -42,6 +42,7 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="Exporters\BbCodeExporter.cs" />
+    <Compile Include="Exporters\DelimeterSeperatedValueExporter.cs" />
     <Compile Include="Exporters\IExporter.cs" />
     <Compile Include="Exporters\MarkdownExporter.cs" />
     <Compile Include="Exporters\PlainTextExporter.cs" />
diff --git a/Core/Exporters/DelimeterSeperatedValueExporter.cs b/Core/Exporters/DelimeterSeperatedValueExporter.cs
new file mode 100644
index 0000000000..f1820ac9a8
--- /dev/null
+++ b/Core/Exporters/DelimeterSeperatedValueExporter.cs
@@ -0,0 +1,145 @@
+using System;
+using System.IO;
+using System.Linq;
+
+namespace CKAN.Exporters
+{
+    public sealed class DelimeterSeperatedValueExporter : IExporter
+    {
+        private const string WritePattern = "{1}{0}{2}{0}{3}{0}{4}{0}{5}" +
+                                            "{0}{6}{0}{7}{0}{8}{0}{9}{0}{10}" +
+                                            "{0}{11}{0}{12}{0}{13}{0}{14}{0}{15}" +
+                                            "{0}{16}{0}{17}{0}{18}";
+        private readonly string _delimter;
+
+        public DelimeterSeperatedValueExporter(Delimter delimter)
+        {
+            switch (delimter)
+            {
+                case Delimter.Comma:
+                    _delimter = ",";
+                    break;
+                case Delimter.Tab:
+                    _delimter = "\t";
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException();
+            }
+        }
+
+        public void Export(Registry registry, TextWriter writer)
+        {
+            writer.WriteLine(WritePattern,
+                _delimter,
+                "identifier",
+                "version",
+                "name",
+                "abstract",
+                "description",
+                "author",
+                "kind",
+                "download",
+                "download_size",
+                "ksp_version",
+                "ksp_version_min",
+                "ksp_version_max",
+                "license",
+                "release_status",
+                "repository",
+                "homepage",
+                "bugtracker",
+                "kerbalstuff"
+            );
+
+            foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
+            {
+                writer.WriteLine(WritePattern,
+                    _delimter,
+                    mod.Module.identifier,
+                    mod.Module.version,
+                    QuoteIfNecessary(mod.Module.name),
+                    QuoteIfNecessary(mod.Module.@abstract),
+                    QuoteIfNecessary(mod.Module.description),
+                    QuoteIfNecessary(string.Join(";", mod.Module.author)),
+                    QuoteIfNecessary(mod.Module.kind),
+                    WriteUri(mod.Module.download),
+                    mod.Module.download_size,
+                    mod.Module.ksp_version,
+                    mod.Module.ksp_version_min,
+                    mod.Module.ksp_version_max,
+                    mod.Module.license,
+                    mod.Module.release_status,
+                    WriteRepository(mod.Module.resources),
+                    WriteHomepage(mod.Module.resources),
+                    WriteBugtracker(mod.Module.resources),
+                    WriteKerbalStuff(mod.Module.resources)
+                );
+            }
+
+            writer.Flush();
+        }
+
+        private string WriteUri(Uri uri)
+        {
+            return uri != null ? QuoteIfNecessary(uri.ToString()) : string.Empty;
+        }
+
+        private string WriteRepository(ResourcesDescriptor resources)
+        {
+            if (resources != null && resources.repository != null)
+            {
+                return QuoteIfNecessary(resources.repository.ToString());
+            }
+
+            return string.Empty;
+        }
+
+        private string WriteHomepage(ResourcesDescriptor resources)
+        {
+            if (resources != null && resources.homepage != null)
+            {
+                return QuoteIfNecessary(resources.homepage.ToString());
+            }
+
+            return string.Empty;
+        }
+
+        private string WriteBugtracker(ResourcesDescriptor resources)
+        {
+            if (resources != null && resources.bugtracker != null)
+            {
+                return QuoteIfNecessary(resources.bugtracker.ToString());
+            }
+
+            return string.Empty;
+        }
+
+        private string WriteKerbalStuff(ResourcesDescriptor resources)
+        {
+            if (resources != null && resources.kerbalstuff != null)
+            {
+                return QuoteIfNecessary(resources.kerbalstuff.ToString());
+            }
+
+            return string.Empty;
+        }
+
+        private string QuoteIfNecessary(string value)
+        {
+            if (value != null && value.IndexOf(_delimter, StringComparison.Ordinal) >= 0)
+            {
+                return "\"" + value + "\"";
+            }
+            else
+            {
+                return value;
+            }
+        }
+
+        public enum Delimter
+        {
+            Comma,
+            Tab
+        }
+    }
+}
diff --git a/GUI/Main.cs b/GUI/Main.cs
index 0c3cf7e69d..0146abd2d7 100644
--- a/GUI/Main.cs
+++ b/GUI/Main.cs
@@ -975,8 +975,12 @@ private void exportModListToolStripMenuItem_Click(object sender, EventArgs e)
                             new BbCodeExporter().Export(registry, writer);
                             break;
                         case ExportFileType.Csv:
+                            new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Comma)
+                                .Export(registry, writer);
                             break;
                         case ExportFileType.Tsv:
+                            new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Tab)
+                                .Export(registry, writer);
                             break;
                         default:
                             throw new ArgumentOutOfRangeException();

From 7c308d39b0089d55db18782257294f92f840e2d4 Mon Sep 17 00:00:00 2001
From: Dwayne Bent <dbb@dbb.io>
Date: Wed, 10 Jun 2015 21:26:34 -0400
Subject: [PATCH 06/13] Implement in command line

---
 Cmdline/Action/List.cs | 149 +++++++++++++++++++++++++++++------------
 Cmdline/Options.cs     |   3 +
 2 files changed, 111 insertions(+), 41 deletions(-)

diff --git a/Cmdline/Action/List.cs b/Cmdline/Action/List.cs
index 4be34529e7..3ba6ad69dd 100644
--- a/Cmdline/Action/List.cs
+++ b/Cmdline/Action/List.cs
@@ -1,7 +1,10 @@
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
+using CKAN.Exporters;
+using CKAN.Types;
 using log4net;
 
 namespace CKAN.CmdLine
@@ -23,7 +26,20 @@ public int RunCommand(CKAN.KSP ksp, object raw_options)
 
             Registry registry = RegistryManager.Instance(ksp).registry;
 
-            if (!(options.porcelain))
+
+            ExportFileType? exportFileType = null;
+
+            if (!string.IsNullOrWhiteSpace(options.export))
+            {
+                exportFileType = GetExportFileType(options.export);
+
+                if (exportFileType == null)
+                {
+                    user.RaiseError("Unknown export format: {0}", options.export);
+                }
+            }
+
+            if (!(options.porcelain) && exportFileType == null)
             {
                 user.RaiseMessage("\nKSP found at {0}\n", ksp.GameDir());
                 user.RaiseMessage("KSP Version: {0}\n", ksp.Version());
@@ -31,66 +47,117 @@ public int RunCommand(CKAN.KSP ksp, object raw_options)
                 user.RaiseMessage("Installed Modules:\n");
             }
 
-            var installed = new SortedDictionary<string, Version>(registry.Installed());
-
-            foreach (KeyValuePair<string, Version> mod in installed)
+            if (exportFileType == null)
             {
-                Version current_version = mod.Value;
+                var installed = new SortedDictionary<string, Version>(registry.Installed());
 
-                string bullet = "*";
-
-                if (current_version is ProvidesVersion)
-                {
-                    // Skip virtuals for now.
-                    continue;
-                }
-                else if (current_version is DllVersion)
-                {
-                    // Autodetected dll
-                    bullet = "-";
-                }
-                else
+                foreach (KeyValuePair<string, Version> mod in installed)
                 {
-                    try
-                    {
-                        // Check if upgrades are available, and show appropriately.
-                        CkanModule latest = registry.LatestAvailable(mod.Key, ksp.Version());
+                    Version current_version = mod.Value;
 
-                        log.InfoFormat("Latest {0} is {1}", mod.Key, latest);
+                    string bullet = "*";
 
-                        if (latest == null)
-                        {
-                            // Not compatible!
-                            bullet = "X";
-                        }
-                        else if (latest.version.IsEqualTo(current_version))
+                    if (current_version is ProvidesVersion)
+                    {
+                        // Skip virtuals for now.
+                        continue;
+                    }
+                    else if (current_version is DllVersion)
+                    {
+                        // Autodetected dll
+                        bullet = "-";
+                    }
+                    else
+                    {
+                        try
                         {
-                            // Up to date
-                            bullet = "-";
+                            // Check if upgrades are available, and show appropriately.
+                            CkanModule latest = registry.LatestAvailable(mod.Key, ksp.Version());
+
+                            log.InfoFormat("Latest {0} is {1}", mod.Key, latest);
+
+                            if (latest == null)
+                            {
+                                // Not compatible!
+                                bullet = "X";
+                            }
+                            else if (latest.version.IsEqualTo(current_version))
+                            {
+                                // Up to date
+                                bullet = "-";
+                            }
+                            else if (latest.version.IsGreaterThan(mod.Value))
+                            {
+                                // Upgradable
+                                bullet = "^";
+                            }
                         }
-                        else if (latest.version.IsGreaterThan(mod.Value))
+                        catch (ModuleNotFoundKraken)
                         {
-                            // Upgradable
-                            bullet = "^";
+                            log.InfoFormat("{0} is installed, but no longer in the registry", mod.Key);
+                            bullet = "?";
                         }
                     }
-                    catch (ModuleNotFoundKraken)
-                    {
-                        log.InfoFormat("{0} is installed, but no longer in the registry", mod.Key);
-                        bullet = "?";
-                    }
+
+                    user.RaiseMessage("{0} {1} {2}", bullet, mod.Key, mod.Value);
                 }
+            }
+            else
+            {
+                var writer = Console.Out;
 
-                user.RaiseMessage("{0} {1} {2}", bullet, mod.Key, mod.Value);
+                switch (exportFileType.Value)
+                {
+                    case ExportFileType.PlainText:
+                        new PlainTextExporter().Export(registry, writer);
+                        break;
+                    case ExportFileType.Markdown:
+                        new MarkdownExporter().Export(registry, writer);
+                        break;
+                    case ExportFileType.BbCode:
+                        new BbCodeExporter().Export(registry, writer);
+                        break;
+                    case ExportFileType.Csv:
+                        new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Comma)
+                            .Export(registry, writer);
+                        break;
+                    case ExportFileType.Tsv:
+                        new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Tab)
+                            .Export(registry, writer);
+                        break;
+                    default:
+                        throw new ArgumentOutOfRangeException();
+                }
             }
 
-            if (!(options.porcelain))
+            if (!(options.porcelain) && exportFileType == null)
             {
                 user.RaiseMessage("\nLegend: -: Up to date. X: Incompatible. ^: Upgradable. ?: Unknown ");
             }
 
             return Exit.OK;
         }
+
+        private ExportFileType? GetExportFileType(string export)
+        {
+            export = export.ToLowerInvariant();
+
+            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;
+            }
+        }
     }
 }
 
diff --git a/Cmdline/Options.cs b/Cmdline/Options.cs
index bf59f046e8..baf229f4c7 100644
--- a/Cmdline/Options.cs
+++ b/Cmdline/Options.cs
@@ -176,6 +176,9 @@ internal class ListOptions : CommonOptions
     {
         [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

From 5b47647eacad635189b18b74059b8fe832ccc95d Mon Sep 17 00:00:00 2001
From: Dwayne Bent <dbb@dbb.io>
Date: Wed, 10 Jun 2015 23:46:19 -0400
Subject: [PATCH 07/13] Add some leading spaces to Markdown export

---
 Core/Exporters/MarkdownExporter.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Core/Exporters/MarkdownExporter.cs b/Core/Exporters/MarkdownExporter.cs
index 6edde4cfbd..a467708418 100644
--- a/Core/Exporters/MarkdownExporter.cs
+++ b/Core/Exporters/MarkdownExporter.cs
@@ -9,7 +9,7 @@ public void Export(Registry registry, TextWriter writer)
         {
             foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
             {
-                writer.WriteLine(@"- *{0}* `{1} {2}`", mod.Module.name, mod.identifier, mod.Module.version);
+                writer.WriteLine(@"  - *{0}* `{1} {2}`", mod.Module.name, mod.identifier, mod.Module.version);
             }
 
             writer.Flush();

From b25cd7c8103a52fc8e46bcc164204f4c6b66bd36 Mon Sep 17 00:00:00 2001
From: Dwayne Bent <dbb@dbb.io>
Date: Thu, 11 Jun 2015 10:05:33 -0400
Subject: [PATCH 08/13] Update doc comments

---
 Core/Exporters/IExporter.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/Core/Exporters/IExporter.cs b/Core/Exporters/IExporter.cs
index 7cb98b849d..2870c5e0c8 100644
--- a/Core/Exporters/IExporter.cs
+++ b/Core/Exporters/IExporter.cs
@@ -3,15 +3,15 @@
 namespace CKAN.Exporters
 {
     /// <summary>
-    /// Represents an object that can 
+    /// Represents an object that can export a list of mods in a machine/human readable text format.
     /// </summary>
     public interface IExporter
     {
         /// <summary>
-        /// 
+        /// Export the installed mods.
         /// </summary>
-        /// <param name="registry"></param>
-        /// <param name="writer"></param>
+        /// <param name="registry">The registry of mods to be exported.</param>
+        /// <param name="writer">The text writer to write the export to.</param>
         void Export(Registry registry, TextWriter writer);
     }
 }

From f018bfe5c49c8601d687485c6c20175878dcdfb7 Mon Sep 17 00:00:00 2001
From: Dwayne Bent <dbb@dbb.io>
Date: Fri, 12 Jun 2015 09:36:45 -0400
Subject: [PATCH 09/13] Use Stream instead of TextWriter

---
 Cmdline/Action/List.cs                        | 12 +--
 Core/Exporters/BbCodeExporter.cs              | 17 ++--
 .../DelimeterSeperatedValueExporter.cs        | 91 ++++++++++---------
 Core/Exporters/IExporter.cs                   |  8 +-
 Core/Exporters/MarkdownExporter.cs            | 11 ++-
 Core/Exporters/PlainTextExporter.cs           | 12 ++-
 GUI/Main.cs                                   | 12 +--
 7 files changed, 85 insertions(+), 78 deletions(-)

diff --git a/Cmdline/Action/List.cs b/Cmdline/Action/List.cs
index 3ba6ad69dd..a583eaafe5 100644
--- a/Cmdline/Action/List.cs
+++ b/Cmdline/Action/List.cs
@@ -104,26 +104,26 @@ public int RunCommand(CKAN.KSP ksp, object raw_options)
             }
             else
             {
-                var writer = Console.Out;
+                var stream = Console.OpenStandardOutput();
 
                 switch (exportFileType.Value)
                 {
                     case ExportFileType.PlainText:
-                        new PlainTextExporter().Export(registry, writer);
+                        new PlainTextExporter().Export(registry, stream);
                         break;
                     case ExportFileType.Markdown:
-                        new MarkdownExporter().Export(registry, writer);
+                        new MarkdownExporter().Export(registry, stream);
                         break;
                     case ExportFileType.BbCode:
-                        new BbCodeExporter().Export(registry, writer);
+                        new BbCodeExporter().Export(registry, stream);
                         break;
                     case ExportFileType.Csv:
                         new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Comma)
-                            .Export(registry, writer);
+                            .Export(registry, stream);
                         break;
                     case ExportFileType.Tsv:
                         new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Tab)
-                            .Export(registry, writer);
+                            .Export(registry, stream);
                         break;
                     default:
                         throw new ArgumentOutOfRangeException();
diff --git a/Core/Exporters/BbCodeExporter.cs b/Core/Exporters/BbCodeExporter.cs
index f97edbade7..1bd0471e13 100644
--- a/Core/Exporters/BbCodeExporter.cs
+++ b/Core/Exporters/BbCodeExporter.cs
@@ -5,18 +5,19 @@ namespace CKAN.Exporters
 {
     public sealed class BbCodeExporter : IExporter
     {
-        public void Export(Registry registry, TextWriter writer)
+        public void Export(Registry registry, Stream stream)
         {
-            writer.WriteLine("[LIST]");
-
-            foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
+            using (var writer = new StreamWriter(stream))
             {
-                writer.WriteLine(@"[*][I]{0}[/I] ({1} {2})", mod.Module.name, mod.identifier, mod.Module.version);
-            }
+                writer.WriteLine("[LIST]");
 
-            writer.WriteLine("[/LIST]");
+                foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
+                {
+                    writer.WriteLine(@"[*][I]{0}[/I] ({1} {2})", mod.Module.name, mod.identifier, mod.Module.version);
+                }
 
-            writer.Flush();
+                writer.WriteLine("[/LIST]");
+            }
         }
     }
 }
diff --git a/Core/Exporters/DelimeterSeperatedValueExporter.cs b/Core/Exporters/DelimeterSeperatedValueExporter.cs
index f1820ac9a8..e9abd6b04b 100644
--- a/Core/Exporters/DelimeterSeperatedValueExporter.cs
+++ b/Core/Exporters/DelimeterSeperatedValueExporter.cs
@@ -27,56 +27,57 @@ public DelimeterSeperatedValueExporter(Delimter delimter)
             }
         }
 
-        public void Export(Registry registry, TextWriter writer)
+        public void Export(Registry registry, Stream stream)
         {
-            writer.WriteLine(WritePattern,
-                _delimter,
-                "identifier",
-                "version",
-                "name",
-                "abstract",
-                "description",
-                "author",
-                "kind",
-                "download",
-                "download_size",
-                "ksp_version",
-                "ksp_version_min",
-                "ksp_version_max",
-                "license",
-                "release_status",
-                "repository",
-                "homepage",
-                "bugtracker",
-                "kerbalstuff"
-            );
-
-            foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
+            using (var writer = new StreamWriter(stream))
             {
                 writer.WriteLine(WritePattern,
                     _delimter,
-                    mod.Module.identifier,
-                    mod.Module.version,
-                    QuoteIfNecessary(mod.Module.name),
-                    QuoteIfNecessary(mod.Module.@abstract),
-                    QuoteIfNecessary(mod.Module.description),
-                    QuoteIfNecessary(string.Join(";", mod.Module.author)),
-                    QuoteIfNecessary(mod.Module.kind),
-                    WriteUri(mod.Module.download),
-                    mod.Module.download_size,
-                    mod.Module.ksp_version,
-                    mod.Module.ksp_version_min,
-                    mod.Module.ksp_version_max,
-                    mod.Module.license,
-                    mod.Module.release_status,
-                    WriteRepository(mod.Module.resources),
-                    WriteHomepage(mod.Module.resources),
-                    WriteBugtracker(mod.Module.resources),
-                    WriteKerbalStuff(mod.Module.resources)
+                    "identifier",
+                    "version",
+                    "name",
+                    "abstract",
+                    "description",
+                    "author",
+                    "kind",
+                    "download",
+                    "download_size",
+                    "ksp_version",
+                    "ksp_version_min",
+                    "ksp_version_max",
+                    "license",
+                    "release_status",
+                    "repository",
+                    "homepage",
+                    "bugtracker",
+                    "kerbalstuff"
                 );
-            }
 
-            writer.Flush();
+                foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
+                {
+                    writer.WriteLine(WritePattern,
+                        _delimter,
+                        mod.Module.identifier,
+                        mod.Module.version,
+                        QuoteIfNecessary(mod.Module.name),
+                        QuoteIfNecessary(mod.Module.@abstract),
+                        QuoteIfNecessary(mod.Module.description),
+                        QuoteIfNecessary(string.Join(";", mod.Module.author)),
+                        QuoteIfNecessary(mod.Module.kind),
+                        WriteUri(mod.Module.download),
+                        mod.Module.download_size,
+                        mod.Module.ksp_version,
+                        mod.Module.ksp_version_min,
+                        mod.Module.ksp_version_max,
+                        mod.Module.license,
+                        mod.Module.release_status,
+                        WriteRepository(mod.Module.resources),
+                        WriteHomepage(mod.Module.resources),
+                        WriteBugtracker(mod.Module.resources),
+                        WriteKerbalStuff(mod.Module.resources)
+                    );
+                }
+            }
         }
 
         private string WriteUri(Uri uri)
@@ -141,5 +142,7 @@ public enum Delimter
             Comma,
             Tab
         }
+
+
     }
 }
diff --git a/Core/Exporters/IExporter.cs b/Core/Exporters/IExporter.cs
index 2870c5e0c8..0eca948bc1 100644
--- a/Core/Exporters/IExporter.cs
+++ b/Core/Exporters/IExporter.cs
@@ -3,15 +3,15 @@
 namespace CKAN.Exporters
 {
     /// <summary>
-    /// Represents an object that can export a list of mods in a machine/human readable text format.
+    /// Represents an object that can export a list of mods in a machine or human readable format.
     /// </summary>
     public interface IExporter
     {
         /// <summary>
-        /// Export the installed mods.
+        /// Export installed mods.
         /// </summary>
         /// <param name="registry">The registry of mods to be exported.</param>
-        /// <param name="writer">The text writer to write the export to.</param>
-        void Export(Registry registry, TextWriter writer);
+        /// <param name="stream">The output stream to be written to.</param>
+        void Export(Registry registry, Stream stream);
     }
 }
diff --git a/Core/Exporters/MarkdownExporter.cs b/Core/Exporters/MarkdownExporter.cs
index a467708418..84dcbf4ec2 100644
--- a/Core/Exporters/MarkdownExporter.cs
+++ b/Core/Exporters/MarkdownExporter.cs
@@ -5,14 +5,15 @@ namespace CKAN.Exporters
 {
     public sealed class MarkdownExporter : IExporter
     {
-        public void Export(Registry registry, TextWriter writer)
+        public void Export(Registry registry, Stream stream)
         {
-            foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
+            using (var writer = new StreamWriter(stream))
             {
-                writer.WriteLine(@"  - *{0}* `{1} {2}`", mod.Module.name, mod.identifier, mod.Module.version);
+                foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
+                {
+                    writer.WriteLine(@"  - *{0}* `{1} {2}`", mod.Module.name, mod.identifier, mod.Module.version);
+                }
             }
-
-            writer.Flush();
         }
     }
 }
diff --git a/Core/Exporters/PlainTextExporter.cs b/Core/Exporters/PlainTextExporter.cs
index 481030def7..ba6b9ed2cc 100644
--- a/Core/Exporters/PlainTextExporter.cs
+++ b/Core/Exporters/PlainTextExporter.cs
@@ -5,14 +5,16 @@ namespace CKAN.Exporters
 {
     public sealed class PlainTextExporter : IExporter
     {
-        public void Export(Registry registry, TextWriter writer)
+
+        public void Export(Registry registry, Stream stream)
         {
-            foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
+            using (var writer = new StreamWriter(stream))
             {
-                writer.WriteLine(@"{0} ({1} {2})", mod.Module.name, mod.identifier, mod.Module.version);
+                foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
+                {
+                    writer.WriteLine(@"{0} ({1} {2})", mod.Module.name, mod.identifier, mod.Module.version);
+                }
             }
-
-            writer.Flush();
         }
     }
 }
diff --git a/GUI/Main.cs b/GUI/Main.cs
index 0146abd2d7..a945989a5a 100644
--- a/GUI/Main.cs
+++ b/GUI/Main.cs
@@ -952,7 +952,7 @@ private void exportModListToolStripMenuItem_Click(object sender, EventArgs e)
 
                 var fileMode = File.Exists(dlg.FileName) ? FileMode.Truncate : FileMode.CreateNew;
 
-                using (var writer = new StreamWriter(new FileStream(dlg.FileName, fileMode)))
+                using (var stream = new FileStream(dlg.FileName, fileMode))
                 {
                     var registry = RegistryManager.Instance(CurrentInstance).registry;
 
@@ -966,21 +966,21 @@ private void exportModListToolStripMenuItem_Click(object sender, EventArgs e)
                             File.Copy(Path.Combine(CurrentInstance.CkanDir(), "installed-default.ckan"), dlg.FileName);
                             break;
                         case ExportFileType.PlainText:
-                            new PlainTextExporter().Export(registry, writer);
+                            new PlainTextExporter().Export(registry, stream);
                             break;
                         case ExportFileType.Markdown:
-                            new MarkdownExporter().Export(registry, writer);
+                            new MarkdownExporter().Export(registry, stream);
                             break;
                         case ExportFileType.BbCode:
-                            new BbCodeExporter().Export(registry, writer);
+                            new BbCodeExporter().Export(registry, stream);
                             break;
                         case ExportFileType.Csv:
                             new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Comma)
-                                .Export(registry, writer);
+                                .Export(registry, stream);
                             break;
                         case ExportFileType.Tsv:
                             new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Tab)
-                                .Export(registry, writer);
+                                .Export(registry, stream);
                             break;
                         default:
                             throw new ArgumentOutOfRangeException();

From cf138c74cb67a777690cd590cc321e6ad09f1743 Mon Sep 17 00:00:00 2001
From: Dwayne Bent <dbb@dbb.io>
Date: Fri, 12 Jun 2015 09:37:25 -0400
Subject: [PATCH 10/13] Use bold instead of italics

---
 Core/Exporters/BbCodeExporter.cs   | 2 +-
 Core/Exporters/MarkdownExporter.cs | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Core/Exporters/BbCodeExporter.cs b/Core/Exporters/BbCodeExporter.cs
index 1bd0471e13..6060421872 100644
--- a/Core/Exporters/BbCodeExporter.cs
+++ b/Core/Exporters/BbCodeExporter.cs
@@ -13,7 +13,7 @@ public void Export(Registry registry, Stream stream)
 
                 foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
                 {
-                    writer.WriteLine(@"[*][I]{0}[/I] ({1} {2})", mod.Module.name, mod.identifier, mod.Module.version);
+                    writer.WriteLine(@"[*][B]{0}[/B] ({1} {2})", mod.Module.name, mod.identifier, mod.Module.version);
                 }
 
                 writer.WriteLine("[/LIST]");
diff --git a/Core/Exporters/MarkdownExporter.cs b/Core/Exporters/MarkdownExporter.cs
index 84dcbf4ec2..20a3197bf9 100644
--- a/Core/Exporters/MarkdownExporter.cs
+++ b/Core/Exporters/MarkdownExporter.cs
@@ -11,7 +11,7 @@ public void Export(Registry registry, Stream stream)
             {
                 foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
                 {
-                    writer.WriteLine(@"  - *{0}* `{1} {2}`", mod.Module.name, mod.identifier, mod.Module.version);
+                    writer.WriteLine(@"  - **{0}** `{1} {2}`", mod.Module.name, mod.identifier, mod.Module.version);
                 }
             }
         }

From 7305ce014726b697e40940221eed391d9b9658af Mon Sep 17 00:00:00 2001
From: Dwayne Bent <dbb@dbb.io>
Date: Fri, 12 Jun 2015 09:46:39 -0400
Subject: [PATCH 11/13] Use a meta-exporter to consolidate code

---
 Cmdline/Action/List.cs     | 25 ++---------------
 Core/CKAN-core.csproj      |  1 +
 Core/Exporters/Exporter.cs | 40 +++++++++++++++++++++++++++
 GUI/Main.cs                | 55 +++++++++++++-------------------------
 4 files changed, 62 insertions(+), 59 deletions(-)
 create mode 100644 Core/Exporters/Exporter.cs

diff --git a/Cmdline/Action/List.cs b/Cmdline/Action/List.cs
index a583eaafe5..9f58c53b34 100644
--- a/Cmdline/Action/List.cs
+++ b/Cmdline/Action/List.cs
@@ -105,29 +105,8 @@ public int RunCommand(CKAN.KSP ksp, object raw_options)
             else
             {
                 var stream = Console.OpenStandardOutput();
-
-                switch (exportFileType.Value)
-                {
-                    case ExportFileType.PlainText:
-                        new PlainTextExporter().Export(registry, stream);
-                        break;
-                    case ExportFileType.Markdown:
-                        new MarkdownExporter().Export(registry, stream);
-                        break;
-                    case ExportFileType.BbCode:
-                        new BbCodeExporter().Export(registry, stream);
-                        break;
-                    case ExportFileType.Csv:
-                        new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Comma)
-                            .Export(registry, stream);
-                        break;
-                    case ExportFileType.Tsv:
-                        new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Tab)
-                            .Export(registry, stream);
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException();
-                }
+                new Exporter(exportFileType.Value).Export(registry, stream);
+                stream.Flush();
             }
 
             if (!(options.porcelain) && exportFileType == null)
diff --git a/Core/CKAN-core.csproj b/Core/CKAN-core.csproj
index 47deb913f0..fcd0e24c30 100644
--- a/Core/CKAN-core.csproj
+++ b/Core/CKAN-core.csproj
@@ -43,6 +43,7 @@
   <ItemGroup>
     <Compile Include="Exporters\BbCodeExporter.cs" />
     <Compile Include="Exporters\DelimeterSeperatedValueExporter.cs" />
+    <Compile Include="Exporters\Exporter.cs" />
     <Compile Include="Exporters\IExporter.cs" />
     <Compile Include="Exporters\MarkdownExporter.cs" />
     <Compile Include="Exporters\PlainTextExporter.cs" />
diff --git a/Core/Exporters/Exporter.cs b/Core/Exporters/Exporter.cs
new file mode 100644
index 0000000000..fa9db557cd
--- /dev/null
+++ b/Core/Exporters/Exporter.cs
@@ -0,0 +1,40 @@
+using System;
+using System.IO;
+using CKAN.Types;
+
+namespace CKAN.Exporters
+{
+    public sealed class Exporter : IExporter
+    {
+        private readonly IExporter _exporter;
+
+        public Exporter(ExportFileType exportFileType)
+        {
+            switch (exportFileType)
+            {
+                case ExportFileType.PlainText:
+                    _exporter = new PlainTextExporter();
+                    break;
+                case ExportFileType.Markdown:
+                    _exporter = new MarkdownExporter();
+                    break;
+                case ExportFileType.BbCode:
+                    _exporter = new BbCodeExporter();
+                    break;
+                case ExportFileType.Csv:
+                    _exporter = new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Comma);
+                    break;
+                case ExportFileType.Tsv:
+                    _exporter = new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Tab);
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException();
+            }
+        }
+
+        public void Export(Registry registry, Stream stream)
+        {
+            _exporter.Export(registry, stream);
+        }
+    }
+}
diff --git a/GUI/Main.cs b/GUI/Main.cs
index a945989a5a..f8b3521133 100644
--- a/GUI/Main.cs
+++ b/GUI/Main.cs
@@ -930,12 +930,12 @@ private void exportModListToolStripMenuItem_Click(object sender, EventArgs e)
         {
             var exportOptions = new List<ExportOption>
             {
-                new ExportOption(Types.ExportFileType.Ckan, "CKAN metadata (*.ckan)", "ckan"),
-                new ExportOption(Types.ExportFileType.PlainText, "Plain text (*.txt)", "txt"),
-                new ExportOption(Types.ExportFileType.Markdown, "Markdown (*.md)", "md"),
-                new ExportOption(Types.ExportFileType.BbCode, "BBCode (*.txt)", "txt"),
-                new ExportOption(Types.ExportFileType.Csv, "Comma-seperated values (*.csv)", "csv"),
-                new ExportOption(Types.ExportFileType.Tsv, "Tab-seperated values (*.tsv)", "tsv")
+                new ExportOption(ExportFileType.Ckan, "CKAN metadata (*.ckan)", "ckan"),
+                new ExportOption(ExportFileType.PlainText, "Plain text (*.txt)", "txt"),
+                new ExportOption(ExportFileType.Markdown, "Markdown (*.md)", "md"),
+                new ExportOption(ExportFileType.BbCode, "BBCode (*.txt)", "txt"),
+                new ExportOption(ExportFileType.Csv, "Comma-seperated values (*.csv)", "csv"),
+                new ExportOption(ExportFileType.Tsv, "Tab-seperated values (*.tsv)", "tsv")
             };
 
             var filter = string.Join("|", exportOptions.Select(i => i.ToString()).ToArray());
@@ -950,40 +950,23 @@ private void exportModListToolStripMenuItem_Click(object sender, EventArgs e)
             {
                 var exportOption = exportOptions[dlg.FilterIndex - 1]; // FilterIndex is 1-indexed
 
-                var fileMode = File.Exists(dlg.FileName) ? FileMode.Truncate : FileMode.CreateNew;
+                if (exportOption.ExportFileType == ExportFileType.Ckan)
+                {
+                    // Save, just to be certain that the installed-*.ckan metapackage is generated
+                    RegistryManager.Instance(CurrentInstance).Save();
 
-                using (var stream = new FileStream(dlg.FileName, fileMode))
+                    // TODO: The core might eventually save as something other than 'installed-default.ckan'
+                    File.Copy(Path.Combine(CurrentInstance.CkanDir(), "installed-default.ckan"), dlg.FileName);
+                }
+                else
                 {
-                    var registry = RegistryManager.Instance(CurrentInstance).registry;
+                    var fileMode = File.Exists(dlg.FileName) ? FileMode.Truncate : FileMode.CreateNew;
 
-                    switch (exportOption.ExportFileType)
+                    using (var stream = new FileStream(dlg.FileName, fileMode))
                     {
-                        case ExportFileType.Ckan:
-                            // Save, just to be certain that the installed-*.ckan metapackage is generated
-                            RegistryManager.Instance(CurrentInstance).Save();
-
-                            // TODO: The core might eventually save as something other than 'installed-default.ckan'
-                            File.Copy(Path.Combine(CurrentInstance.CkanDir(), "installed-default.ckan"), dlg.FileName);
-                            break;
-                        case ExportFileType.PlainText:
-                            new PlainTextExporter().Export(registry, stream);
-                            break;
-                        case ExportFileType.Markdown:
-                            new MarkdownExporter().Export(registry, stream);
-                            break;
-                        case ExportFileType.BbCode:
-                            new BbCodeExporter().Export(registry, stream);
-                            break;
-                        case ExportFileType.Csv:
-                            new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Comma)
-                                .Export(registry, stream);
-                            break;
-                        case ExportFileType.Tsv:
-                            new DelimeterSeperatedValueExporter(DelimeterSeperatedValueExporter.Delimter.Tab)
-                                .Export(registry, stream);
-                            break;
-                        default:
-                            throw new ArgumentOutOfRangeException();
+                        var registry = RegistryManager.Instance(CurrentInstance).registry;
+
+                        new Exporter(exportOption.ExportFileType).Export(registry, stream);
                     }
                 }
             }

From 0a0f08cca06fbbdb30e9cd663eeb5bed28550fbd Mon Sep 17 00:00:00 2001
From: Dwayne Bent <dbb@dbb.io>
Date: Fri, 12 Jun 2015 10:39:28 -0400
Subject: [PATCH 12/13] Add doc comment to Exporter

---
 Cmdline/Action/List.cs     | 2 +-
 Core/Exporters/Exporter.cs | 8 ++++++++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/Cmdline/Action/List.cs b/Cmdline/Action/List.cs
index 9f58c53b34..d0202dae1b 100644
--- a/Cmdline/Action/List.cs
+++ b/Cmdline/Action/List.cs
@@ -117,7 +117,7 @@ public int RunCommand(CKAN.KSP ksp, object raw_options)
             return Exit.OK;
         }
 
-        private ExportFileType? GetExportFileType(string export)
+        private static ExportFileType? GetExportFileType(string export)
         {
             export = export.ToLowerInvariant();
 
diff --git a/Core/Exporters/Exporter.cs b/Core/Exporters/Exporter.cs
index fa9db557cd..5ccd3ee593 100644
--- a/Core/Exporters/Exporter.cs
+++ b/Core/Exporters/Exporter.cs
@@ -4,6 +4,14 @@
 
 namespace CKAN.Exporters
 {
+    /// <summary>
+    /// An implementation of <see cref="IExporter"/> that knows how to export to the different types of
+    /// <see cref="ExportFileType"/>.
+    /// </summary>
+    /// <remarks>
+    /// <see cref="ExportFileType.Ckan"/> is currently unhandled as that requires use of the
+    /// <see cref="RegistryManager"/> rather than just the <see cref="Registry"/>.
+    /// </remarks>
     public sealed class Exporter : IExporter
     {
         private readonly IExporter _exporter;

From c3ae159db63f2d55c8db3e612f9ad3fb707c067d Mon Sep 17 00:00:00 2001
From: Dwayne Bent <dbb@dbb.io>
Date: Sat, 13 Jun 2015 08:30:27 -0400
Subject: [PATCH 13/13] Markdown spec has no indentation for lists

---
 Core/Exporters/MarkdownExporter.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Core/Exporters/MarkdownExporter.cs b/Core/Exporters/MarkdownExporter.cs
index 20a3197bf9..87f3434106 100644
--- a/Core/Exporters/MarkdownExporter.cs
+++ b/Core/Exporters/MarkdownExporter.cs
@@ -11,7 +11,7 @@ public void Export(Registry registry, Stream stream)
             {
                 foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name))
                 {
-                    writer.WriteLine(@"  - **{0}** `{1} {2}`", mod.Module.name, mod.identifier, mod.Module.version);
+                    writer.WriteLine(@"- **{0}** `{1} {2}`", mod.Module.name, mod.identifier, mod.Module.version);
                 }
             }
         }