Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 1 – Start here: C#, .NET and your first programs

Console input, output, arguments and exit codes

Read standard input in C# until it ends, parse numbers safely, keep results and errors apart, set exit codes, and print numbers the same on every computer.

  • Beginner
  • 15 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Read lines from standard input until Console.ReadLine returns null
  • Parse numbers from text with TryParse, NumberStyles and an explicit culture
  • Separate results from problems with standard output, standard error and the exit code
  • Format and parse numbers with the invariant culture so that output does not depend on the computer
  • Validate command-line arguments and answer misuse with a usage message and exit status 2

Before you start

What you’ll learn · Before you start

What you will learn

  • Read lines from standard input until Console.ReadLine returns null
  • Parse numbers from text with TryParse, NumberStyles and an explicit culture
  • Separate results from problems with standard output, standard error and the exit code
  • Format and parse numbers with the invariant culture so that output does not depend on the computer
  • Validate command-line arguments and answer misuse with a usage message and exit status 2

Before you start

On this page

The same eight lines of C# printed the total 1,234,567.50 as 1,234,567.50 under the culture of the United States, 12,34,567.50 under India’s and 1.234.567,50 under Germany’s, and read the price 2.50 as 250 under the last. A console program has more to get right than its arithmetic: where its input ends, what to do with a line that is not a number, where its errors go, what its exit code tells the script that ran it, and which culture: The conventions of a language and region, such as en-IN or de-DE, that .NET uses to format and parse numbers, dates and money. CultureInfo.CurrentCulture follows the settings of the computer. shapes every number it reads and writes.

This lesson builds a small marks tool step by step: a loop that stops when the input ends, parsing that turns bad text into a decision instead of a crash, results and problems on separate streams, numbers in the invariant culture, and arguments checked before any work starts.

Standard input ends when ReadLine returns null

A program reads its standard input: The stream a program reads its input from. It is the keyboard by default, and a shell can connect it to a file with < or to another program's output with |; Console.ReadLine reads it line by line. without knowing where it comes from: the keyboard, a file after < on the command line, or another program after |. Console.ReadLine() returns the next line without its line ending, and once no lines are left, it returns null. That is why its type is string?. This program adds up the marks it reads, one per line, until the input ends:

total.cs, reading marks.txt C# · total/total.cs
// Adds up the marks on standard input, one per line, until the input ends.
int count = 0;
int total = 0;
string? line;
while ((line = Console.ReadLine()) is not null)
{
    if (string.IsNullOrWhiteSpace(line))
    {
        continue; // a blank line holds no mark
    }
    total += int.Parse(line);
    count++;
}
Console.WriteLine($"{count} marks, total {total}");

Input (standard input)

72
85

90

Output

3 marks, total 247

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run total.cs

The input had four lines, and the program counted 3 marks with a total of 247. The while condition reads and tests in one go: the assignment puts the next line into line, and the comparison with null ends the loop where the input ends. A blank line is not the end of the input: ReadLine returns "", an empty string, so the program skips it with string.IsNullOrWhiteSpace. Step through the run:

Interactive · stage by stage total.cs reading marks.txt line by line Step through the four lines of marks.txt and the end of the input. The arrows aside are the ways back to the next ReadLine.
total.cs reading marks.txt line by line, step 7 of 7. Showing Console.ReadLine() (null), Is it null? (yes: the loop ends), Is it blank?, Parse and add (count 3, total 247). In focus: Is it null?.next lineskip itConsole.ReadLine()nullIs it null?yes: the loop endsIs it blank?"" or only spacesParse and addcount 3, total 247prints 3 marks, total 247

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Line 1. ReadLine returns "72", the first line of marks.txt, without its line ending. It is not null, and it is not blank.
  2. Add 72. int.Parse turns "72" into 72. total becomes 72 and count 1, and the loop goes back for the next line.
  3. Line 2. ReadLine returns "85". After it, total is 157 and count is 2.
  4. A blank line. Line 3 is empty, so ReadLine returns "", an empty string, which is not null. string.IsNullOrWhiteSpace says it is blank, and continue goes straight back to ReadLine.
  5. Line 4. ReadLine returns "90": total becomes 247 and count 3.
  6. The input has ended. There is no line 5, so ReadLine returns null. The while condition is false, the loop ends, and nothing tries to parse the null.
  7. Print. The line after the loop prints 3 marks, total 247: three marks, because the blank line was skipped, not counted.

