Skip to main content
SEMastery
.NET Corebeginner

How to Create Command-Line Console Applications in .NET

A beginner-friendly guide to building command-line console apps in .NET 10 with the dotnet CLI, arguments, options, and System.CommandLine.

11 min readUpdated January 21, 2026

Think about a vending machine at a railway station. You walk up, press a few buttons, and out comes your snack. You do not need a touch screen or a friendly cartoon helper. You just press buttons, and the machine does its job.

A command-line console application works the same way. The user types a few words, presses Enter, and the program does its job and prints the result. No mouse. No colourful windows. Just text in, text out. It is simple, fast, and very powerful.

In this guide you will learn how to build console apps in .NET 10, step by step. We will start with the smallest possible program, then teach it to read what the user types, and finally use a helpful library called System.CommandLine to build a real tool with proper commands and options.

Why learn console apps first?

When you are learning to cook, you start with boiling water before you attempt a wedding feast. Console apps are the "boiling water" of programming. They are the simplest kind of app, so they are the best place to learn the core ideas.

Console apps are not just for practice, though. Real engineers use them every day:

Use caseExample
Developer toolsgit, dotnet, npm are all console apps
Automation scriptsRename 500 files, clean up logs, send a report
Background jobsA nightly task that emails sales numbers
Learning and testingTry a new idea fast without any UI work

A console app talks to the world through three simple channels. Understanding these three will make everything else click.

The three streams every console app uses

stdin is the input the user types. stdout is where normal results go. stderr is a separate channel just for errors, so problems do not get mixed up with good results. Keeping these separate is a small habit that makes your tools play nicely with other tools.

Step 1: Create your first console app

You need the .NET SDK installed. In .NET 10 (the current long-term support release), one command creates a full project for you. Open your terminal and type:

dotnet new console -o HelloCli
cd HelloCli
dotnet run

The first line says "make a new console project in a folder named HelloCli". The second line moves you into that folder. The third line builds and runs it. You should see Hello, World! printed on the screen.

Let us look at what dotnet new console gave you. The main file is Program.cs, and it is very short:

// Program.cs
Console.WriteLine("Hello, World!");

That is the whole program. There is no class, no Main method, no curly braces around everything. This style is called top-level statements. The compiler quietly wraps your code in a Main method for you, so you can focus on what matters.

Here is what happens behind the scenes when you run dotnet run.

From source code to running app

Restore
Build
Run

Steps

1

Restore

Download any packages you need

2

Build

Compile C# into a DLL

3

Run

Start the app and print output

What dotnet run does for you

Step 2: Read what the user types

A vending machine that ignores your button presses is useless. Your app needs to read what the user types after the program name. Those typed words are called command-line arguments.

With top-level statements, .NET hands you a ready-made array called args. Each word becomes one item in the array.

// Program.cs
if (args.Length == 0)
{
    Console.WriteLine("Please tell me your name. Example: dotnet run -- Riya");
    return;
}
 
string name = args[0];
Console.WriteLine($"Namaste, {name}! Welcome to your first CLI tool.");

Run it like this:

dotnet run -- Riya

The -- tells dotnet run "everything after this belongs to my program, not to you". So Riya lands in args[0], and the app prints a friendly greeting.

This works, but it has limits. What if the user types options like --shout or --times 3? With a plain args array, you have to write all the checking code by hand. That gets messy fast. This is the moment to bring in a helper.

Step 3: Meet System.CommandLine

System.CommandLine is a free, open-source library from Microsoft. It does the boring, error-prone work of reading arguments and options for you. It also gives you nice help text and error messages for free, and it works with Native AOT for tiny, fast tools.

Add it to your project:

dotnet add package System.CommandLine

Before we write code, let us learn three words you will see again and again. Getting these clear in your head makes the library easy.

TermWhat it meansExample
CommandAn action the tool can dogit commit
ArgumentA required value, by positionthe file name in cat notes.txt
OptionA named, often optional setting--times 3 or --shout

Here is how those pieces fit together when a user runs your tool.

How a command line is broken into pieces

Step 4: Build a real greeter tool

Let us build a small tool that greets a person, can repeat the greeting, and can shout it in capital letters. This shows arguments and options working together.

// Program.cs
using System.CommandLine;
 
var nameArgument = new Argument<string>("name")
{
    Description = "The person to greet"
};
 
var timesOption = new Option<int>("--times")
{
    Description = "How many times to greet",
    DefaultValueFactory = _ => 1
};
 
var shoutOption = new Option<bool>("--shout")
{
    Description = "Greet in capital letters"
};
 
var root = new RootCommand("A friendly greeting tool")
{
    nameArgument,
    timesOption,
    shoutOption
};
 
root.SetAction(parseResult =>
{
    string name = parseResult.GetValue(nameArgument)!;
    int times = parseResult.GetValue(timesOption);
    bool shout = parseResult.GetValue(shoutOption);
 
    for (int i = 0; i < times; i++)
    {
        string message = $"Hello, {name}!";
        Console.WriteLine(shout ? message.ToUpper() : message);
    }
    return 0;
});
 
return root.Parse(args).Invoke();

Now try a few runs:

