diff --git a/CliFx.Demo/CliFx.Demo.csproj b/CliFx.Demo/CliFx.Demo.csproj
new file mode 100644
index 0000000..f6af40b
--- /dev/null
+++ b/CliFx.Demo/CliFx.Demo.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ netcoreapp2.2
+ latest
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CliFx.Demo/Commands/BookAddCommand.cs b/CliFx.Demo/Commands/BookAddCommand.cs
new file mode 100644
index 0000000..110ca3d
--- /dev/null
+++ b/CliFx.Demo/Commands/BookAddCommand.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Threading.Tasks;
+using CliFx.Attributes;
+using CliFx.Demo.Internal;
+using CliFx.Demo.Models;
+using CliFx.Demo.Services;
+using CliFx.Exceptions;
+using CliFx.Services;
+
+namespace CliFx.Demo.Commands
+{
+ [Command("book add", Description = "Add a book to the library.")]
+ public partial class BookAddCommand : ICommand
+ {
+ private readonly LibraryService _libraryService;
+
+ [CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
+ public string Title { get; set; }
+
+ [CommandOption("author", 'a', IsRequired = true, Description = "Book author.")]
+ public string Author { get; set; }
+
+ [CommandOption("published", 'p', Description = "Book publish date.")]
+ public DateTimeOffset Published { get; set; }
+
+ [CommandOption("isbn", 'n', Description = "Book ISBN.")]
+ public Isbn Isbn { get; set; }
+
+ public BookAddCommand(LibraryService libraryService)
+ {
+ _libraryService = libraryService;
+ }
+
+ public Task ExecuteAsync(IConsole console)
+ {
+ // To make the demo simpler, we will just generate random publish date and ISBN if they were not set
+ if (Published == default)
+ Published = CreateRandomDate();
+ if (Isbn == default)
+ Isbn = CreateRandomIsbn();
+
+ if (_libraryService.GetBook(Title) != null)
+ throw new CommandErrorException(1, "Book already exists.");
+
+ var book = new Book(Title, Author, Published, Isbn);
+ _libraryService.AddBook(book);
+
+ console.Output.WriteLine("Book added.");
+ console.RenderBook(book);
+
+ return Task.CompletedTask;
+ }
+ }
+
+ public partial class BookAddCommand
+ {
+ private static readonly Random Random = new Random();
+
+ private static DateTimeOffset CreateRandomDate() => new DateTimeOffset(
+ Random.Next(1800, 2020),
+ Random.Next(1, 12),
+ Random.Next(1, 28),
+ Random.Next(1, 23),
+ Random.Next(1, 59),
+ Random.Next(1, 59),
+ TimeSpan.Zero);
+
+ public static Isbn CreateRandomIsbn() => new Isbn(
+ Random.Next(0, 999),
+ Random.Next(0, 99),
+ Random.Next(0, 99999),
+ Random.Next(0, 99),
+ Random.Next(0, 9));
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Demo/Commands/BookCommand.cs b/CliFx.Demo/Commands/BookCommand.cs
new file mode 100644
index 0000000..8739d8f
--- /dev/null
+++ b/CliFx.Demo/Commands/BookCommand.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Threading.Tasks;
+using CliFx.Attributes;
+using CliFx.Demo.Internal;
+using CliFx.Demo.Services;
+using CliFx.Exceptions;
+using CliFx.Services;
+
+namespace CliFx.Demo.Commands
+{
+ [Command("book", Description = "View, list, add or remove books.")]
+ public class BookCommand : ICommand
+ {
+ private readonly LibraryService _libraryService;
+
+ [CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
+ public string Title { get; set; }
+
+ public BookCommand(LibraryService libraryService)
+ {
+ _libraryService = libraryService;
+ }
+
+ public Task ExecuteAsync(IConsole console)
+ {
+ var book = _libraryService.GetBook(Title);
+
+ if (book == null)
+ throw new CommandErrorException(1, "Book not found.");
+
+ console.RenderBook(book);
+
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Demo/Commands/BookListCommand.cs b/CliFx.Demo/Commands/BookListCommand.cs
new file mode 100644
index 0000000..68f3aa4
--- /dev/null
+++ b/CliFx.Demo/Commands/BookListCommand.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Threading.Tasks;
+using CliFx.Attributes;
+using CliFx.Demo.Internal;
+using CliFx.Demo.Services;
+using CliFx.Services;
+
+namespace CliFx.Demo.Commands
+{
+ [Command("book list", Description = "List all books in the library.")]
+ public class BookListCommand : ICommand
+ {
+ private readonly LibraryService _libraryService;
+
+ public BookListCommand(LibraryService libraryService)
+ {
+ _libraryService = libraryService;
+ }
+
+ public Task ExecuteAsync(IConsole console)
+ {
+ var library = _libraryService.GetLibrary();
+
+ var isFirst = true;
+ foreach (var book in library.Books)
+ {
+ // Margin
+ if (!isFirst)
+ console.Output.WriteLine();
+ isFirst = false;
+
+ // Render book
+ console.RenderBook(book);
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Demo/Commands/BookRemoveCommand.cs b/CliFx.Demo/Commands/BookRemoveCommand.cs
new file mode 100644
index 0000000..5fa7ca5
--- /dev/null
+++ b/CliFx.Demo/Commands/BookRemoveCommand.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Threading.Tasks;
+using CliFx.Attributes;
+using CliFx.Demo.Services;
+using CliFx.Exceptions;
+using CliFx.Services;
+
+namespace CliFx.Demo.Commands
+{
+ [Command("book remove", Description = "Remove a book from the library.")]
+ public class BookRemoveCommand : ICommand
+ {
+ private readonly LibraryService _libraryService;
+
+ [CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
+ public string Title { get; set; }
+
+ public BookRemoveCommand(LibraryService libraryService)
+ {
+ _libraryService = libraryService;
+ }
+
+ public Task ExecuteAsync(IConsole console)
+ {
+ var book = _libraryService.GetBook(Title);
+
+ if (book == null)
+ throw new CommandErrorException(1, "Book not found.");
+
+ _libraryService.RemoveBook(book);
+
+ console.Output.WriteLine($"Book {Title} removed.");
+
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Demo/Internal/ConsoleExtensions.cs b/CliFx.Demo/Internal/ConsoleExtensions.cs
new file mode 100644
index 0000000..ff71cae
--- /dev/null
+++ b/CliFx.Demo/Internal/ConsoleExtensions.cs
@@ -0,0 +1,30 @@
+using System;
+using CliFx.Demo.Models;
+using CliFx.Services;
+
+namespace CliFx.Demo.Internal
+{
+ internal static class ConsoleExtensions
+ {
+ public static void RenderBook(this IConsole console, Book book)
+ {
+ // Title
+ console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Title));
+
+ // Author
+ console.Output.Write(" ");
+ console.Output.Write("Author: ");
+ console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Author));
+
+ // Published
+ console.Output.Write(" ");
+ console.Output.Write("Published: ");
+ console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine($"{book.Published:d}"));
+
+ // ISBN
+ console.Output.Write(" ");
+ console.Output.Write("ISBN: ");
+ console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Isbn));
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Demo/Models/Book.cs b/CliFx.Demo/Models/Book.cs
new file mode 100644
index 0000000..43cbcd1
--- /dev/null
+++ b/CliFx.Demo/Models/Book.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace CliFx.Demo.Models
+{
+ public class Book
+ {
+ public string Title { get; }
+
+ public string Author { get; }
+
+ public DateTimeOffset Published { get; }
+
+ public Isbn Isbn { get; }
+
+ public Book(string title, string author, DateTimeOffset published, Isbn isbn)
+ {
+ Title = title;
+ Author = author;
+ Published = published;
+ Isbn = isbn;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Demo/Models/Extensions.cs b/CliFx.Demo/Models/Extensions.cs
new file mode 100644
index 0000000..5b97758
--- /dev/null
+++ b/CliFx.Demo/Models/Extensions.cs
@@ -0,0 +1,22 @@
+using System.Linq;
+
+namespace CliFx.Demo.Models
+{
+ public static class Extensions
+ {
+ public static Library WithBook(this Library library, Book book)
+ {
+ var books = library.Books.ToList();
+ books.Add(book);
+
+ return new Library(books);
+ }
+
+ public static Library WithoutBook(this Library library, Book book)
+ {
+ var books = library.Books.Where(b => b != book).ToArray();
+
+ return new Library(books);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Demo/Models/Isbn.cs b/CliFx.Demo/Models/Isbn.cs
new file mode 100644
index 0000000..5d2532b
--- /dev/null
+++ b/CliFx.Demo/Models/Isbn.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Globalization;
+
+namespace CliFx.Demo.Models
+{
+ public partial class Isbn
+ {
+ public int EanPrefix { get; }
+
+ public int RegistrationGroup { get; }
+
+ public int Registrant { get; }
+
+ public int Publication { get; }
+
+ public int CheckDigit { get; }
+
+ public Isbn(int eanPrefix, int registrationGroup, int registrant, int publication, int checkDigit)
+ {
+ EanPrefix = eanPrefix;
+ RegistrationGroup = registrationGroup;
+ Registrant = registrant;
+ Publication = publication;
+ CheckDigit = checkDigit;
+ }
+
+ public override string ToString() => $"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}";
+ }
+
+ public partial class Isbn
+ {
+ public static Isbn Parse(string value)
+ {
+ var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries);
+
+ return new Isbn(
+ int.Parse(components[0], CultureInfo.InvariantCulture),
+ int.Parse(components[1], CultureInfo.InvariantCulture),
+ int.Parse(components[2], CultureInfo.InvariantCulture),
+ int.Parse(components[3], CultureInfo.InvariantCulture),
+ int.Parse(components[4], CultureInfo.InvariantCulture));
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Demo/Models/Library.cs b/CliFx.Demo/Models/Library.cs
new file mode 100644
index 0000000..1e23f99
--- /dev/null
+++ b/CliFx.Demo/Models/Library.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+
+namespace CliFx.Demo.Models
+{
+ public partial class Library
+ {
+ public IReadOnlyList Books { get; }
+
+ public Library(IReadOnlyList books)
+ {
+ Books = books;
+ }
+ }
+
+ public partial class Library
+ {
+ public static Library Empty { get; } = new Library(Array.Empty());
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Demo/Program.cs b/CliFx.Demo/Program.cs
new file mode 100644
index 0000000..e22e1e1
--- /dev/null
+++ b/CliFx.Demo/Program.cs
@@ -0,0 +1,33 @@
+using System.Threading.Tasks;
+using CliFx.Demo.Commands;
+using CliFx.Demo.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CliFx.Demo
+{
+ public static class Program
+ {
+ public static Task Main(string[] args)
+ {
+ // We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
+ var services = new ServiceCollection();
+
+ // Register services
+ services.AddSingleton();
+
+ // Register commands
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+
+ var serviceProvider = new DefaultServiceProviderFactory().CreateServiceProvider(services);
+
+ return new CliApplicationBuilder()
+ .WithCommandsFromThisAssembly()
+ .UseCommandFactory(type => (ICommand) serviceProvider.GetRequiredService(type))
+ .Build()
+ .RunAsync(args);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Demo/Readme.md b/CliFx.Demo/Readme.md
new file mode 100644
index 0000000..9ef1228
--- /dev/null
+++ b/CliFx.Demo/Readme.md
@@ -0,0 +1,7 @@
+# CliFx Demo Project
+
+Sample command line interface for managing a library of books.
+
+This demo project shows basic CliFx functionality such as command routing, option parsing, autogenerated help text, and some other things.
+
+You can get a list of available commands by running `CliFx.Demo --help`.
\ No newline at end of file
diff --git a/CliFx.Demo/Services/LibraryService.cs b/CliFx.Demo/Services/LibraryService.cs
new file mode 100644
index 0000000..3210093
--- /dev/null
+++ b/CliFx.Demo/Services/LibraryService.cs
@@ -0,0 +1,43 @@
+using System;
+using System.IO;
+using System.Linq;
+using CliFx.Demo.Models;
+using Newtonsoft.Json;
+
+namespace CliFx.Demo.Services
+{
+ public class LibraryService
+ {
+ private string StorageFilePath => Path.Combine(Directory.GetCurrentDirectory(), "Data.json");
+
+ private void StoreLibrary(Library library)
+ {
+ var data = JsonConvert.SerializeObject(library);
+ File.WriteAllText(StorageFilePath, data);
+ }
+
+ public Library GetLibrary()
+ {
+ if (!File.Exists(StorageFilePath))
+ return Library.Empty;
+
+ var data = File.ReadAllText(StorageFilePath);
+
+ return JsonConvert.DeserializeObject(data);
+ }
+
+ public Book GetBook(string title) => GetLibrary().Books.FirstOrDefault(b => b.Title == title);
+
+ public void AddBook(Book book)
+ {
+ var updatedLibrary = GetLibrary().WithBook(book);
+ StoreLibrary(updatedLibrary);
+ }
+
+ public void RemoveBook(Book book)
+ {
+ var updatedLibrary = GetLibrary().WithoutBook(book);
+ StoreLibrary(updatedLibrary);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
index e71679a..961f3d4 100644
--- a/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
+++ b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
@@ -3,8 +3,8 @@
Exe
net46
- latest
1.2.3.4
+ latest
diff --git a/CliFx.Tests.Dummy/Commands/LogCommand.cs b/CliFx.Tests.Dummy/Commands/LogCommand.cs
index 7786388..ddfb20c 100644
--- a/CliFx.Tests.Dummy/Commands/LogCommand.cs
+++ b/CliFx.Tests.Dummy/Commands/LogCommand.cs
@@ -5,7 +5,7 @@ using CliFx.Services;
namespace CliFx.Tests.Dummy.Commands
{
- [Command("log", Description = "Calculates the logarithm of a value.")]
+ [Command("log", Description = "Calculate the logarithm of a value.")]
public class LogCommand : ICommand
{
[CommandOption("value", 'v', IsRequired = true, Description = "Value whose logarithm is to be found.")]
diff --git a/CliFx.Tests.Dummy/Commands/SumCommand.cs b/CliFx.Tests.Dummy/Commands/SumCommand.cs
index ec106f0..e0d3e59 100644
--- a/CliFx.Tests.Dummy/Commands/SumCommand.cs
+++ b/CliFx.Tests.Dummy/Commands/SumCommand.cs
@@ -6,7 +6,7 @@ using CliFx.Services;
namespace CliFx.Tests.Dummy.Commands
{
- [Command("sum", Description = "Calculates the sum of all input values.")]
+ [Command("sum", Description = "Calculate the sum of all input values.")]
public class SumCommand : ICommand
{
[CommandOption("values", 'v', IsRequired = true, Description = "Input values.")]
diff --git a/CliFx.sln b/CliFx.sln
index 1c10759..62051c1 100644
--- a/CliFx.sln
+++ b/CliFx.sln
@@ -16,7 +16,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Readme.md = Readme.md
EndProjectSection
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Benchmarks", "CliFx.Benchmarks\CliFx.Benchmarks.csproj", "{8ACD6DC2-D768-4850-9223-5B7C83A78513}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Benchmarks", "CliFx.Benchmarks\CliFx.Benchmarks.csproj", "{8ACD6DC2-D768-4850-9223-5B7C83A78513}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Demo", "CliFx.Demo\CliFx.Demo.csproj", "{AAB6844C-BF71-448F-A11B-89AEE459AB15}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -76,6 +78,18 @@ Global
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x64.Build.0 = Release|Any CPU
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x86.ActiveCfg = Release|Any CPU
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x86.Build.0 = Release|Any CPU
+ {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x64.Build.0 = Debug|Any CPU
+ {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x86.Build.0 = Debug|Any CPU
+ {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x64.ActiveCfg = Release|Any CPU
+ {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x64.Build.0 = Release|Any CPU
+ {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.ActiveCfg = Release|Any CPU
+ {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/CliFx/ICliApplication.cs b/CliFx/ICliApplication.cs
index dd4f34e..573d470 100644
--- a/CliFx/ICliApplication.cs
+++ b/CliFx/ICliApplication.cs
@@ -9,8 +9,7 @@ namespace CliFx
public interface ICliApplication
{
///
- /// Runs application with specified command line arguments.
- /// Returns exit code.
+ /// Runs application with specified command line arguments and returns an exit code.
///
Task RunAsync(IReadOnlyList commandLineArguments);
}
diff --git a/Readme.md b/Readme.md
index 45f854b..9c6ac76 100644
--- a/Readme.md
+++ b/Readme.md
@@ -276,6 +276,7 @@ public async Task ConcatCommand_Test()
- [NUnit](https://github.com/nunit/nunit)
- [CliWrap](https://github.com/Tyrrrz/CliWrap)
- [FluentAssertions](https://github.com/fluentassertions/fluentassertions)
+- [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json)
- [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet)
- [Coverlet](https://github.com/tonerdo/coverlet)