Tree

Back in my childhood while playing with a Win98 desktop, I've discovered the command `tree`.
For a child that knew nothing about computers properly, the amount of text displayed made me happy.
It was not only a bunch of text, it was a proper listing of dirs and subdirs.

On my path to gain some experience with C# I thought "Why not make my version of tree?".
Surely the world does not need another version tree, but it won't hurt either.
To make it more interesting I did most of it without a proper IDE.

Setting the scene

Tree does this:

$ tree # Tree is the current Directory
Tree
|--Tree.sln
|-/Program
| |--Program.cs
| |--Program.csproj
|-/Tree
| |--FileTree.cs
| |--Renderer.cs
| |--Tree.csproj
|-/Tree.Test
|--FileTreeTest.cs
|--RendererTest.cs
|--Tree.Test.csproj
|--Usings.cs

And the code is here.

But to write down my own initial direction:

Beginning a project

To C# pros or IDE users this might be cake, but starting a C# project can be REALLY alien depending from were you came.
Fortunately Tree is a simple project, so bootstrapping the project is like 35% of the job, which makes it somewhat enjoyable.

The `dotnet` command

`dotnet new console` gives you a Hello World, `dotnet build` and `dotnet run` do what you expect and the rest is history.

Looks like units of code in C# are grouped into projects (.csproj) and they can be either runnable (like console) or linkable (like library). The `.csproj` contains a XML describing your project and the references it needs.

Unlike Go or other friends, testing is itself a separate unit of code which uses a testing framework to test other units of code.

Nuget is used to resolve external dependencies and this is how it imports xUnit, which is the testing framework I'm using.

Because projects are either runnable or linkable and tests are a separate unit, we get our simple, but almost intuitive, project structure as seen here.

To skip the hassle of moving between project folders, solutions files that are usually managed by the IDE can be used to manage a reference to multiple projects with the `dotnet sln ...` collection of commands.

Laying the structure

The Tree project holds the logic to generate the model and render it, Tree.Test tests those functionalities and Program does the dirty part of processing user input and walking the file system.

Model

The Node defines the base behavior needed to render the listing.

public abstract class
Node
{
protected string
Name;

public
Node(string name)
{
Name = name;
}

public string
GetName()
=> Name;

abstract public void
Add(Node node);

abstract public List<Node>
GetChildren();

abstract public bool
IsFinal();
}

Because the walking itself is handled inside the Program project, Directory and File classes are as simple as needed and not tied any SO walking.
The Directory class contains the method Directory With(Node node) which gives a DSL feel to it when writing listing tests.

Rendering

The algorithm is mostly implemented here:

static void
Render(NodeKind kind, string name, In<string> stream, List<IdentKind> indentation)
{
for(int i = 0; i < indentation.Count-1; i++)
switch(indentation[i])
{
case(IdentKind.NonFinal):
stream.Put("| ");
break;
case(IdentKind.Final):
stream.Put(" ");
break;
}

if(indentation.Count > 0)
switch(kind)
{
case(NodeKind.File):
stream.Put("|--");
break;
case(NodeKind.Directory):
stream.Put("|-/");
break;
}

stream.Put(name);
stream.Put("\n");
}

The indentation list is used to handle the proper padding.
When rendering nodes inside the last node of a parent directory, its marked as final.
The "smart" bit is on ignoring the last indent of the list, this simplifies a bit the overall manipulation of the list.

Program

The final bit is mostly discovering the APIs to list directories and parse arguments. Most of the code is derived from the structure of the main function:

static void
Main(string[] args)
{
Setup setup = new(args);
Node? node = null;

var error = setup.Error;
if(error is not null)
Exit(error);

foreach(var path in setup.Paths)
{
if(IsValid(path))
node = Lookup(path, setup);

if(node is not null)
Render(node, setup);
else
throw new Exception(String.Format(
"Invalid path {0}", path));
}
}

Installing

I was not able to find an easy way to install the final console project into my environment, maybe there's a `dotnet install` of some sort, but copying the executable suffices my needs.


Go home.

☆Powered by Azure Blob