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.
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 case | Example |
|---|---|
| Developer tools | git, dotnet, npm are all console apps |
| Automation scripts | Rename 500 files, clean up logs, send a report |
| Background jobs | A nightly task that emails sales numbers |
| Learning and testing | Try 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.
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 runThe 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
Steps
Restore
Download any packages you need
Build
Compile C# into a DLL
Run
Start the app and print output
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 -- RiyaThe -- 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.CommandLineBefore we write code, let us learn three words you will see again and again. Getting these clear in your head makes the library easy.
| Term | What it means | Example |
|---|---|---|
| Command | An action the tool can do | git commit |
| Argument | A required value, by position | the file name in cat notes.txt |
| Option | A named, often optional setting | --times 3 or --shout |
Here is how those pieces fit together when a user runs your tool.
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 -- --helpThe 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
Steps
Type
User enters the command
Parse
Split into command, args, options
Validate
Check types and required values
Run
Your action code executes
Output
Print result or error
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.
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 -- listNotice 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 code | Meaning | When to use it |
|---|---|---|
| 0 | Success | The job finished correctly |
| 1 | General error | Something failed, no special reason |
| 2 | Misuse | Bad 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, notConsole.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, andstderr. - Create one with
dotnet new console -o MyApp, thencd MyAppanddotnet run. - Top-level statements let you skip the boilerplate and start with one line of code.
- The plain
argsarray is the simplest way to read what the user types. Use--afterdotnet runto pass arguments. - System.CommandLine parses arguments, options, and sub-commands for you, and gives free
--helptext 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
gitanddotnetdo. - Always return a clear exit code:
0for success, non-zero for failure. Send errors tostderr.
References and further reading
- Create a .NET console application - Microsoft Learn
- Tutorial: Get started with System.CommandLine - Microsoft Learn
- System.CommandLine overview - Microsoft Learn
- Command-line syntax overview for System.CommandLine - Microsoft Learn
- Main() and command-line arguments - Microsoft Learn
- NuGet Gallery: System.CommandLine
Related Posts
Exploring C# File-Based Apps in .NET 10: Run a Single .cs File
Learn how C# file-based apps in .NET 10 let you run a single .cs file with dotnet run, add packages with directives, and grow into a full project.
Building File-Based Apps in .NET With Multi-File Support
Learn how to run C# without a project file using dotnet run app.cs in .NET 10, split code across files with #:include, and add packages with directives.
Run C# Scripts With dotnet run app.cs: No Project Files Needed
Learn to run C# scripts with dotnet run app.cs in .NET 10 — no project files, no setup. Add NuGet packages, pass arguments, and automate tasks fast.
The New .slnx Solution Format: A Simple Migration Guide for .NET 10
Learn the new XML-based .slnx solution format in .NET 10, why it beats the old .sln file, and how to migrate safely with one CLI command.
How to Start a New .NET Project in 2026: A Beginner's Friendly Guide
A warm, step-by-step guide to starting a new .NET 10 project in 2026 with the dotnet CLI, the right template, and good folder habits from day one.
How to Write Elegant Code with C# Pattern Matching
Learn C# pattern matching the easy way. Use is, switch expressions, property and list patterns to write clean, readable, and elegant .NET code.