Create a bash-like shell for your APIs
netshell maps methods into shell commands.
[Command("echo")]
public void Echo(string text, ConsoleColor color = ConsoleColor.White)
{
Console.ForegroundColor = color;
Console.WriteLine(text);
}
Gets you this:
netshell was developed as a way for ops to have a simplified command-line interface into the application internals without having to know implementation details. Most of the methods you want to export are likely already written as part of tests.
A great alternative for .NET applications is to use PowerShell, it allows for easy scripting of common tasks. Unfortunately, with PowerShell there are some significant hurdles:
- Mix of scripts and assemblies, type loading issues
- Async is difficult
- Suggestions are non-trivial to implement
- Porting existing C# code is tricky, especially when it uses closures
- Auto-completion
- Suggestions
- Fuzzy matching
- Named parameters
- Object printer
- Async support
- Dependency injection
- Help generation
- Stacked commands
- and more
Add the Command([name], [description])
attribute to a method:
class Commands
{
[Command("echo", "Print out the text to standard out")]
void Echo(string text, ConsoleColor color = ConsoleColor.White)
{
}
}
In your Main
method:
static int Main(string[] args)
{
var shell = new NetShell.RpcShell(new Commands()) { Prompt = "sh" };
return shell.Run();
}
or, if you have nothing to customize:
static int Main(string[] args) => NetShell.RpcShell.Run<Commands>();
To create auto-complete suggestions for parameters, add a Suggest
attribute with the name of the method that must be called to get suggestions. The method is expected to return an enumerable of strings.
[Command("connect")]
public void Connect(
[Suggest(nameof(ListPorts))] string port = default,
[Suggest(nameof(BaudRates))] int baud = 9600,
)
{
}
public string[] ListPorts() => SerialPort.GetPortNames();
public string[] BaudRates() => "9600, 14400, 19200, 38400, 57600, 115200".Split(',');
You can either choose to have a void
method and use Console.WriteLine
, or you can return a value from the method. Return values will be displayed with the best-matching object printer.
[Command]
public Person[] GetPeople()
{
return new Person[] {
new Person { Age = 10, Name = "Asti" },
new Person { Age = 11, Name = "Johny" }
};
}
and when you run this:
>getpeople
---------------
| Age | Name |
---------------
| 10 | Asti |
---------------
| 11 | Johny |
---------------
You can do use RpcShell.Register
to register a new dependency to be injected.
Whenever a method is being called, it will inject all registered types as parameters, and these injected parameters will not show up as part of your command syntax.
For example, if you do
shell.Register(new HttpClient());
in your method you can use HttpClient
as a parameter, and it will be injected in.
[Command("validate-phone")]
public Task<string> Validate(string number, HttpClient client) =>
client.GetStringAsync($"https://numvalidate.com/api/validate?number={number}");
The shell itself is injected. For example, to change the current prompt from within a command:
[Command("cd")]
public void ChangeDir(string path, Shell shell)
{
shell.Prompt = Environment.CurrentDirectory = path;
}
You can write async
methods as you would normally.
[Command("validate-phone")]
public async Task<string> Validate(string number)
{
var response = await new HttpClient().GetAsync($"https://numvalidate.com/api/validate?number={number}");
var json = await response.Content.ReadAsStringAsync();
return json;
}
You can also have cancellation.
[Command("validate-phone")]
public async Task<string> Validate(string number, CancellationToken cancellation)
{
var response = await new System.Net.Http.HttpClient().GetAsync($"https://numvalidate.com/api/validate?number={number}", cancellation);
var json = await response.Content.ReadAsStringAsync();
return json;
}
The CancellationToken
is injected and has no impact on your commands' syntax. See this.
Pressing any key cancels the executing command.
> help validate-phone
Command validate-phone
Syntax: validate-phone (String number)
Although there exists a internal implementation of help
, it's a good idea to implement your own help method with any additional info you may have.
[Command("help")]
public string Help(string command, RpcShell shell)
{
return shell.GetHelp(command);
}
There's a default implementation for exit
.
Implementing exit
:
[Command("exit")]
public void Exit(RpcShell shell)
{
shell.Exit(0);
}
There're default implementations of help
, exit
and clear
in CommandBase
.
You can just inherit from it if you want to avoid writing your own.
The submodules are internal to the project to simplify differences with the original projects.