Reading a fixed number of lines is the mistake this loop avoids. This program expects three marks and gets two:

three.cs: three reads, two lines of input C# · three/three.cs
// Expects exactly three marks on standard input, and gets two.
int total = 0;
for (int i = 0; i < 3; i++)
{
    string? line = Console.ReadLine();
    total += int.Parse(line!); // the ! hides that line is null once the input has ended
}
Console.WriteLine($"Total: {total}");

Input (standard input)

72
85

Output (exit status 134)

Printed as an error (standard error)

Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 's')
   at System.Int32.Parse(String s)
   at Program.<Main>$(String[] args) in ./three.cs:line 6

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run three.cs

The third ReadLine returned null, and int.Parse stopped the program with an ArgumentNullException at line 6, exit status 134. The ! after line had told the compiler not to warn about exactly that, as the previous lesson showed.

Counting lines you have not read yet

for (int i = 0; i < 3; i++) over Console.ReadLine() breaks on any input with fewer lines, and silently ignores any extra lines. Loop until ReadLine returns null, and count what you actually read.

Checkpoint

Check: Reading standard input until it ends

  1. Question 1 of 2 What does Console.ReadLine() return once the input has no lines left?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: null

    Microsoft's reference says ReadLine returns the next line, or null if no more lines are available. Loop until null, and the loop ends exactly where the input does.

  2. Question 2 of 2 total.cs reads the four lines 72, 85, an empty line and 90. How many marks does it count?

    Type a number.

    A hint

    What does string.IsNullOrWhiteSpace say about the empty line?

    Show the answer: Question 2 of 2

    Answer: 3 marks

    The empty line is read as "", which is not null, so the loop goes on; IsNullOrWhiteSpace skips it with continue. The run printed 3 marks, total 247.

TryParse turns bad text into a decision

int.Parse in total.cs trusts its input: a line such as 9O, typed with the letter O, would stop the program with a FormatException. int.TryParse takes the same text and answers with a bool instead: true and the number, or false when the text is not one, as Microsoft’s guide to parsing numbers describes. This program tries both on eight pieces of text:

parse.cs: int.Parse and int.TryParse side by side C# · parse/parse.cs
using System.Globalization;

// What int.Parse and int.TryParse make of eight pieces of text.
string[] inputs = ["42", " 42 ", "-7", "4 2", "", "9O", "3000000000", "1,250"];
Console.WriteLine($"{"Text",-14} {"int.Parse",-18} int.TryParse");
foreach (string text in inputs)
{
    string parsed;
    try
    {
        parsed = int.Parse(text, CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture);
    }
    catch (Exception e) when (e is FormatException or OverflowException)
    {
        parsed = e.GetType().Name;
    }
    bool ok = int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value);
    string tried = $"{ok}, {value.ToString(CultureInfo.InvariantCulture)}";
    string shown = $"\"{text}\"";
    Console.WriteLine($"{shown,-14} {parsed,-18} {tried}");
}
bool grouped = int.TryParse("1,250", NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out int withGroups);
Console.WriteLine($"With NumberStyles.AllowThousands, \"1,250\" gives {grouped}, {withGroups.ToString(CultureInfo.InvariantCulture)}");

Output

Text           int.Parse          int.TryParse
"42"           42                 True, 42
" 42 "         42                 True, 42
"-7"           -7                 True, -7
"4 2"          FormatException    False, 0
""             FormatException    False, 0
"9O"           FormatException    False, 0
"3000000000"   OverflowException  False, 0
"1,250"        FormatException    False, 0
With NumberStyles.AllowThousands, "1,250" gives True, 1250

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run parse.cs

