Commit 9a63a276 authored by Tim Bureck's avatar Tim Bureck

* Fixed default argument and option values not being considered in ArgsInput

* Fixed missing binding for default IInput in Application
* Added license file
* Added readme file
parent f3069103
Copyright 2019 Tim Bureck
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
# Terminal App
Terminal App is a lightweight framework that enables you to quickly write your own terminal/command line applications. It is heavily inspired by [Symfony's Console component](https://github.com/symfony/console). Why? Because I think it's API is simple yet powerful and I wanted something similar in a .NET environment as well.
# How to install
Simply add this library to your project by using NuGet:
```
dotnet add TBureck.Terminal
```
# Usage
There are two classes that are of particular importance for you to write your own application:
- `Application` is the main entry point for your application code. It manages commands as well as the general flow of handling commands
- `Command` is the base class for your commands. It offers APIs for defining meta information as well as applicable arguments and options for this command
## Setting up and running an `Application`
For starters, try this little program:
```
using TBureck.Terminal;
public class Program
{
public static int main(String[] args)
{
Application app = new Application();
return app.Run(args);
}
}
```
Apparently this program does not do very much, it will even result with exit code 1. This is because we did tell it what it should do. In order to do so, you can call the program with one argument:
```
$ mytestapp.exe list
```
The first argument passed to the `Run` method will be used to find the command to be executed. The framework comes with a pre-defined `ListCommand`, which will print a list of all commands registered with the `Application` object.
## Writing a command
Writing a command can be done by extending the `Command` class. You will need to define a few things so that the application can later find that command:
```
using TBureck.Terminal.Commands;
public class SayHelloCommand : Command
{
public override void Configure()
{
this.Name = "say-hello";
this.Description = "This friendly command says 'hello'!";
}
public override int Execute(IInput input, IOutput output)
{
output.WriteLine("Hello World!");
return 0;
}
}
```
In the `Configure` method you can add meta information for your command, like the name it should be found under. The `Execute` method will be run within the `Application`'s `Run` method.
Ok, we have a command. All we have to do now is connect it to our application object. Edit your `Program` class so it looks like this:
```
using TBureck.Terminal;
public class Program
{
public static int Main(string[] args)
{
Application app = new Application();
app.Add(new SayHelloCommand());
return app.Run(args);
}
}
```
Now, when running:
```
$ mytestapp.exe say-hello
```
the program should print "Hello World!" and return with exit code 0, which marks a successful run.
## Using arguments and options
Let's extend our `SayHelloCommand` a bit. Assume you want to greet a special... thing or person or whatever you want to greet. One way to do this is adding an argument. Adjust your class as follows:
```
public class SayHelloCommand : Command
{
public override void Configure()
{
this.Name = "say-hello";
this.Description = "This friendly command says 'hello'!";
this.AddArgument("subject", InputArgumentMode.Optional, "What/Who do you want to greet", "World");
}
public override int Execute(IInput input, IOutput output)
{
output.WriteLine($"Hello {input.Arguments["subject"]}!");
return 0;
}
}
```
The `AddArgument` method has four parameters:
- name; the name under which the argument will be accessible via the `IInput` object
- mode; whether the argument should be required or optional. Defaults to optional
- description; a human-readable description
- default value; the value which should be used when the argument has not been passed. Only for optional arguments. Defaults to `null`
Once you have added an argument, you will be able to access it via the `Arguments` property of the `IInput` object. Use the name as the accessor.
Secondly, we might want to greet with a different word. To achieve this, you can use another argument or an option:
```
public class SayHelloCommand : Command
{
public override void Configure()
{
this.Name = "say-hello";
this.Description = "This friendly command says 'hello'!";
this.AddArgument("subject", InputArgumentMode.Optional, "What/Who do you want to greet", "World");
this.AddOption("greeting", "g", InputOptionMode.ValueRequired, "The word used for the greeting.", "Hello");
}
public override int Execute(IInput input, IOutput output)
{
output.WriteLine($"{input.Options["greeting"]} {input.Arguments["subject"]}!");
return 0;
}
}
```
The `AddOption` method has five parameters:
- name; the name under which the option will be accessible
- shortcut; a shortcut that can be used instead of the name
- mode; whether the option accepts a value or not and whether it is required. Defaults to no value
- description; a human-readable description
- default value; the default value of the option, which can only be applied, if the option accepts a value. Defaults to `null`
Options per se are always optional. The user might leave them away at any time. Now you can call the enhanced program with:
```
$ mytestapp.exe say-hello Internet -g Hi
```
......@@ -50,16 +50,21 @@ namespace TBureck.Terminal
return 1;
}
if (input == null) {
input = ArgsInput.Of(args.Skip(1).ToList());
}
int exitCode;
try {
// Treat first argument as the command name:
string commandName = args.FirstOrDefault();
ICommand command = this.FindCommand(commandName);
if (input == null) {
ArgsInput argsInput = ArgsInput
.Of(args.Skip(1).ToList());
argsInput.Bind(command.InputDefinition);
input = argsInput;
}
exitCode = command.Execute(input, output);
} catch (Exception e) {
......
......@@ -29,10 +29,22 @@ namespace TBureck.Terminal.IO
this.Options.Clear();
this.InputDefinition = inputDefinition;
this.InitializeDefaults();
this.ParseArgs(this._unparsedArgs);
}
private void InitializeDefaults()
{
foreach (InputArgument argument in this.InputDefinition.Arguments.Where(a => a.Default != null)) {
this.Arguments[argument.Name] = argument.Default;
}
foreach (InputOption option in this.InputDefinition.Options.Where(o => o.Default != null)) {
this.Options[option.Name] = option.Default;
}
}
private void ParseArgs(IList<string> args)
{
if (args.Count == 0) {
......
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>TBureck.Terminal</RootNamespace>
<PackageId>TBureck.Terminal</PackageId>
<PackageVersion>0.1.0</PackageVersion>
<Authors>Tim Bureck &lt;kontakt@tbureck.de&gt;</Authors>
<Description>A simple .NET terminal application framework</Description>
<PackageProjectUrl>https://git.tbureck.de/tbureck/terminal-app</PackageProjectUrl>
<PackageLicenseUrl>LICENSE</PackageLicenseUrl>
<RepositoryUrl>https://git.tbureck.de/tbureck/terminal-app</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>terminal; cli; command line</PackageTags>
</PropertyGroup>
</Project>
......@@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal", "Terminal\Termin
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalTests", "TerminalTests\TerminalTests.csproj", "{864400FB-177A-4934-BFAB-D24B0D835B4F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "TestApp\TestApp.csproj", "{A3FA89B3-4BE8-40F4-AD24-93B1543F9AD7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
......@@ -18,5 +20,9 @@ Global
{864400FB-177A-4934-BFAB-D24B0D835B4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{864400FB-177A-4934-BFAB-D24B0D835B4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{864400FB-177A-4934-BFAB-D24B0D835B4F}.Release|Any CPU.Build.0 = Release|Any CPU
{A3FA89B3-4BE8-40F4-AD24-93B1543F9AD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A3FA89B3-4BE8-40F4-AD24-93B1543F9AD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A3FA89B3-4BE8-40F4-AD24-93B1543F9AD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A3FA89B3-4BE8-40F4-AD24-93B1543F9AD7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
......@@ -28,6 +28,20 @@ namespace TBureck.Tests.Terminal.IO
Assert.Empty(argsInput.Options);
}
[Fact]
public void DefaultArgumentAndOption_AreTakenOver()
{
InputDefinition inputDefinition = new InputDefinition();
inputDefinition.AddArgument(InputArgument.Of("foo", @default: "bar"));
inputDefinition.AddOption(InputOption.Of("def", mode: InputOptionMode.ValueRequired, @default: "foobar"));
ArgsInput argsInput = ArgsInput.Of(new List<string>());
argsInput.Bind(inputDefinition);
Assert.Equal("bar", argsInput.Arguments["foo"]);
Assert.Equal("foobar", argsInput.Options["def"]);
}
[Fact]
public void SimpleArgument_IsWrittenIntoArgumentsNotIntoOptions()
{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment