[.NET]: Let’s Create a CLI with System.CommandLine

[.NET]: Let’s Create a CLI with System.CommandLine

Today I will be showing you how I have built a CLI task tracker using console applications in .NET.

This task tracker consists of

  • tasks you need to do
  • tasks you are currently working on
  • tasks you have done

The requirement is such that a user should be able to

  • add, update and delete tasks
  • list all tasks
  • mark a task as either Todo, In Progress, or Done

My CLI commands are as follows

taskcli tasks status-list
taskcli tasks read
taskcli tasks add "your-task"
taskcli tasks edit "task-id" "task-name"
taskcli tasks remove "task-id"
taskcli tasks status "task-id" "Done|InProgress|ToDo"
taskcli tasks clear

In order to achieve this goal, I will be using System.CommandLine package from Microsoft NuGet package to build this command-line applications. For the sake of this project, I have decided to use the version 2 beta 7 because the documentation is readily available.

System.CommandLine provides us with command-line syntaxes that is beneficial to use. For instance,

  1. Option
  • It is a named parameter that can be passed to a command.
  • It is typically used with 2 hyphens (--)
  • For example, taskcli tasks read --file tasks.json shows that --file can be used explicitly. It can also use "tasks.json" by default; for instance taskcli tasks read
  • Since Option<T> binds to the type <T>, it can behave either as boolean, FileInfo, int or string, the value returned is of that type.
  • Option is optional, but if set to IsRequired, we need to specify the options and the value that comes along with it.
  1. Argument
  • it is an unnamed parameter which is used for getting values
  • For example, id is used as an argument as we need to specify an id when deleting or updating a task.
  • taskcli tasks delete 1
  1. RootCommand
  • It is the top level statement in the entry point of your CLI which holds description, global options and child commands.
  • For example taskcli
  1. Command / Subcommand
  • General purpose class for any command / subcommand
  • It can either be an action or a group of related actions
  • For example tasks is a command that specifies an action
  • Command is used when we have multiple resources working together and for visibility, for example taskcli users could be used to map users to tasks and also it makes sense to keep it this way due to existing ecosystems already using it
  • Sub command is just a command nested under another command
  • For example add subcommand can invoke using taskcli tasks

Codes

One global option and 3 arguments were used.

Option<FileInfo> fileOption = new("--file")
        {
            Description = "An option whose argument is parsed as a FileInfo",
            Required = true,
            DefaultValueFactory = result =>
            {
                if (result.Tokens.Count == 0)
                {
                    return new FileInfo("tasks.json");

                }
                string filePath = result.Tokens.Single().Value;
                if (!File.Exists(filePath))
                {
                    result.AddError("File does not exist");
                    return null;
                }
                else
                
                {
                    return new FileInfo(filePath);
                }
            }
        };

In option, – file is used in order to tell the CLI where to read/write the tasks, and to validate the existence of our JSON file. If it does not then an error is thrown.

var idArgument = new Argument<int>("id");

var taskArgument = new Argument<string>("TaskName");
        
var statusArgument = new Argument<TaskStatus>("Status");

Several arguments are created, id, TaskName, and Status.

The reason is because we will be passing at times id, TaskName, or status, depending on what we are trying to achieve.

For instance, if we needed to add tasks, we would do

taskcli tasks add "Writing codes"
RootCommand rootCommand = new("TaskCliTool");
fileOption.Recursive = true;
rootCommand.Options.Add(fileOption);

The root command has a task command, and task command several actions; add, delete, update, read, status-list, status update and clear

Command taskCommand = new("tasks", "Work with file to contain tasks");
        rootCommand.Subcommands.Add(taskCommand);
        
        Command viewStatusCommand = new("status-list","read and display tasks" );
        taskCommand.Subcommands.Add(viewStatusCommand);
        
        Command readCommand = new("read","read and display tasks" );
        taskCommand.Subcommands.Add(readCommand);
        
        Command addCommand = new("add","Add task");
        addCommand.Arguments.Add(taskArgument);
        addCommand.Aliases.Add("insert");
        taskCommand.Subcommands.Add(addCommand);
        
        Command deleteComamnd = new("delete","Delete task")
        {
            idArgument
        };
        deleteComamnd.Aliases.Add("remove");
        taskCommand.Subcommands.Add(deleteComamnd);
        
        Command updateCommand = new("update","Update task")
        {
            idArgument, taskArgument
        };
        updateCommand.Aliases.Add("edit");
        taskCommand.Subcommands.Add(updateCommand);
        
        Command updateStatus = new("status","Update status task")
        {
            idArgument, statusArgument 
        };
        taskCommand.Subcommands.Add(updateStatus);
        
        Command clearCommand = new("clear","clear tasks" );
        taskCommand.Subcommands.Add(clearCommand);

