Hello everybody,

 

I would like to share with you an app I've been working on. I call it AF Bash. It allows you to interact with AF the same way you interact with your files through CMD:

2018-03-13 14_48_38-C__Users_rborges_Documents_Projects_afbash_afbash_bin_Debug_afbash.exe.png

 

From a code perspective, it's a framework that provides a quick and easy way for you to implement your set of commands, so I encourage you to write your custom commands and send a pull request to the main repository! But first, lets break it down into topics and explain the architecture and implementation details

 

1) Architecture

Here is a very simple implementation diagram:

2018-03-13 16_12_05-Drawing1 - Visio Professional.png

 

AFBash is a console application that uses Autofac as its IoC container and exposes an interface called ICommand that is implemented by BaseCommand, an abstract class that is the base class for all commands to derive from. It provides a context class full of goodies that should be used to access AF SDK data. The console is wrapped around a custom version of ReadLine, where I can manage command history, autocompletation and command cancelling.

 

I strongly encourage you to follow the comments in the main entry point because it will make easier o understand how the main loop works and what it expects from your custom Command.

 

2) Adding a new Command

So lets stop the chit chat and go to the fun part. How to create a custom command. For this example, let me show you how to implement a dir / ls command.

 

First you need a class that is derived from BaseCommand.

 

class Dir : BaseCommand 

 

Because we are using Autofac's IoC container, we just need to declare a ctor that receives a Context as parameter.

 

public Dir(Context context) : base(context)

 

Now we have to take care of 4 functions:

 

public override ConsoleOutput Execute(CancellationToken token)
public override List<string> GetAliases()
public override string GetHelp()
public override (bool, string) ProcessArgs(string args)

 

The GetAliases() function must provide the alias you want for the command that you are implementing. The GetHelp() must return a simple help information.

 

public override List<string> GetAliases()
{
     return new List<string> { "dir", "ls" };
}
public override string GetHelp()
{
     return "Lists all children of an element";
}

 

The ProcessArgs() function is where get the arguments that your command must parse and store any variable that will be used later by Execute(). Note that BaseCommand exposes a global variable called BaseElement where you can store the AFObject output of your parsing. Going get back to our exemple, a dir command can be execute with or without parameters. A parameter-less dir implies that you want to list everything from the current element. Meanwhile, a parameter may be a full or relative path. So how to take care of it? simple. The AppContext has a function called GetElementRelativeToCurrent(string arg). It will return the element based on the argument passed. Here some examples:

Current
Argument
Result
\\Server\Database\FirstElement\Child".."\\Server\Database\FirstElement
\\Server\Database\FirstElement\Child"grandChild"\\Server\Database\FirstElement\Child\grandChild
\\Server\Database\FirstElement\Child"\" or "\\"\\ (a state where no element is selected)
\\Server\Database\FirstElement\Child"~"\\DefaultServer\DefaultDatabase
\\Server\Database\FirstElement\"child\grandChild"\\Server\Database\FirstElement\Child\grandChild
\\Server\Database\FirstElement\Child"" or null\\Server\Database\FirstElement\Child

 

So far we have this for our argument processing (note that I'm using C#7.0, where a function can return multiple arguments.):

 

public virtual (bool, string) ProcessArgs(string args)
{
    BaseElement = AppContext.GetElementRelativeToCurrent(args);

    if (BaseElement == null)
        return (false, string.Format("Object '{0}' not found", args));
    else
        return (true, null);
}

 

It's only missing one thing: when your current element is the top most node, the CurrentElement is Null because there is no PISystem selected. So a Null BaseElement is not necessarily a bad thing. We just have to check whether the user intentionally did that or was a mistake. This processing is already implemented as a virtual function on BaseCommand. So if your function argument is a path you don't even need to override it as BaseElement will be populated with the target element.

 

Finally the Execute() function.

 

It must return a ConsoleOutput, a wrapper class that makes easier for you to print structures into the console. In our exemple lets set a header message like CMD's dir:

 

ConsoleOutput console = new ConsoleOutput();
console.AddHeaderLine(string.Format("Children of {0}", BaseElement is null ? "root" : BaseElement.GetPath()));

 

We have a table-like result, so we need to set the headers:

 

console.SetBodyHeader(new List<string> { "Type", "Name" });

 

Now, we get the children of BaseElement and loop through them, printing everything we want:

 

var children = AppContext.GetChildren(BaseElement);
children.ForEach(c => {
                console.AddBodyLine(new List<Tuple<string, Color>> {
                    new Tuple<string, Color>(c.Identity.ToString(), AppContext.Colors[c.Identity]),
                    new Tuple<string, Color>(c.ToString(), AppContext.Colors.Base)
                });
            });
return console;

 

And that's it! Now you just need to compile and you are good to go! The actual implementation of my dir also handle data for attributes. I encourage you to go and see how I did it.

 

3) Available Commands

 

So far I have implemented 7 basic commands: CD / LS

 

This is a work in progress, so if you clone this project, keep your repo up-to-date because I will keep pushing bug fixes and new features.

 

Finally, if you find bugs or have questions, let me know!