Each row follows from the rules of the default style for integers, NumberStyles.Integer, which allows digits, white space before and after, and a sign in front. So " 42 " and "-7" parse, while "4 2", "" and "9O" throw a FormatException. "3000000000" is a well-formed number that does not fit: an int holds at most 2,147,483,647, so it throws an OverflowException instead. "1,250" fails because a group separator is not allowed until you add NumberStyles.AllowThousands, as the last line shows. In every failing row TryParse returned false and set its out variable to 0, the False, 0 of the last column, so the program could decide what to do next instead of crashing.

The second argument of each call, CultureInfo.InvariantCulture, decides which characters count as the decimal and group separators. Without it, parsing uses the culture of the computer, and a later section shows what that costs.

Key idea

Use TryParse for any text you did not write yourself, pass the NumberStyles you accept and a culture, and decide in code what a line that fails means.

Checkpoint

Check: Parse and TryParse

  1. Question 1 of 2 What does int.Parse("3000000000") do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It throws an OverflowException: the number is larger than an int can hold

    int.Parse throws FormatException for text that is not a number and OverflowException for a number outside the range of int. int.TryParse returns false for both.

  2. Question 2 of 2 int.TryParse("1,250", NumberStyles.Integer, CultureInfo.InvariantCulture, out int n) returns false. What makes it accept the text?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A style that includes NumberStyles.AllowThousands

    NumberStyles says which parts a string may have. NumberStyles.Integer, the default for integers, has no group separator, so add AllowThousands when the input may contain one.

Results go to standard output, problems to standard error

A program has two output streams. standard output: The stream where a program writes its results, with Console.Write and Console.WriteLine. A shell can send it to a file with > or to another program with |., where Console.WriteLine writes, is for the results; standard error: A second output stream, for messages about problems, written with Console.Error. It reaches the terminal even when standard output goes to a file, and a shell redirects it on its own with 2>., where Console.Error.WriteLine writes, is for messages about problems. Both appear in a terminal, but a shell can send each one somewhere else, so a result saved to a file never has an error message mixed into it. This version of the marks tool reports every line it cannot use and ends with an exit status that says whether the input was clean:

sum.cs: results, problems and an exit status C# · sum/sum.cs
using System.Globalization;

// Adds up the marks on standard input and reports, on standard error, every line it cannot use.
int count = 0, total = 0, bad = 0, lineNumber = 0;
string? line;
while ((line = Console.ReadLine()) is not null)
{
    lineNumber++;
    if (string.IsNullOrWhiteSpace(line))
    {
        continue;
    }
    if (int.TryParse(line, NumberStyles.Integer, CultureInfo.InvariantCulture, out int mark))
    {
        total += mark;
        count++;
    }
    else
    {
        Console.Error.WriteLine($"line {lineNumber}: not a whole number: \"{line}\"");
        bad++;
    }
}
Console.WriteLine($"{count} marks, total {total}");
return bad == 0 ? 0 : 1;

Input (standard input)

72
85
9O

90
absent

Output (exit status 1)

3 marks, total 247

Printed as an error (standard error)

line 3: not a whole number: "9O"
line 6: not a whole number: "absent"

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run sum.cs

The run printed 3 marks, total 247 on standard output and two lines on standard error, for lines 3 and 6, and its last line, return bad == 0 ? 0 : 1;, made the exit status 1. Step through where each line went:

Interactive · step by step Where sum.cs sends what it prints Step through one run on marks.txt. Watch which stream each line goes to, and what the shell gets at the end.
Where sum.cs sends what it prints, step 6 of 6. Showing marks.txt, sum.cs, Your shell (exit status 1), Standard output (3 marks, total 247), Standard error (lines 3 and 6). In focus: Standard output, Standard error.standard inputresultsproblemsexit statusmarks.txtsum.csYour shellexit status 1Standard output3 marks, total 247Standard errorlines 3 and 6> report.txt

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The shell starts it. dotnet run sum.cs < marks.txt starts the program with the file on its standard input. The program never opens the file: it calls Console.ReadLine.
  2. A line it cannot use. Lines 1 and 2 are marks. Line 3, 9O, is not a whole number, so TryParse returns false and the program writes a line about it to Console.Error, standard error.
  3. Another one. Line 4 is blank and skipped, line 5 is 90, and line 6, absent, goes to standard error too. Neither bad line stops the program.
  4. The result. The loop ends at the end of the input, and Console.WriteLine writes the result to standard output: 3 marks, total 247.
  5. The exit status. return 1 ends the program with exit status 1, because two lines were bad. The shell gets the status, so a command after && does not run, and a script can test it.
  6. Redirected. With > report.txt 2> problems.txt on the command line, the shell sends each stream to its own file: report.txt gets the result, problems.txt the two bad lines.