Methods used for Saving and Reading from JSON:

 static List<TaskItem> Load(FileInfo file)
    {
        if(!file.Exists) return new List<TaskItem>();
        var json = File.ReadAllText(file.FullName);
        return string.IsNullOrWhiteSpace(json) ? new List<TaskItem>() : 
            (JsonSerializer.Deserialize<List<TaskItem>>(json) ?? new List<TaskItem>());
    }

    static void Save(FileInfo file, List<TaskItem> items)
    {
        Console.WriteLine("Serialising tasks...");
        var json = JsonSerializer.Serialize(items);
        File.WriteAllText(file.FullName, json);
    }

Methods used to manipulate the JSON

static void StatusOptions()
    {
        Console.WriteLine("Available tasks status options:\n");
        foreach (TaskStatus status in Enum.GetValues(typeof(TaskStatus)))
        {
            Console.WriteLine($"{status}");
        }
    }
    private static void ListTasks(FileInfo file)
    {
        foreach (string line in File.ReadLines(file.FullName))
        {
            Console.WriteLine(line);
        }
    }

    private static void AddTask(FileInfo file, string task)
    {
        Console.WriteLine("Adding task");
        // Get tasks from JSON file
        var items = Load(file);
        var nextId = items.Count == 0 ? 1 : items.Max(item => item.id) + 1 ;
        items.Add(new TaskItem(nextId, task, TaskStatus.ToDo));
        
        //  Save to JSON
        Save(file, items);
        Console.WriteLine($"Added task {task} with id {nextId}");
    }

    private static void DeleteTask(FileInfo file, int taskId)
    {
        Console.WriteLine("Deleting task");
        var items = Load(file);
          items.Where(item => item.id == taskId).ToList().ForEach(item => items.Remove(item));
        
        Save(file, items);
        Console.WriteLine($"Deleted task {taskId}");
    }

    private static void UpdateTask(FileInfo file, int taskId, string newTaskName)
    {
        Console.WriteLine("Updating task");
        var items = Load(file);
        var item = items.SingleOrDefault(item => item.id == taskId);
        if (item != null) item.TaskName = newTaskName;
        // var id = items.FindIndex(item => item.id == taskId);
        // items[id] = items[id] with {Text = newText};
        Save(file, items);
        Console.WriteLine($"Updated task {taskId}");
    }

    private static void UpdateStatus(FileInfo file, int taskId, TaskStatus status)
    {
        Console.WriteLine("Updating status");
        var items = Load(file);
        var item = items.SingleOrDefault(item => item.id == taskId);
        if (item != null) item.Status = status;
        Save(file, items);
        Console.WriteLine($"Updated status {taskId}");
    }

    public static void ClearTasks(FileInfo file)
    {
        Console.WriteLine("Clearing tasks");
        var items = Load(file);
        items.Clear();
        Save(file, items);
    }

Results

fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks read
[]
fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks add "hello world"
Adding task
Serialising tasks...
Added task hello world with id 1
fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks add "Writing Code"
Adding task
Serialising tasks...
Added task Writing Code with id 2
fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks add "Testing Code"
Adding task
Serialising tasks...
Added task Testing Code with id 3
fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks add "Write blog post"
Adding task
Serialising tasks...
Added task Write blog post with id 4
fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks delete 1
Deleting task
Serialising tasks...
Deleted task 1
fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks read                 
[{"id":2,"TaskName":"Writing Code","Status":"ToDo"},{"id":3,"TaskName":"Testing Code","Status":"ToDo"},{"id":4,"TaskName":"Write blog post","Status":"ToDo"}]
fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks status-list
Available tasks status options:

ToDo
InProgress
Done
fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks status 2 "Done"
Updating status
Serialising tasks...
Updated status 2
fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks read           
[{"id":2,"TaskName":"Writing Code","Status":"Done"},{"id":3,"TaskName":"Testing Code","Status":"ToDo"},{"id":4,"TaskName":"Write blog post","Status":"ToDo"}]
fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks update 2 "Write CLI Code"
Updating task
Serialising tasks...
Updated task 2
fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks read                     
[{"id":2,"TaskName":"Write CLI Code","Status":"Done"},{"id":3,"TaskName":"Testing Code","Status":"ToDo"},{"id":4,"TaskName":"Write blog post","Status":"ToDo"}]
fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks clear
Clearing tasks
Serialising tasks...
fz3hra@Ummes-MacBook-Pro ~ % taskcli tasks read 
[]

And... that's basically it. I have built a small CLI task tracker and shared the steps so that you can also build yours.


References

Tutorial: Get started with System.CommandLine - .NET
Learn how to use the System.CommandLine library for command-line apps.

About Me

I am Zaahra, a Google Women Techmakers Ambassador who enjoy mentoring people and writing about technical contents that might help people in their developer journey. I also enjoy building stuffs to solve real life problems.

To reach me:

LinkedIn: https://www.linkedin.com/in/faatimah-iz-zaahra-m-0670881a1/

X (previously Twitter): _fz3hra

GitHub: https://github.com/fz3hra

Cheers,

Umme Faatimah-Iz-Zaahra Mujore | Google Women TechMakers Ambassador | Software Engineer