dotnet run -- Riya
dotnet run -- Riya --times 3
dotnet run -- Riya --times 2 --shout
dotnet run -- --help

The last one is a gift. You never wrote any help text logic, yet --help prints a clean list of arguments and options. The library built it from the descriptions you gave. If a user types something wrong, like --times abc, the library prints a clear error and a non-zero exit code, so other scripts know something failed.

Here is the journey a typed command takes inside your app.

The life of a command

Type
Parse
Validate
Run
Output

Steps

1

Type

User enters the command

2

Parse

Split into command, args, options

3

Validate

Check types and required values

4

Run

Your action code executes

5

Output

Print result or error

From typed text to printed result

Step 5: Add sub-commands

Big tools group their actions into sub-commands. Think of dotnet build, dotnet test, and dotnet run. dotnet is the main command, and build, test, and run are sub-commands. Each one does a different job.

Let us imagine a tiny notes tool with two sub-commands: add and list. The structure looks like a small tree.

A tool with two sub-commands

In code, you create each sub-command and attach it to the root. Each sub-command gets its own arguments, options, and action.

// Program.cs
using System.CommandLine;
 
var textArgument = new Argument<string>("text")
{
    Description = "The note to save"
};
 
var addCommand = new Command("add", "Add a new note");
addCommand.Arguments.Add(textArgument);
addCommand.SetAction(parseResult =>
{
    string text = parseResult.GetValue(textArgument)!;
    File.AppendAllText("notes.txt", text + Environment.NewLine);
    Console.WriteLine($"Saved: {text}");
    return 0;
});
 
var listCommand = new Command("list", "Show all notes");
listCommand.SetAction(_ =>
{
    if (!File.Exists("notes.txt"))
    {
        Console.WriteLine("No notes yet.");
        return 0;
    }
    Console.WriteLine(File.ReadAllText("notes.txt"));
    return 0;
});
 
var root = new RootCommand("A tiny notes tool");
root.Subcommands.Add(addCommand);
root.Subcommands.Add(listCommand);
 
return root.Parse(args).Invoke();

Now your tool has two clear actions:

dotnet run -- add "Buy milk"
dotnet run -- list

Notice how each sub-command stays small and focused. This is the same idea the real dotnet and git tools use, just smaller. As your tool grows, you keep adding sub-commands without making any single piece complicated.

Exit codes: how scripts know if you succeeded

There is one habit that separates a toy from a professional tool: the exit code. When your program ends, it returns a number. By a long-standing rule, 0 means success and any other number means something went wrong.

This matters because other programs and scripts check that number to decide what to do next. If your backup tool returns 0, the script continues. If it returns 1, the script can stop and raise an alarm.

Exit codeMeaningWhen to use it
0SuccessThe job finished correctly
1General errorSomething failed, no special reason
2MisuseBad arguments or options

In top-level statements you set the exit code by simply return-ing an integer, or by setting Environment.ExitCode. In the greeter example above, our action returned 0 to signal success. Always returning a clear exit code makes your tool a good citizen in the bigger world of scripts and automation.

// Program.cs
try
{
    DoTheWork();
    return 0; // success
}
catch (Exception ex)
{
    Console.Error.WriteLine($"Error: {ex.Message}"); // goes to stderr
    return 1; // failure
}

Notice Console.Error.WriteLine. That sends the message to stderr, the error channel. This is the polite thing to do, because it keeps your error messages out of the normal output that another program might be reading.

A few friendly habits

As you build more tools, these small habits will make your apps feel professional:

  • Give every argument and option a clear Description. Future-you will be grateful when reading --help.
  • Send errors to Console.Error, not Console.WriteLine.
  • Return a meaningful exit code so scripts can react.
  • Keep each sub-command small and focused on one job.
  • Provide sensible default values so users type less.

When to use which approach

You have now seen two ways to read input: the plain args array, and System.CommandLine. Which should you pick?

Use the plain args array when your tool takes one or two simple inputs and you are just learning or prototyping. It needs zero extra packages and is perfect for a quick script.

Reach for System.CommandLine when you have options, sub-commands, default values, or you want automatic --help and good error messages. In short: if the tool is something other people will use, the library pays for itself quickly.

A small note on libraries: some popular .NET packages have changed their licences recently. For example, MediatR and MassTransit are now commercially licensed for many uses. System.CommandLine, the library we used here, is open source and free, maintained by Microsoft, so you can use it without worry.

Quick recap

  • A console app is a text-in, text-out program. It uses three streams: stdin, stdout, and stderr.
  • Create one with dotnet new console -o MyApp, then cd MyApp and dotnet run.
  • Top-level statements let you skip the boilerplate and start with one line of code.
  • The plain args array is the simplest way to read what the user types. Use -- after dotnet run to pass arguments.
  • System.CommandLine parses arguments, options, and sub-commands for you, and gives free --help text and error messages.
  • Learn three words: command (an action), argument (a value by position), and option (a named setting like --shout).
  • Group actions into sub-commands the way git and dotnet do.
  • Always return a clear exit code: 0 for success, non-zero for failure. Send errors to stderr.

References and further reading

Related Posts