The shell does the sending. In Bash and zsh, the Bash manual’s redirections work like this:

You type Standard output goes to Standard error goes to
dotnet run sum.cs < marks.txt the terminal the terminal
… > report.txt report.txt the terminal
… > report.txt 2> problems.txt report.txt problems.txt
… 2>&1 | tee all.txt the next program, with standard error mixed in the same place as standard output

The exit status is the third channel, and the one scripts read: 0 for success and anything else for failure, which &&, if and build servers test. Return it from your top-level code, as sum.cs does. The other way, setting Environment.ExitCode and letting the program run to its end, works only when the top-level code returns no number: Microsoft’s reference says the property is ignored when Main returns a value.

Text beyond ASCII on Windows

On macOS with a UTF-8 locale, as in this lesson’s sessions, console output is UTF-8. A Windows console starts with the code page of the system’s locale, so a program that prints text such as नमस्ते sets Console.OutputEncoding = System.Text.Encoding.UTF8; before its first Console.Write, and the console needs a font that has those characters.

Checkpoint

Check: Standard output, standard error and the exit status

  1. Question 1 of 2 You run dotnet run sum.cs < marks.txt > report.txt. What does report.txt contain, and what still appears in the terminal?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: report.txt holds 3 marks, total 247; the two lines about lines 3 and 6 appear in the terminal

    Results on standard output and problems on standard error let a shell keep them apart: > for the first, 2> for the second.

  2. Question 2 of 2 Why does sum.cs end with return bad == 0 ? 0 : 1;?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: So that a script or build server can tell from the exit status alone whether every line was a mark

    The exit status is the channel scripts test with &&, if and build servers. The run on marks.txt ended with 1 because two lines were bad.

Culture decides how numbers look, so choose it yourself

Every formatting and parsing call that is not given a culture uses CultureInfo.CurrentCulture, which comes from the settings of the computer the program runs on. On the Mac that ran this lesson’s sessions, the variable LC_ALL set it, which lets this session run the same program as if on four computers set up for four countries, then a second program, invariant, that names its culture itself:

culture/Program.cs C# · culture/Program.cs
using System.Globalization;

// Prints a total and reads a price the way the culture of the computer says.
decimal total = 1234567.5m;
Console.WriteLine($"Culture:  {CultureInfo.CurrentCulture.Name}");
Console.WriteLine($"Total:    {total:N2}");
Console.WriteLine($"As money: {total:C}");
Console.WriteLine($"Parsed:   {decimal.Parse("2.50")}");
invariant/Program.cs C# · invariant/Program.cs
using System.Globalization;

// The same total and price, printed and read with the invariant culture.
CultureInfo invariant = CultureInfo.InvariantCulture;
decimal total = 1234567.5m;
Console.WriteLine($"Culture:  {CultureInfo.CurrentCulture.Name}");
Console.WriteLine(string.Create(invariant, $"Total:    {total:N2}"));
Console.WriteLine(string.Create(invariant, $"Parsed:   {decimal.Parse("2.50", invariant)}"));
One program, four cultures, then the invariant culture Python · culture_session.py
from session import run

