Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Important
File-based apps are a feature of .NET 10, which is in preview. Some information relates to prerelease product that might be modified before release. Microsoft makes no warranties, express or implied, with respect to the information provided here.
File-based apps are programs contained within a single *.cs file that are built and run without a corresponding project (*.csproj) file. File-based apps are ideal for learning C# because they have less complexity: The entire program is stored in a single file. File-based apps are also useful for building command line utilities. On Unix platforms, file-based apps can be run using #! (shebang) directives.
In this tutorial, you:
- Create a file-based program.
- Add Unix shebang (
#!) support. - Read command line arguments.
- Handle standard input.
- Write ASCII art output.
- Process command line arguments.
- Use parsed command line results.
- Test the final application.
You build a file-based program that writes text as ASCII art. The app is contained in a single file, uses NuGet packages that implement some of the core features.
Prerequisites
- The .NET 10 preview SDK. Download it from the .NET download site.
- Visual Studio Code. Download it from the Visual Studio Code homepage.
- (Optional) The C# DevKit extension for Visual Studio Code. Download it from the Visual Studio Code marketplace.
Create a file-based program
Open Visual Studio Code and create a new file named
AsciiArt.cs. Enter the following text:Console.WriteLine("Hello, world!");Save the file. Then, open the integrated terminal in Visual Studio Code and type:
dotnet run AsciiArt.cs
The first time you run this program, the dotnet host builds the executable from your source file, stores build artifacts in a temporary folder, then runs the created executable. You can verify this experience by typing dotnet run AsciiArt.cs again. This time, the dotnet host determines that the executable is current, and runs the executable without building it again. You don't see any build output.
The preceding steps demonstrate that file-based apps aren't script files. They're C# source files that are built using a generated project file in a temporary folder. One of the lines of output displayed when you built the program should look something like this (on Windows):
AsciiArt succeeded (7.3s) → AppData\Local\Temp\dotnet\runfile\AsciiArt-85c58ae0cd68371711f06f297fa0d7891d0de82afde04d8c64d5f910ddc04ddc\bin\debug\AsciiArt.dll
On unix platforms, the output folder is something similar to:
AsciiArt succeeded (7.3s) → Library/Application Support/dotnet/runfile/AsciiArt-85c58ae0cd68371711f06f297fa0d7891d0de82afde04d8c64d5f910ddc04ddc/bin/debug/AsciiArt.dll
That output tells you where the temporary files and build outputs are placed. Throughout this tutorial, anytime you edit the source file, the dotnet host updates the executable before it runs.
File-based apps are regular C# programs. The only limitation is that they must be written in one source file. You can use top-level statements or a classic Main method as an entry point. You can declare any types: classes, interfaces, and structs. You can structure the algorithms in a file-based program the same as you would in any C# program. You can even declare multiple namespaces to organize your code. If you find a file-based program is growing too large for a single file, you can convert it to a project based program and split the source into multiple files. File-based apps are a great prototyping tool. You can start experimenting with minimal overhead to prove concepts and build algorithms.
Unix shebang (#!) support
Note
Support for #! directives applies on unix platforms only. There isn't a similar directive for Windows to directly execute a C# program. On Windows, you must use dotnet run on the command line.
On unix, you can run file-based apps directly, typing the source file name on the command line instead of dotnet run. You need to make two changes:
Set execute permissions on the source file:
chmod +x AsciiArt.csAdd a shebang (
#!) directive as the first line of theAsciiArt.csfile:#!/usr/local/share/dotnet/dotnet run
The location of dotnet can be different on different unix installations. Use the command whence dotnet to local the dotnet host in your environment.
After making these two changes, you can run the program from the command line directly:
./AsciiArt.cs
If you prefer, you can remove the extension so you can type ./AsciiArt instead. You can add the #! to your source file even if you use Windows. The Windows command line doesn't support #!, but the C# compiler allows that directive in file-based apps on all platforms.
Read command line arguments
Now, write all arguments on the command line to the output.
Replace the current contents of
AsciiArt.cswith the following code:if (args.Length > 0) { string message = string.Join(' ', args); Console.WriteLine(message); }You can run this version by typing the following command:
dotnet run AsciiArt.cs -- This is the command line.The
--option indicates that all following command arguments should be passed to the AsciiArt program. The argumentsThis is the command line.are passed as an array of strings, where each string is one word:This,is,the,command, andline..
This version demonstrates these new concepts:
- The command line arguments are passed to the program using the predefined variable
args. Theargsvariable is an array of strings:string[]. If the length ofargsis 0, that means no arguments were provided. Otherwise, each word on the argument list is stored in the corresponding entry in the array. - The
string.Joinmethod joins multiple strings into a single string, with the specified separator. In this case, the separator is a single space. - Console.WriteLine writes the string to the standard output console, followed by a new line.
Handle standard input
That handles command line arguments correctly. Now, add the code to handle reading input from standard input (stdin) instead of command line arguments.
Add the following
elseclause to theifstatement you added in the preceding code:else { while (Console.ReadLine() is string line && line.Length > 0) { Console.WriteLine(line); } }The preceding code reads the console input until either a blank line or a
nullis read. (The Console.ReadLine method returnsnullif the input stream is closed by typing ctrl+C.)Test reading standard input by creating a new text file in the same folder. Name the file
input.txtand add the following lines:Hello from ... dotnet! You can create file-based apps in .NET 10 and C# 14 Have fun writing useful utilitiesKeep the lines short so they format correctly when you add the feature to use ASCII art.
Run the program again.
With bash:
cat input.txt | dotnet run AsciiArt.csOr, with PowerShell:
Get-Content input.txt | dotnet run AsciiArt.cs
Now your program can accept either command line arguments or standard input.
Write ASCII Art output
Next, add a package that supports ASCII art, Colorful.Console. To add a package to a file-based program, you use the #:package directive.
Add the following directive after the
#!directive in your AsciiArt.cs file:#:package Colorful.Console@1.2.15Important
The version
1.2.15was the latest version of theColorful.Consolepackage when this tutorial was last updated. Check the package's NuGet page for the latest version to ensure you use a package version with the latest security fixes.Change the lines that call
Console.WriteLineto use theColorful.Console.WriteAsciimethod instead:async Task WriteAsciiArt(AsciiMessageOptions options) { foreach (string message in options.Messages) { Colorful.Console.WriteAscii(message); await Task.Delay(options.Delay); } }Run the program, and you see ASCII art output instead of echoed text.
Process command options
Next, let's add command line parsing. The current version writes each word as a different line of output. The command line arguments you added support two features:
Quote multiple words that should be written on one line:
AsciiArt.cs "This is line one" "This is another line" "This is the last line"Add a
--delayoption to pause between each line:AsciiArt.cs --delay 1000
Users should be able to use both arguments together.
Most command line applications need to parse command line arguments to handle options, commands, and user input effectively. The System.CommandLine library provides comprehensive capabilities to handle commands, subcommands, options, and arguments, allowing you to concentrate on what your application does rather than the mechanics of parsing command line input.
The System.CommandLine library offers several key benefits:
- Automatic help text generation and validation.
- Support for POSIX and Windows command-line conventions.
- Built-in tab completion capabilities.
- Consistent parsing behavior across applications.
Add the
System.CommandLinepackage. Add this directive after the existing package directive:#:package System.CommandLine@2.0.0-beta6Important
The version
2.0.0-beta6was the latest version when this tutorial was last updated. If there's a newer version available, use the latest version to ensure you have the latest security packages. Check the package's NuGet page for the latest version to ensure you use a package version with the latest security fixes.Add the necessary using statements at the top of your file (after the
#!and#:packagedirectives):using System.CommandLine; using System.CommandLine.Parsing;Define the delay option and messages argument. Add the following code to create the
CommandLine.OptionandCommandLine.Argumentobjects to represent the command line option and argument:Option<int> delayOption = new("--delay") { Description = "Delay between lines, specified as milliseconds.", DefaultValueFactory = parseResult => 100 }; Argument<string[]> messagesArgument = new("Messages") { Description = "Text to render." };In command-line applications, options typically begin with
--(double dash) and can accept arguments. The--delayoption accepts an integer argument that specifies the delay in milliseconds. ThemessagesArgumentdefines how any remaining tokens after options are parsed as text. Each token becomes a separate string in the array, but text can be quoted to include multiple words in one token. For example,"This is one message"becomes a single token, whileThis is four tokensbecomes four separate tokens.The preceding code defines the argument type for the
--delayoption, and that the arguments are an array ofstringvalues. This application has only one command, so you use the root command.Create a root command and configure it with the option and argument. Add the argument and option to the root command:
RootCommand rootCommand = new("Ascii Art file-based program sample"); rootCommand.Options.Add(delayOption); rootCommand.Arguments.Add(messagesArgument);Add the code to parse the command line arguments and handle any errors. This code validates the command line arguments and stores parsed arguments in the System.CommandLine.ParseResult object:
ParseResult result = rootCommand.Parse(args); foreach (ParseError parseError in result.Errors) { Console.Error.WriteLine(parseError.Message); } if (result.Errors.Count > 0) { return 1; }
The preceding code validates all command line arguments. If the validation fails, errors are written to the console, and the app exits.
Use parsed command line results
Now, finish the app to use the parsed options and write the output. First, define a record to hold the parsed options. File-based apps can include type declarations, like records and classes. They must be after all top-level statements and local functions.
Add a
recorddeclaration to store the messages and the delay option value:public record AsciiMessageOptions(string[] Messages, int Delay);Add the following local function before the record declaration. This method handles both command line arguments and standard input, and returns a new record instance:
async Task<AsciiMessageOptions> ProcessParseResults(ParseResult result) { int delay = result.GetValue(delayOption); List<string> messages = [.. result.GetValue(messagesArgument) ?? Array.Empty<string>()]; if (messages.Count == 0) { while (Console.ReadLine() is string line && line.Length > 0) { Colorful.Console.WriteAscii(line); await Task.Delay(delay); } } return new([.. messages], delay); }Create a local function to write the ASCII art with the specified delay. This function writes each message in the record with the specified delay between each message:
async Task WriteAsciiArt(AsciiMessageOptions options) { foreach (string message in options.Messages) { Colorful.Console.WriteAscii(message); await Task.Delay(options.Delay); } }Replace the
ifclause you wrote earlier with the following code that processes the command line arguments and write the output:var parsedArgs = await ProcessParseResults(result); await WriteAsciiArt(parsedArgs); return 0;
You created a record type that provides structure to the parsed command line options and arguments. New local functions create an instance of the record, and use the record to write the ASCII art output.
Test the final application
Test the application by running several different commands. If you have trouble, here's the finished sample to compare with what you built:
#!/usr/local/share/dotnet/dotnet run
#:package Colorful.Console@1.2.15
#:package System.CommandLine@2.0.0-beta6
using System.CommandLine;
using System.CommandLine.Parsing;
Option<int> delayOption = new("--delay")
{
Description = "Delay between lines, specified as milliseconds.",
DefaultValueFactory = parseResult => 100
};
Argument<string[]> messagesArgument = new("Messages")
{
Description = "Text to render."
};
RootCommand rootCommand = new("Ascii Art file-based program sample");
rootCommand.Options.Add(delayOption);
rootCommand.Arguments.Add(messagesArgument);
ParseResult result = rootCommand.Parse(args);
foreach (ParseError parseError in result.Errors)
{
Console.Error.WriteLine(parseError.Message);
}
if (result.Errors.Count > 0)
{
return 1;
}
var parsedArgs = await ProcessParseResults(result);
await WriteAsciiArt(parsedArgs);
return 0;
async Task<AsciiMessageOptions> ProcessParseResults(ParseResult result)
{
int delay = result.GetValue(delayOption);
List<string> messages = [.. result.GetValue(messagesArgument) ?? Array.Empty<string>()];
if (messages.Count == 0)
{
while (Console.ReadLine() is string line && line.Length > 0)
{
// <WriteAscii>
Colorful.Console.WriteAscii(line);
// </WriteAscii>
await Task.Delay(delay);
}
}
return new([.. messages], delay);
}
async Task WriteAsciiArt(AsciiMessageOptions options)
{
foreach (string message in options.Messages)
{
Colorful.Console.WriteAscii(message);
await Task.Delay(options.Delay);
}
}
public record AsciiMessageOptions(string[] Messages, int Delay);
In this tutorial, you learned to build a file-based program, where you build the program in a single C# file. These programs don't use a project file, and can use the #! directive on unix systems. Learners can create these programs after trying our online tutorials and before building larger project-based apps. File-based apps are also a great platform for command line utilities.