# culture formats and parses with the culture of the computer; invariant passes CultureInfo.InvariantCulture. Each
# program runs with LC_ALL set to another culture, as on four computers set up for four countries.
run("""
cd culture
dotnet build -v q
LC_ALL=en_US.UTF-8 dotnet bin/Debug/net10.0/Culture.dll
LC_ALL=en_IN.UTF-8 dotnet bin/Debug/net10.0/Culture.dll
LC_ALL=de_DE.UTF-8 dotnet bin/Debug/net10.0/Culture.dll
!LC_ALL=fr_FR.UTF-8 dotnet bin/Debug/net10.0/Culture.dll
cd ../invariant
dotnet build -v q
LC_ALL=de_DE.UTF-8 dotnet bin/Debug/net10.0/Invariant.dll
LC_ALL=fr_FR.UTF-8 dotnet bin/Debug/net10.0/Invariant.dll
""")

Output

$ cd culture
$ dotnet build -v q

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed …
$ LC_ALL=en_US.UTF-8 dotnet bin/Debug/net10.0/Culture.dll
Culture:  en-US
Total:    1,234,567.50
As money: $1,234,567.50
Parsed:   2.50
$ LC_ALL=en_IN.UTF-8 dotnet bin/Debug/net10.0/Culture.dll
Culture:  en-IN
Total:    12,34,567.50
As money: ₹12,34,567.50
Parsed:   2.50
$ LC_ALL=de_DE.UTF-8 dotnet bin/Debug/net10.0/Culture.dll
Culture:  de-DE
Total:    1.234.567,50
As money: 1.234.567,50 €
Parsed:   250
$ LC_ALL=fr_FR.UTF-8 dotnet bin/Debug/net10.0/Culture.dll
Culture:  fr-FR
Total:    1 234 567,50
As money: 1 234 567,50 €
Unhandled exception. System.FormatException: The input string '2.50' was not in a correct format.
   at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
   at System.Number.ParseDecimal[TChar](ReadOnlySpan`1 value, NumberStyles styles, NumberFormatInfo info)
   at Program.<Main>$(String[] args) in culture/Program.cs:line 8
[exit status 134]
$ cd ../invariant
$ dotnet build -v q

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed …
$ LC_ALL=de_DE.UTF-8 dotnet bin/Debug/net10.0/Invariant.dll
Culture:  de-DE
Total:    1,234,567.50
Parsed:   2.50
$ LC_ALL=fr_FR.UTF-8 dotnet bin/Debug/net10.0/Invariant.dll
Culture:  fr-FR
Total:    1,234,567.50
Parsed:   2.50

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 culture_session.py

The total printed as 1,234,567.50 in en-US, 12,34,567.50 in en-IN, 1.234.567,50 in de-DE and with spaces in fr-FR. Parsing was worse. decimal.Parse("2.50") gave 2.50 in en-US and en-IN, 250 in de-DE, where the dot groups thousands, and in fr-FR it threw a FormatException that ended the program. A data file read on the wrong computer is either rejected or, worse, read wrong without a word. The second program passes CultureInfo.InvariantCulture, the invariant culture: CultureInfo.InvariantCulture: a culture tied to no country that formats and parses the same way on every computer, with a dot for decimals and a comma between thousands. Use it for text that programs exchange., to every call, and printed 1,234,567.50 and 2.50 under de-DE and fr-FR alike.

The Indian grouping

The en-IN culture groups digits in lakhs and crores: 12,34,567.50 is 12 lakh 34 thousand 567.50, the number that en-US writes as 1,234,567.50, and its currency format added the ₹ sign. Use it for amounts that people in India read on screen, never for files that programs read back.

The rule that follows from those runs:

The text is for Format and parse with Because
a person, on their own screen CultureInfo.CurrentCulture, the default they expect their own separators
a file, a log, another program CultureInfo.InvariantCulture it reads the same on every computer
a known audience, such as an Indian report CultureInfo.GetCultureInfo("en-IN") the format is fixed by the reader, not the computer

For interpolated strings, string.Create(CultureInfo.InvariantCulture, $"…") formats every value in the string with the invariant culture, as the second program does.

Checkpoint

Check: Formatting and parsing with a culture

  1. Question 1 of 2 Under the de-DE culture, what number did decimal.Parse("2.50") return in the session?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 250

    In German the dot groups thousands and the comma marks decimals, so "2.50" was read as 250: a price a hundred times too large, with no error at all.

  2. Question 2 of 2 A tool writes totals to a CSV file that another program reads. Which culture should it format the numbers with?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: CultureInfo.InvariantCulture

    Text for programs (files, logs, protocols) uses the invariant culture; text that a person reads on their own screen may use their culture.

Arguments are checked before any work starts

A tool learns what to do from its arguments, and a wrong argument is a different failure from bad data: the person who typed the command made a mistake, and the right answer is a short usage line. grades takes a pass mark as its only argument, reads marks from standard input and counts how many reach it:

grades/Program.cs C# · grades/Program.cs
using System.Globalization;

// grades <pass-mark>: reads one mark per line from standard input and counts how many reach the pass mark.
// Exit status: 0 when every line was a mark, 1 when a line was not, 2 when the arguments are wrong.
if (args.Length != 1
    || !int.TryParse(args[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int passMark)
    || passMark is < 0 or > 100)
{
    Console.Error.WriteLine("usage: grades <pass-mark>, a whole number from 0 to 100");
    return 2;
}

int passed = 0, failed = 0, bad = 0, lineNumber = 0;
string? line;
while ((line = Console.ReadLine()) is not null)
{
    lineNumber++;
    if (string.IsNullOrWhiteSpace(line))
    {
        continue;
    }
    if (int.TryParse(line, NumberStyles.Integer, CultureInfo.InvariantCulture, out int mark) && mark is >= 0 and <= 100)
    {
        if (mark >= passMark)
        {
            passed++;
        }
        else
        {
            failed++;
        }
    }
    else
    {
        Console.Error.WriteLine($"line {lineNumber}: not a mark from 0 to 100: \"{line}\"");
        bad++;
    }
}
Console.WriteLine($"{passed} passed, {failed} failed (pass mark {passMark})");
return bad == 0 ? 0 : 1;

The first statement checks the argument three ways: exactly one, a whole number, and from 0 to 100. Step through the checks for three ways of starting it:

Interactive · step by step How grades checks its argument before it reads a line Step through the three runs of the session. A check that fails sends the run to the usage message.
How grades checks its argument before it reads a line, step 4 of 4. Showing Exactly one argument? (1 argument), A whole number? (40), From 0 to 100? (40 is in range), Read the marks (2 passed, 1 failed), Usage message. In focus: Read the marks.yesyesyesnononoExactly one argument?1 argumentA whole number?40From 0 to 100?40 is in rangeRead the marks2 passed, 1 failedUsage messagestandard error, exit 2

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. No argument. dotnet run with no argument gives args.Length 0. The first check fails, so the program prints its usage line to standard error and returns 2 before it reads any input.
  2. A word, not a number. dotnet run -- forty gives one argument, so the count is right, but int.TryParse cannot read forty as a number. The same usage line, the same exit status 2.
  3. A number in range. dotnet run -- 40 passes all three checks: one argument, a whole number, from 0 to 100. Only now does the program start reading marks from standard input.
  4. The work itself. On clean.txt it prints 2 passed, 1 failed and exits with 0. On marks.txt it also reports lines 3 and 6 on standard error and exits with 1: a data problem, not a usage problem.

The session runs the tool as a user would. The output of the last run goes to two files, so its two streams can be read apart:

grades: two usage errors, a clean run and a data error Python · grades_session.py
from session import run

# grades run five times: with no argument, with a word for the pass mark, on clean input, and on input with two bad
# lines, first to the terminal and then with its two output streams sent to two files.
run("""
cd grades
dotnet build -v q
!dotnet run
!dotnet run -- forty
dotnet run -- 40 < clean.txt
!dotnet run -- 40 < marks.txt
!dotnet run -- 40 < marks.txt > passed.txt 2> problems.txt
cat passed.txt
cat problems.txt
""")

Output

$ cd grades
$ dotnet build -v q

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed …
$ dotnet run
usage: grades <pass-mark>, a whole number from 0 to 100
[exit status 2]
$ dotnet run -- forty
usage: grades <pass-mark>, a whole number from 0 to 100
[exit status 2]
$ dotnet run -- 40 < clean.txt
2 passed, 1 failed (pass mark 40)
$ dotnet run -- 40 < marks.txt
line 3: not a mark from 0 to 100: "9O"
line 6: not a mark from 0 to 100: "105"
2 passed, 1 failed (pass mark 40)
[exit status 1]
$ dotnet run -- 40 < marks.txt > passed.txt 2> problems.txt
[exit status 1]
$ cat passed.txt
2 passed, 1 failed (pass mark 40)
$ cat problems.txt
line 3: not a mark from 0 to 100: "9O"
line 6: not a mark from 0 to 100: "105"

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 grades_session.py

No argument and the word forty both printed the usage line and ended with exit status 2. The clean input passed with 0. marks.txt held 9O and 105, so the tool reported both on standard error, still counted the rest and ended with 1. With > passed.txt 2> problems.txt, the result and the problems landed in separate files. Three exit statuses now mean three things: 0 success, 1 bad data, 2 wrong usage. Bash itself uses 2 the same way: its manual says its builtins return 2 to indicate incorrect usage.

Marks Percentage Calculator (India) Check a pass mark or a percentage by hand before you write it into a tool.

Checkpoint

Check: Checking command-line arguments

  1. Question 1 of 2 With which exit status does grades end when it is run with no argument?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 2

    A wrong or missing argument is a usage error: grades prints its usage line on standard error and returns 2, the status Bash's own builtins use for incorrect usage.

  2. Question 2 of 2 Why does grades check its argument before it reads a single line of input?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A wrong argument means the whole run is wrong, so it fails at once, before reading or writing anything

    Validate arguments first, fail fast with a usage line and status 2, and only then start the work, which can still fail with status 1 on bad data.

Practise: a marks summary

Write a file-based app that reads lines such as 101,Asha,72 from standard input: a roll number, a name and a mark. It prints how many lines were valid, the average to two decimals in the invariant culture and the name of the topper; it reports every invalid line on standard error with its line number; and it exits with 1 if any line was invalid. Run it on clean input first, then add a blank line, a mark of 9O, a line with two fields and an empty input, and check its exit status each time with echo $?.

Then work through the questions on the five ideas of this lesson: weaker ideas come first, and every answer arrives with its reason:

Practice that adapts to you

Practise this lesson

12 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 What is the type of Console.ReadLine()'s result, with nullable reference types switched on?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: string?, because it returns null at the end of the input

    string? warns you, through the nullable warnings, wherever the result is used without a null check.

  2. Question 2 of 12 three.cs reads exactly three lines with int.Parse(line!), and its input has two. What happens?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: The third ReadLine returns null and int.Parse throws an ArgumentNullException at line 6

    Read until null instead of a fixed number of times, and count what you read.

  3. Question 3 of 12 cat marks.txt | dotnet run total.cs and dotnet run total.cs < marks.txt both work. What does the program have to change between them?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: Nothing: either way the shell connects marks.txt to its standard input, and ReadLine reads it

    Standard input hides where the text comes from, which is what makes a tool usable from files, pipes and the keyboard alike.

  4. Question 4 of 12 What does int.TryParse do when the text is not a number?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: It returns false and sets its out variable to 0

    Parse throws when it fails, and TryParse returns false, so the caller decides what a bad line means.

  5. Question 5 of 12 With the default NumberStyles.Integer, which text does int.Parse accept?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: " 42 ", with a space before and after

    NumberStyles.Integer is digits, leading and trailing white space and a leading sign. Anything else needs another style, or is not a number.

  6. Question 6 of 12 Why should a console tool write its error messages with Console.Error.WriteLine rather than Console.WriteLine?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: So that the messages do not mix with the results when standard output is sent to a file or another program

    Standard output carries results and standard error carries messages about problems, so a shell can route each one on its own.

  7. Question 7 of 12 A build step runs dotnet run sum.cs < marks.txt && echo imported. sum.cs reports two bad lines. What does the step print last, and why?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: It prints the result and the two problem lines but not imported, because the exit status is 1

    A non-zero exit status stops && chains and fails build steps, which is why a tool returns one when it could not do all of its job.

  8. Question 8 of 12 Which culture does decimal.Parse("2.50") use when no culture is passed?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: CultureInfo.CurrentCulture, from the settings of the computer it runs on

    Without a culture, formatting and parsing follow the computer's culture, so pass one wherever the text is not for a person's own screen.

  9. Question 9 of 12 What happened when the culture program parsed "2.50" under fr-FR?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: decimal.Parse threw a FormatException and the program ended with exit status 134

    The same input was right in two cultures, wrong by a factor of 100 in one and an exception in another: parse text from files with an explicit culture.

  10. Question 10 of 12 Which call prints a decimal named total as 1,234,567.50 on every computer?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: total.ToString("N2", CultureInfo.InvariantCulture)

    Pass the culture to ToString, Parse and string.Create. The invariant program printed 1,234,567.50 under de-DE and fr-FR alike.

  11. Question 11 of 12 Where does grades write its usage line?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: To standard error, with exit status 2

    Wrong usage: a short usage line on standard error and exit status 2, before any work.

  12. Question 12 of 12 A nightly script runs grades and must tell three outcomes apart. Which design lets it?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: Exit status 0 for success, 1 for bad data, 2 for wrong usage, and the details on standard error

    A small, documented set of exit statuses is the tool's contract with its callers: success, a data problem, a usage problem.

Interview questions

Fresher · Why does Console.ReadLine() return a nullable string, string??

Because it has two kinds of answer. While input remains, it returns the next line without its line ending, which can be "" for an empty line. When no lines are left, because a redirected file or a pipe has ended or the user ended the input at the keyboard, with Ctrl+D at the start of a line on macOS and Linux or Ctrl+Z then Enter on Windows, it returns null. With nullable reference types switched on, string? makes the compiler warn wherever you use the result without checking it, as CS8602 does for line.Trim(). The usual loop is while ((line = Console.ReadLine()) is not null) { … }, which stops exactly at the end of the input and treats an empty line as a line.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A price import parses values such as 2.50 from a supplier's CSV file. It worked for months; then a new server set up for Germany imported every price 100 times too high, without a single error. Explain what happened, how you fix it, and how you find the rows already imported wrongly.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    Your C# command-line tool runs in shell pipelines and CI jobs. Users complain that error messages end up inside the output files and that failed runs still let the next step start. Redesign the tool's contract with its callers.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Console.ReadLine() returns null when the input has ended, so read in a loop until null and treat "" as a blank line, not as the end.
  • int.Parse throws FormatException or OverflowException on text it cannot read, while TryParse returns false, so bad input becomes a decision in your code.
  • Results go to standard output and problems to standard error, so that a shell can send them to different places, and the exit status tells the caller whether it worked.
  • Without a culture, formatting and parsing follow the computer: 2.50 became 250 in de-DE, so files and logs use CultureInfo.InvariantCulture.
  • Check arguments before any work and answer misuse with a usage line on standard error and exit status 2, keeping 1 for bad data and 0 for success.

Terms in this lesson

culture
The conventions of a language and region, such as en-IN or de-DE, that .NET uses to format and parse numbers, dates and money. CultureInfo.CurrentCulture follows the settings of the computer.
invariant culture
CultureInfo.InvariantCulture: a culture tied to no country that formats and parses the same way on every computer, with a dot for decimals and a comma between thousands. Use it for text that programs exchange.
standard error
A second output stream, for messages about problems, written with Console.Error. It reaches the terminal even when standard output goes to a file, and a shell redirects it on its own with 2>.
standard input
The stream a program reads its input from. It is the keyboard by default, and a shell can connect it to a file with < or to another program's output with |; Console.ReadLine reads it line by line.
standard output
The stream where a program writes its results, with Console.Write and Console.WriteLine. A shell can send it to a file with > or to another program with |.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.